File sh-3.12.0.obscpio of Package shfmt

07070100000000000081A4000000000000000000000001686AE5C000000051000000000000000000000000000000000000001900000000sh-3.12.0/.gitattributes# To prevent CRLF breakages on Windows for fragile files, like testdata.
* -text
07070100000001000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001200000000sh-3.12.0/.github07070100000002000081A4000000000000000000000001686AE5C00000000E000000000000000000000000000000000000001E00000000sh-3.12.0/.github/FUNDING.ymlgithub: mvdan
07070100000003000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001C00000000sh-3.12.0/.github/workflows07070100000004000081A4000000000000000000000001686AE5C00000218D000000000000000000000000000000000000002500000000sh-3.12.0/.github/workflows/test.ymlon: [push, pull_request]
name: Test
jobs:
  test:
    strategy:
      matrix:
        go-version: [1.23.x, 1.24.x]
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-go@v5
      with:
        go-version: ${{ matrix.go-version }}
        cache: false

    - run: go test ./...

    - run: go test -race ./...
      if: matrix.os == 'ubuntu-latest'
    - run: GOARCH=386 go test -count=1 ./...
      if: matrix.os == 'ubuntu-latest'
    - name: confirm tests with Bash 5.2
      run: |
        go install mvdan.cc/dockexec@latest
        CGO_ENABLED=0 go test -run TestRunnerRunConfirm -exec 'dockexec bash:5.2' ./interp
      if: matrix.os == 'ubuntu-latest'

    # Test that we can build for platforms that we can't currently test on.
    - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.24.x'
      run: |
        GOOS=plan9 GOARCH=amd64 go build ./...
        GOOS=js GOARCH=wasm go build ./...

    # Static checks from this point forward. Only run on one Go version and on
    # Linux, since it's the fastest platform, and the tools behave the same.
    - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.24.x'
      run: diff <(echo -n) <(gofmt -s -d .)
    - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.24.x'
      run: go vet ./...

  test-linux-alpine:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Test as root, without cgo, and with busybox
      run: docker run -v="$PWD:/pwd" -w=/pwd -e=CGO_ENABLED=0 golang:1.24.0-alpine go test ./...

  docker:
    name: Build and test Docker images
    # Only deploy if previous stages pass.
    needs: [test, test-linux-alpine]
    runs-on: ubuntu-latest
    services:
      registry:
        image: registry:2
        ports:
          - 5000:5000
        # this is needed because we restart the docker daemon for experimental
        # support
        options: "--restart always"
    env:
      # Export environment variables for all stages.
      DOCKER_USER: ${{ secrets.DOCKER_USER }}
      DOCKER_DEPLOY_IMAGES: false
      DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
      DOCKER_REPO: shfmt
      # We use all platforms for which FROM images in our Dockerfile are
      # available.
      DOCKER_PLATFORMS: >
        linux/386
        linux/amd64
        linux/arm/v7
        linux/arm64/v8
        linux/ppc64le

      # linux/s390x TODO: reenable when we figure out its weird errors when
      # fetching dependencies, including:
      #
      # zip: checksum error
      # Get "https://proxy.golang.org/...": local error: tls: bad record MAC
      # Get "https://proxy.golang.org/...": local error: tls: unexpected message
      # Get "https://proxy.golang.org/...": x509: certificate signed by unknown authority
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0 # also fetch tags for 'git describe'
    # Enable docker daemon experimental support (for 'pull --platform').
    - name: Enable experimental support
      run: |
        config='/etc/docker/daemon.json'
        if [[ -e "$config" ]]; then
          sudo sed -i -e 's/{/{ "experimental": true, /' "$config"
        else
          echo '{ "experimental": true }' | sudo tee "$config"
        fi
        sudo systemctl restart docker
    - uses: docker/setup-qemu-action@v3
    - uses: docker/setup-buildx-action@v3
      with:
        driver-opts: network=host
    - name: Set up env vars
      run: |
        set -vx
        # Export environment variable for later stages.
        if echo "$GITHUB_REF" | grep -q '^refs/heads/master$'; then
          # Pushes to the master branch deploy 'latest'.
          echo "TAG=latest" >> $GITHUB_ENV
        elif echo "$GITHUB_REF" | grep -q '^refs/heads/docker-push-test$'; then
          # Pushes to the test branch deploy 'latest-test'.
          echo "TAG=latest-test" >> $GITHUB_ENV
        elif echo "$GITHUB_REF" | grep -q '^refs/tags/'; then
          # Pushes to a git tag use it as the docker tag.
          echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
        else
          # Otherwise, we build and test the image locally, but we don't push it.
          echo "TAG=${GITHUB_SHA::8}" >> $GITHUB_ENV
        fi
        echo "DOCKER_BASE=test/${{ env.DOCKER_REPO }}" >> $GITHUB_ENV
        echo "DOCKER_BUILD_PLATFORMS=${DOCKER_PLATFORMS// /,}" >> $GITHUB_ENV
    - name: Build and push to local registry
      uses: docker/build-push-action@v5
      with:
        provenance: false # temporarily work around https://github.com/containers/skopeo/issues/1874
        context: .
        file: ./cmd/shfmt/Dockerfile
        platforms: ${{ env.DOCKER_BUILD_PLATFORMS }}
        push: true
        tags: localhost:5000/${{ env.DOCKER_BASE }}:${{ env.TAG }}
    - name: Build and push to local registry (alpine)
      uses: docker/build-push-action@v5
      with:
        provenance: false # temporarily work around https://github.com/containers/skopeo/issues/1874
        context: .
        file: ./cmd/shfmt/Dockerfile
        platforms: ${{ env.DOCKER_BUILD_PLATFORMS }}
        push: true
        tags: localhost:5000/${{ env.DOCKER_BASE }}:${{ env.TAG }}-alpine
        target: alpine
    - name: Test multi-arch Docker images locally
      run: |
        for platform in $DOCKER_PLATFORMS; do
          for ext in '' '-alpine'; do
            image="localhost:5000/${DOCKER_BASE}:${TAG}${ext}"
            msg="Testing docker image $image on platform $platform"
            line="${msg//?/=}"
            printf "\n${line}\n${msg}\n${line}\n"
            docker pull -q --platform "$platform" "$image"
            if [[ -n "$ext" ]]; then
              echo -n "Image architecture: "
              docker run --rm --entrypoint /bin/sh "$image" -c 'uname -m'
            fi
            version=$(docker run --rm "$image" --version)
            echo "shfmt version: $version"
            if [[ $TAG != "latest" ]] &&
              [[ $TAG != "latest-test" ]] &&
              [[ $TAG != "$version" ]] &&
              ! echo "$version" | grep -q "$TAG"; then
              echo "Version mismatch: shfmt $version tagged as $TAG"
              exit 1
            fi
            docker run --rm -v "$PWD:/mnt" -w '/mnt' "$image" -d cmd/shfmt/docker-entrypoint.sh
          done
        done
    - name: Check GitHub settings
      if: >
        github.event_name == 'push' &&
        github.repository == 'mvdan/sh' &&
        (github.ref == 'refs/heads/master' ||
        github.ref == 'refs/heads/docker-push-test' ||
        startsWith(github.ref, 'refs/tags/'))
      run: |
        missing=()
        [[ -n "${{ secrets.DOCKER_USER }}" ]] || missing+=(DOCKER_USER)
        [[ -n "${{ secrets.DOCKER_TOKEN }}" ]] || missing+=(DOCKER_TOKEN)
        for i in "${missing[@]}"; do
          echo "Missing github secret: $i"
        done
        (( ${#missing[@]} == 0 )) || exit 1
        echo "DOCKER_DEPLOY_IMAGES=true" >> $GITHUB_ENV
    - name: Login to DockerHub
      if: ${{ env.DOCKER_DEPLOY_IMAGES == 'true' }}
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USER }}
        password: ${{ secrets.DOCKER_TOKEN }}
    - name: Push images to DockerHub
      if: ${{ env.DOCKER_DEPLOY_IMAGES == 'true' }}
      run: |
        for ext in '' '-alpine'; do
          image_src="${DOCKER_BASE}:${TAG}${ext}"

          image_dsts=("${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }}:${TAG}${ext}")
          if echo $TAG | grep -q '^v3\.[0-9]\+\.[0-9]\+$'; then
            image_dsts+=("${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }}:v3${ext}")
          elif [[ $TAG == latest-test ]]; then
            image_dsts+=("${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }}:v3-test${ext}")
          fi

          # Show what we're doing.
          msg="Copy multi-arch docker images to DockerHub ($image_src with ${#image_dsts[@]} destinations)"
          line="${msg//?/=}"
          printf "\n${line}\n${msg}\n${line}\n"

          for image_dst in "${image_dsts[@]}"; do
            skopeo copy --all --src-tls-verify=0 docker://localhost:5000/$image_src docker://docker.io/$image_dst
          done
        done
    - name: Update DockerHub description
      if: ${{ env.DOCKER_DEPLOY_IMAGES == 'true' }}
      uses: peter-evans/dockerhub-description@v4
      with:
        username: ${{ secrets.DOCKER_USER }}
        password: ${{ secrets.DOCKER_TOKEN }}
        repository: ${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }}
        readme-filepath: README.md
07070100000005000081A4000000000000000000000001686AE5C000000063000000000000000000000000000000000000001500000000sh-3.12.0/.gitignore*.a
*.zip

# Don't store any of this in the master branch.
suppressions/
crashers/
corpus/
vendor/
07070100000006000081A4000000000000000000000001686AE5C000008D3A000000000000000000000000000000000000001700000000sh-3.12.0/CHANGELOG.md# Changelog

## [3.12.0] - 2025-07-06

- The `mvdan-sh` JS package is discontinued in favor of `sh-syntax` - #1145
**cmd/shfmt**
  - Support the "simplify" and "minify" flags via EditorConfig - #819
  - Do not allow `--write` to replace non-regular files - #843
**interp**
  - Add `IsBuiltin` to check if a command name is a shell built-in - #1164
  - Add `HandlerContext.Builtin` to allow `ExecHandlerFunc` to call built-ins
  - Initial support for `$!` and `wait PID` - #221
  - Return non-fatal `ExecHandlerFunc` errors via the `Runner.Run` API
  - Add `HandlerContext.Pos` to provide handlers with source positions
  - Deprecate `NewExitStatus` and `IsExitStatus` in favor of `ExitStatus`
  - Fix `wait` to always return the status of the last given job
  - Copy all env vars for background subshells to avoid data races
  - Support reading random numbers via `$RANDOM` and `$SRANDOM`
  - Set `$BASH_REMATCH` when matching regular expressions via `=~`
  - Support modifying local vars from the parent calling function
**expand**
  - Adjust which backslash sequences are expanded in here-docs - #1138
  - Tweak tilde expansions to match Bash semantics
**pattern**
  - Remove the flawed and broken `Braces` mode; use `syntax.SplitBraces` instead
  - Tweak `**` to only act as "globstar" when alone as a path element - #1149
  - Tweak `*` and `**` to not match leading dots in basenames
  - Add a `NoGlobStar` mode to match the POSIX semantics
**fileutil**
  - Treat all non-regular files as definitely not shell scripts - #1089

## [3.11.0] - 2025-03-05

This release drops support for Go 1.22 and includes many enhancements.

- **cmd/shfmt**
  - Support `-l=0` and `-f=0` to split filenames with null bytes - #1096
- **syntax**
  - New iterator API: `Parser.WordsSeq`
  - Fix `Parser.Incomplete` and `IsIncomplete` to work well with `Parser.Words` - #937
  - Initial support for parsing incomplete shell via `RecoverErrors`
  - Expand `LangError` to include which language was used when parsing
- **interp**
  - Refactor setting variables to fix array declaration edge cases - #1108
  - Fix `test` read/write/exec operators to work correctly on directories - #1116
  - Replace the `cancelreader` dependency with `os.File.SetReadDeadline`
  - Avoid waiting for process substitutions, matching Bash
  - Skip `OpenHandler` when opening named pipes for process substitutions - #1120
  - Use `TMPDIR` if set via `Env` to create temporary files such as named pipes
- **expand**
  - New iterator API: `FieldsSeq`
  - Correctly handle repeated backslashes in double quotes - #1106
  - Don't expand backslashes inside here-documents - #1070
  - Replace the `Unset` kind with a new `Variable.Set` boolean field

Consider [becoming a sponsor](https://github.com/sponsors/mvdan) if you benefit from the work that went into this release!

## [3.10.0] - 2024-10-20

- **cmd/shfmt**
  - Report the correct language variant in parser error messages - #1102
  - Move `--filename` out of the parser options category - #1079
- **syntax**
  - Parse all CRLF line endings as LF, including inside heredocs - #1088
  - Count skipped backslashes inside backticks in position column numbers - #1098
  - Count skipped null bytes in position column numbers for consistency
- **interp**
  - Fix a regression in `v3.9.0` which broke redirecting files to stdin - #1099
  - Fix a regression in `v3.9.0` where `HandlerContext.Stdin` was never nil
  - Add an `Interactive` option to be used by interactive shells - #1100
  - Support closing stdin, stdout, and stderr via redirections like `<&-`

Consider [becoming a sponsor](https://github.com/sponsors/mvdan) if you benefit from the work that went into this release!

## [3.9.0] - 2024-08-16

This release drops support for Go 1.21 and includes many fixes.

- **cmd/shfmt**
  - Switch the diff implementation to remove one dependency
- **syntax**
  - Protect against overflows in position offset integers
- **interp**
  - Use `os.Pipe` for stdin to prevent draining by subprocesses - #1085
  - Support cancelling reads in builtins when stdin is a file - #1066
  - Support the `nocaseglob` bash option - #1073
  - Support the Bash 5.2 `@k` parameter expansion operator
  - Support the `test -O` and `test -G` operators on non-Windows - #1080
  - Support the `read -s` builtin flag - #1063
- **expand**
  - Add support for case insensitive globbing - #1073
  - Don't panic when pattern words are nil - #1076

A special thanks to @theclapp for their contributors to this release!

Consider [becoming a sponsor](https://github.com/sponsors/mvdan) if you benefit from the work that went into this release!

## [3.8.0] - 2024-02-11

This release drops support for Go 1.19 and 1.20 and includes many
features and bugfixes, such as improving EditorConfig support in `shfmt`.

- **cmd/shfmt**
  - Support EditorConfig language sections such as `[[shell]]` - #664
  - Add `--apply-ignore` for tools and editors - #1037
- **syntax**
  - Allow formatting redirects before all command argumetnts - #942
  - Support brace expansions with uppercase letters - #1042
  - Unescape backquotes in single quotes within backquotes - #1041
  - Better error when using `function` in POSIX mode - #993
  - Better column numbers for escapes inside backquotes - #1028
- **interp**
  - Support parentheses in classic test commands - #1036
  - Determine access to a directory via `unix.Access` - #1033
  - Support subshells with `FuncEnviron` as `Env` - #1043
  - Add support for `fs.DirEntry` via `ReadDirHandler2`
- **expand**
  - Add support for `fs.DirEntry` via `ReadDir2`
  - Support zero-padding in brace expansions - #1042

## [3.7.0] - 2023-06-18

- **syntax**
  - Correctly parse `$foo#bar` as a single word - #1003
  - Make `&>` redirect operators an error in POSIX mode - #991
  - Avoid producing invalid shell when minifying some heredocs - #923
  - Revert the simplification of `${foo:-}` into `${foo-}` - #970
- **interp**
  - Add `ExecHandlers` to support layering multiple middlewares - #964
  - Add initial support for the `select` clause - #969
  - Support combining the `errexit` and `pipefail` options - #870
  - Set `EUID` just like `UID` - #958
  - Replace panics on unimplemented builtins with errors - #999
  - Tweak build tags to support building for `js/wasm` - #983
- **syntax/typedjson**
  - Avoid `reflect.Value.MethodByName` to reduce binary sizes - #961

## [3.6.0] - 2022-12-11

This release drops support for Go 1.17 and includes many features and fixes.

- **cmd/shfmt**
  - Implement `--from-json` as the reverse of `--to-json` - [#900]
  - Improve the quality of the `--to-json` output - [#900]
  - Provide detected language when erroring with `-ln=auto` - [#803]
- **syntax**
  - Don't require peeking two bytes after `echo *` - [#835]
  - Simplify `${name:-}` to the equivalent `${name-}` - [#849]
  - Don't print trailing whitespaces on nested subshells - [#814]
  - Don't print extra newlines in some case clauses - [#779]
  - Don't indent comments preceding case clause items - [#917]
  - Allow escaped newlines before unquoted words again - [#873]
  - Parse a redirections edge case without spaces - [#879]
  - Give a helpful error when `<<<` is used in POSIX mode - [#881]
  - Forbid `${!foo*}` and `${!foo@}` in mksh mode - [#929]
  - Batch allocations less aggressively in the parser
- **syntax/typedjson**
  - Expose `--from-json` and `--to-json` as Go APIs - [#885]
- **expand**
  - Improve support for expanding array keys and values - [#884]
  - Don't panic on unsupported syntax nodes - [#841]
  - Don't panic on division by zero - [#892]
  - Properly expand unquoted parameters with spaces - [#886]
  - Trim spaces when converting strings to integers - [#928]
- **interp**
  - Add initial implementation for `mapfile` and `readarray` - [#863]
  - Improve matching patterns against multiple lines - [#866]
  - Support `%b` in the `printf` builtin - [#955]
  - Display all Bash options in `shopt` - [#877]
- **pattern**
  - Add `EntireString` to match the entire string using `^$` - [#866]

## [3.5.1] - 2022-05-23

- **cmd/shfmt**
  - Fix the Docker publishing script bug which broke 3.5.0 - [#860]
- **interp**
  - Support multi-line strings when pattern matching in `[[` - [#861]
  - Invalid glob words are no longer removed with `nullglob` - [#862]
- **pattern**
  - `Regexp` now returns the typed error `SyntaxError` - [#862]

## [3.5.0] - 2022-05-11

This release drops support for Go 1.16 and includes many new features.

- **cmd/shfmt**
  - Switch to `-ln=auto` by default to detect the shell language
  - Add support for long flags, like `--indent` for `-i`
- **syntax**
  - Allow extglob wildcards as function names like `@() { ... }`
  - Add support for heredocs surrounded by backquotes
  - Add support for backquoted inline comments
  - Add `NewPos` to create `Pos` values externally
  - Support escaped newlines with CRLF line endings
  - `Minify` no longer omits a leading shebang comment
  - Avoid printing escaped newlines in non-quoted words
  - Fix some printer edge cases where comments weren't properly spaced
- **fileutil**
  - Add `Shebang` to extract the shell language from a `#!` line
- **expand**
  - Reimplement globstar `**` globbing for correctness
  - Replace `os.Stat` as the last direct use of the filesystem
- **interp**
  - Add `CallHandler` to intercept all interpreted `CallExpr` nodes
  - Add `ReadDirHandler` to intercept glob expansion filesystem reads
  - Add `StatHandler` to intercept `os.Stat` and `os.Lstat` calls
  - Always surface exit codes from command substitutions
  - Add initial and incomplete support for `set -x`
  - Add support for `cd -` as `cd "$OLDPWD"`
  - Avoid panic on `set - args`

## [3.4.3] - 2022-02-19

- **cmd/shfmt**
  - New Docker `v3` tag to track the latest stable version
  - Don't duplicate errors when walking directories
- **interp**
  - Properly handle empty paths in the `test` builtin
  - Allow unsetting global vars from inside a function again
  - Use `%w` to wrap errors in `Dir`

## [3.4.2] - 2021-12-24

- The tests no longer assume what locales are installed
- **interp**
  - Keep `PATH` list separators OS-specific to fix a recent regression
  - Avoid negative elapsed durations in the `time` builtin

## [3.4.1] - 2021-11-23

- **syntax**
  - Don't return an empty string on empty input to `Quote`
- **expand**
  - Properly sort in `ListEnviron` to avoid common prefix issues
- **interp**
  - `export` used in functions now affects the global scope
  - Support looking for scripts in `$PATH` in `source`
  - Properly slice arrays in parameter expansions

## [3.4.0] - 2021-10-01

This release drops support for Go 1.15,
which allows the code to start benefitting from `io/fs`.

- **cmd/shfmt**
  - Walks directories ~10% faster thanks to `filepath.WalkDir`
- **syntax**
  - Add `Quote` to mirror `strconv.Quote` for shell syntax
  - Skip null characters when parsing, just like Bash
  - Rewrite fuzzers with Go 1.18's native fuzzing
- **fileutil**
  - Add `CouldBeScript2` using `io/fs.DirEntry`
- **expand**
  - Skip or stop at null characters, just like Bash
- **interp**
  - Set `GID` just like `UID`
  - Add support for `read -p`
  - Add support for `pwd` flags
  - Create random FIFOs for process substitutions more robustly
  - Avoid leaking an open file when interpreting `$(<file)`

## [3.3.1] - 2021-08-01

- **syntax**
  - Don't convert `&` in a separate line into `;`
  - Fix a `BinaryNextLine` edge case idempotency bug
  - Never start printing a command with an escaped newline
- **interp**
  - Support calling `Runner.Reset` before `Runner.Run`
  - Obey `set -e` for failed redirections

## [3.3.0] - 2021-05-17

- **cmd/shfmt**
  - Document the `FORCE_COLOR` env var to always use colors in diffs
- **syntax**
  - Add the printer `SingleLine` option to avoid printing newlines
  - Positions now use more bits for line numbers than column numbers
  - Test operators like `&&` and `||` no longer escape newlines
  - Properly handle closing backquotes in a few edge cases
  - Properly handle trailing escaped newlines in heredocs
- **interp**
  - Redesigned variable scoping to fix a number of edge cases
  - Refactor `set -o nounset` support to fix many edge cases
  - Deprecate `LookPath` in favor of `LookPathDir`
  - Array element words are now expanded correctly
  - Add support for `trap` with error and exit signals
  - Add support for `shopt -s nullglob`
  - Add support for `type -p`

## [3.2.4] - 2021-03-08

- **cmd/shfmt**
  - Don't stop handling arguments when one results in a failure
- **expand**
  - Don't panic when a backslash is followed by EOF

## [3.2.2] - 2021-01-29

- **syntax**
  - Avoid comment position panic in the printer

## [3.2.1] - 2020-12-02

- **syntax**
  - Fix an endless loop when parsing single quotes in parameter expansions
  - Properly print assignments using escaped newlines
  - Print inline heredoc comments in the right place
- **interp**
  - Always expand `~` in Bash test expressions
- **expand**
  - Don't panic on out of bounds array index expansions

## [3.2.0] - 2020-10-29

- **cmd/shfmt**
  - Add a man page via [scdoc](https://sr.ht/~sircmpwn/scdoc/); see [shfmt.1.scd](cmd/shfmt/shfmt.1.scd)
  - Add `-filename` to give a name to standard input
- **syntax**
  - Add initial support for [Bats](https://github.com/bats-core/bats-core)
  - Protect line and column position numbers against overflows
  - Rewrite arithmetic parsing to fix operator precedence
  - Don't add parentheses to `function f {...}` declarations for ksh support
  - `KeepPadding` now obeys extra indentation when using space indentation
  - Properly tokenize `((` within test expressions
  - Properly tokenize single quotes within parameter expansions
  - Obey print options inside `<<-` heredocs
  - Don't simplify indexed parameter expansions in arithmetic expressions
  - Improve parsing errors for missing test expressions
  - `LangVariant` now implements [flag.Value](https://pkg.go.dev/flag#Value)
- **interp**
  - Avoid panic on C-style loops which omit expressions
  - `$@` and `$*` always exist, so `"$@"` can expand to zero words

## [3.1.2] - 2020-06-26

- **syntax**
  - Fix brace indentation when using `FunctionNextLine`
  - Support indirect parameter expansions with transformations
  - Stop heredoc bodies only when the entire line matches
- **interp**
  - Make the tests pass on 32-bit platforms

## [3.1.1] - 2020-05-04

- **cmd/shfmt**
  - Recognise `function_next_line` in EditorConfig files
- **syntax**
  - Don't ignore escaped newlines at the end of heredoc bodies
  - Improve support for parsing regexes in test expressions
  - Count columns for `KeepPadding` in bytes, to better support unicode
  - Never let `KeepPadding` add spaces right after indentation
- **interp**
  - Hide unset variables when executing programs

## [3.1.0] - 2020-04-07

- Redesigned Docker images, including buildx and an Alpine variant
- **cmd/shfmt**
  - Replace source files atomically when possible
  - Support `ignore = true` in an EditorConfig to skip directories
  - Add `-fn` to place function opening braces on the next line
  - Improve behavior of `-f` when given non-directories
  - Docker images and `go get` installs now embed good version information
- **syntax**
  - Add support for nested here-documents
  - Allow parsing for loops with braces, present in mksh and Bash
  - Expand `CaseClause` to describe its `in` token
  - Allow empty lines in Bash arrays in the printer
  - Support disabling `KeepPadding`
  - Avoid mis-printing some programs involving `&`
- **interp**
  - Add initial support for Bash process substitutions
  - Add initial support for aliases
  - Fix an edge case where the status code would not be reset
  - The exit status code can now reflect being stopped by a signal
  - `test -t` now uses the interpreter's stdin/stdout/stderr files
- **expand**
  - Improve the interaction of `@` and `*` with quotes and `IFS`

## [3.0.2] - 2020-02-22

- **syntax**
  - Don't indent after escaped newlines in heredocs
  - Don't parse `*[i]=x` as a valid assignment
- **interp**
  - Prevent subshells from defining funcs in the parent shells
- **expand**
  - Parameters to `Fields` no longer get braces expanded in-place

## [3.0.1] - 2020-01-11

- **cmd/shfmt**
  - Fix an edge case where walking directories could panic
- **syntax**
  - Only do a trailing read in `Parser.Stmts` if we have open heredocs
  - Ensure comments are never folded into heredocs
  - Properly tokenize `)` after a `=~` test regexp
  - Stop parsing a comment at an escaped newline
- **expand**
  - `"$@"` now expands to zero fields when there are zero parameters

## [3.0.0] - 2019-12-16

This is the first stable release as a proper module, now under
`mvdan.cc/sh/v3/...`. Go 1.12 or later is supported.

A large number of changes have been done since the last feature release a year
ago. All users are encouraged to update. Below are the major highlights.

- **cmd/shfmt**
  - Support for [EditorConfig](https://editorconfig.org/) files
  - Drop the dependency on `diff` for the `-d` flag, now using pure Go
- **syntax**
  - Overhaul escaped newlines, now represented as `WordPart` positions
  - Improve some operator type names, to consistently convey meaning
  - Completely remove `StmtList`
  - Redesign `IfClause`, making its "else" another `IfClause` node
  - Redesign `DeclClause` to remove its broken `Opts` field
  - Brace expression parsing is now done with a `BraceExp` word part
  - Improve comment alignment in `Printer` via a post-process step
  - Add support for the `~` bitwise negation operator
  - Record the use of deprecated tokens in the syntax tree
- **interp**
  - Improve the module API as "handlers", to reduce confusion with Go modules
  - Split `LookPath` out of `ExecHandler` to allow custom behavior
  - `Run` now returns `nil` instead of `ShellExitStatus(0)`
  - `OpenDevImpls` is removed; see `ExampleOpenHandler` for an alternative
- **expand**
  - Redesign `Variable` to reduce allocations
  - Add support for more escape sequences
  - Make `Config` a bit more powerful via `func` fields
  - Rework brace expansion via the new `BraceExp` word part
- **pattern**
  - New package for shell pattern matching, extracted from `syntax`
  - Add support for multiple modes, including filenames and braces

Special thanks to Konstantin Kulikov for his contribution to this release.

## [2.6.4] - 2019-03-10

- **syntax**
  - Support array elements without values, like `declare -A x=([index]=)`
  - Parse `for i; do ...` uniquely, as it's short for `for i in "$@"`
  - Add missing error on unclosed nested backquotes
- **expand**
  - Don't expand tildes twice, fixing `echo ~` on Windows
- **interp**
  - Fix the use of `Params` as an option to `New`
  - Support lowercase Windows volume names in `$PATH`

## [2.6.3] - 2019-01-19

- **expand**
  - Support globs with path prefixes and suffixes, like `./foo/*/`
  - Don't error when skipping non-directories in glob walks

## [2.6.2] - 2018-12-08

- **syntax**
  - Avoid premature reads in `Parser.Interactive` when parsing Unicode bytes
  - Fix parsing of certain Bash test expression involving newlines
  - `Redirect.End` now takes the `Hdoc` field into account
  - `ValidName` now returns `false` for an empty string
- **expand**
  - Environment variables on Windows are case insensitive again
- **interp**
  - Don't crash on `declare $unset=foo`
  - Fix a regression where executed programs would receive a broken environment

Note that the published Docker image was changed to set `shfmt` as the
entrypoint, so previous uses with arguments like `docker run mvdan/shfmt:v2.6.1
shfmt --version` should now be `docker run mvdan/shfmt:v2.6.2 --version`.

## [2.6.1] - 2018-11-17

- **syntax**
  - Fix `Parser.Incomplete` with some incomplete literals
  - Fix parsing of Bash regex tests in some edge cases
- **interp**
  - Add support for `$(<file)` special command substitutions

## [2.6.0] - 2018-11-10

This is the biggest v2 release to date. It's now possible to write an
interactive shell, and it's easier and safer to perform shell expansions.

This will be the last major v2 version, to allow converting the project to a Go
module in v3.

- Go 1.10 or later required to build
- **syntax**
  - Add `Parser.Interactive` to implement an interactive shell
  - Add `Parser.Document` to parse a single here-document body
  - Add `Parser.Words` to incrementally parse separate words
  - Add the `Word.Lit` helper method
  - Support custom indentation in `<<-` heredoc bodies
- **interp**
  - Stabilize API and add some examples
  - Introduce a constructor, and redesign `Runner.Reset`
  - Move the context from a field to function parameters
  - Remove `Runner.Stmt` in favor of `Run` with `ShellExitStatus`
- **shell**
  - Stabilize API and add some examples
  - Add `Expand`, as a more powerful `os.Expand`
  - Add `Fields`, similar to the old `Runner.Fields`
  - `Source*` functions now take a context
  - `Source*` functions no longer try to sandbox
- **expand**
  - New package, split from `interp`
  - Allows performing shell expansions in a controlled way
  - Redesigned `Environ` and `Variable` moved from `interp`

## [2.5.1] - 2018-08-03

- **syntax**
  - Fix a regression where semicolons would disappear within switch cases

## [2.5.0] - 2018-07-13

- **syntax**
  - Add support for Bash's `{varname}<` redirects
  - Add `SpaceRedirects` to format redirects like `> word`
  - Parse `$\"` correctly within double quotes
  - A few fixes where minification would break programs
  - Printing of heredocs within `<()` no longer breaks them
  - Printing of single statements no longer adds empty lines
  - Error on invalid parameter names like `${1a}`
- **interp**
  - `Runner.Dir` is now always an absolute path
- **shell**
  - `Expand` now supports expanding a lone `~`
  - `Expand` and `SourceNode` now have default timeouts
- **cmd/shfmt**
  - Add `-sr` to print spaces after redirect operators
  - Don't skip empty string values in `-tojson`
  - Include comment positions in `-tojson`

## [2.4.0] - 2018-05-16

- Publish as a JS package, [mvdan-sh](https://www.npmjs.com/package/mvdan-sh)
- **syntax**
  - Add `DebugPrint` to pretty-print a syntax tree
  - Fix comment parsing and printing in some edge cases
  - Indent `<<-` heredoc bodies if indenting with tabs
  - Add support for nested backquotes
  - Relax parser to allow quotes in arithmetic expressions
  - Don't rewrite `declare foo=` into `declare foo`
- **interp**
  - Add support for `shopt -s globstar`
  - Replace `Runner.Env` with an interface
- **shell**
  - Add `Expand` as a fully featured version of `os.Expand`
- **cmd/shfmt**
  - Set appropriate exit status when `-d` is used

## [2.3.0] - 2018-03-07

- **syntax**
  - Case clause patterns are no longer forced on a single line
  - Add `ExpandBraces`, to perform Bash brace expansion on words
  - Improve the handling of backslashes within backquotes
  - Improve the parsing of Bash test regexes
- **interp**
  - Support `$DIRSTACK`, `${param[@]#word}`, and `${param,word}`
- **cmd/shfmt**
  - Add `-d`, to display diffs when formatting differs
  - Promote `-exp.tojson` to `-tojson`
  - Add `Pos` and `End` fields to nodes in `-tojson`
  - Inline `StmtList` fields to simplify the `-tojson` output
  - Support `-l` on standard input

## [2.2.1] - 2018-01-25

- **syntax**
  - Don't error on `${1:-default}`
  - Allow single quotes in `${x['str key']}` as well as double quotes
  - Add support for `${!foo[@]}`
  - Don't simplify `foo[$x]` to `foo[x]`, to not break string indexes
  - Fix `Stmt.End` when the end token is the background operator `&`
  - Never apply the negation operator `!` to `&&` and `||` lists
  - Apply the background operator `&` to entire `&&` and `||` lists
  - Fix `StopAt` when the stop string is at the beginning of the source
  - In `N>word`, check that `N` is a valid numeric literal
  - Fix a couple of crashers found via fuzzing
- **cmd/shfmt**
  - Don't error if non-bash files can't be written to

## [2.2.0] - 2018-01-18

- Tests on Mac and Windows are now ran as part of CI
- **syntax**
  - Add `StopAt` to stop lexing at a custom arbitrary token
  - Add `TranslatePattern` and `QuotePattern` for pattern matching
  - Minification support added to the printer - see `Minify`
  - Add ParamExp.Names to represent `${!prefix*}`
  - Add TimeClause.PosixFormat for its `-p` flag
  - Fix parsing of assignment values containing `=`
  - Fix parsing of parameter expansions followed by a backslash
  - Fix quotes in parameter expansion operators like `${v:-'def'}`
  - Fix parsing of negated declare attributes like `declare +x name`
  - Fix parsing of `${#@}`
  - Reject bad parameter expansion operators like `${v@WRONG}`
  - Reject inline array variables like `a=(b c) prog`
  - Reject indexing of special vars like `${1[3]}`
  - Reject `${!name}` when in POSIX mode
  - Reject multiple parameter expansion actions like `${#v:-def}`
- **interp**
  - Add Bash brace expansion support, including `{a,b}` and `{x..y}`
  - Pattern matching actions are more correct and precise
  - Exported some Runner internals, including `Vars` and `Funcs`
  - Use the interpreter's `$PATH` to find binaries
  - Roll our own globbing to use our own pattern matching code
  - Support the `getopts` sh builtin
  - Support the `read` bash builtin
  - Numerous changes to improve Windows support
- **shell**
  - New experimental package with high-level utility functions
  - Add `SourceFile` to get the variables declared in a script
  - Add `SourceNode` as a lower-level version of the above
- **cmd/shfmt**
  - Add `-mn`, which minifies programs via `syntax.Minify`

## [2.1.0] - 2017-11-25

- **syntax**
  - Add `Stmts`, to parse one statement at a time
  - Walk no longer ignores comments
  - Parameter expansion end fixes, such as `$foo.bar`
  - Whitespace alignment can now be kept - see `KeepPadding`
  - Introduce an internal newline token to simplify the parser
  - Fix `Block.Pos` to actually return the start position
  - Fix mishandling of inline comments in two edge cases
- **interp**
  - Expose `Fields` to expand words into strings
  - First configurable modules - cmds and files
  - Add support for the new `TimeClause`
  - Add support for namerefs and readonly vars
  - Add support for associative arrays (maps)
  - More sh builtins: `exec return`
  - More bash builtins: `command pushd popd dirs`
  - More `test` operators: `-b -c -t -o`
  - Configurable kill handling - see `KillTimeout`
- **cmd/shfmt**
  - Add `-f` to just list all the shell files found
  - Add `-kp` to keep the column offsets in place
- **cmd/gosh**
  - Now supports a basic interactive mode

## [2.0.0] - 2017-08-30

- The package import paths were moved to `mvdan.cc/sh/...`
- **syntax**
  - Parser and Printer structs introduced with functional options
  - Node positions are now independent - `Position` merged into `Pos`
  - All comments are now attached to nodes
  - Support `mksh` - MirBSD's Korn Shell, used in Android
  - Various changes to the AST:
    - `EvalClause` removed; `eval` is no longer parsed as a keyword
    - Add support for Bash's `time` and `select`
    - Merge `UntilClause` into `WhileClause`
    - Moved `Stmt.Assigns` to `CallExpr.Assigns`
    - Remove `Elif` - chain `IfClause` nodes instead
  - Support for indexed assignments like `a[i]=b`
  - Allow expansions in arithmetic expressions again
  - Unclosed heredocs now produce an error
  - Binary ops are kept in the same line - see `BinaryNextLine`
  - Switch cases are not indented by default - see `SwitchCaseIndent`
- **cmd/shfmt**
  - Add `-s`, which simplifies programs via `syntax.Simplify`
  - Add `-ln <lang>`, like `-ln mksh`
  - Add `-bn` to put binary ops in the next line, like in v1
  - Add `-ci` to indent switch cases, like in v1
- **interp**
  - Some progress made, though still experimental
  - Most of POSIX done - some builtins remain to be done

## [1.3.1] - 2017-05-26

- **syntax**
  - Fix parsing of `${foo[$bar]}`
  - Fix printer regression where `> >(foo)` would be turned into `>>(foo)`
  - Break comment alignment on any line without a comment, fixing formatting issues
  - Error on keywords like `fi` and `done` used as commands

## [1.3.0] - 2017-04-24

- **syntax**
  - Fix backslashes in backquote command substitutions
  - Disallow some test expressions like `[[ a == ! b ]]`
  - Disallow some parameter expansions like `${$foo}`
  - Disallow some arithmetic expressions like `((1=3))` and `(($(echo 1 + 2)))`
  - Binary commands like `&&`, `||` and pipes are now left-associative
- **fileutil**
  - `CouldBeScript` may now return true on non-regular files such as symlinks
- **interp**
  - New experimental package to interpret a `syntax.File` in pure Go

## [1.2.0] - 2017-02-22

- **syntax**
  - Add support for escaped characters in bash regular expressions
- **fileutil**
  - New package with some code moved from `cmd/shfmt`, now importable
  - New funcs `HasShebang` and `CouldBeScript`
  - Require shebangs to end with whitespace to reject `#!/bin/shfoo`

## [1.1.0] - 2017-01-05

- **syntax**
  - Parse `[[ a = b ]]` like `[[ a == b ]]`, deprecating `TsAssgn` in favour of `TsEqual`
  - Add support for the `-k`, `-G`, `-O` and `-N` unary operators inside `[[ ]]`
  - Add proper support for `!` in parameter expansions, like `${!foo}`
  - Fix a couple of crashes found via fuzzing
- **cmd/shfmt**
  - Rewrite `[[ a = b ]]` into the saner `[[ a == b ]]` (see above)

## [1.0.0] - 2016-12-13

- **syntax**
  - Stable release, API now frozen
  - `Parse` now reads input in chunks of 1KiB
- **cmd/shfmt**
  - Add `-version` flag

## [0.6.0] - 2016-12-05

- **syntax**
  - `Parse` now takes an `io.Reader` instead of `[]byte`
  - Invalid UTF-8 is now reported as an error
  - Remove backtracking for `$((` and `((`
  - `Walk` now takes a func literal to simplify its use

## [0.5.0] - 2016-11-24

- **cmd/shfmt**
  - Remove `-cpuprofile`
  - Don't read entire files into memory to check for a shebang
- **syntax**
  - Use `uint32` for tokens and positions in nodes
  - Use `Word` and `Lit` pointers consistently instead of values
  - Ensure `Word.Parts` is never empty
  - Add support for expressions in array indexing and parameter expansion slicing

## [0.4.0] - 2016-11-08

- Merge `parser`, `ast`, `token` and `printer` into a single package `syntax`
- Use separate operator types in nodes rather than `Token`
- Use operator value names that express their function
- Keep `;` if on a separate line when formatting
- **cmd/shfmt**
  - Allow whitespace after `#!` in a shebang
- **syntax**
  - Implement operator precedence for `[[ ]]`
  - Parse `$(foo)` and ``foo`` as the same (`shfmt` then converts the latter to the former)
  - Rename `Quoted` to `DblQuoted` for clarity
  - Split `((foo))` nodes as their own type, `ArithmCmd`
  - Add support for bash parameter expansion slicing

## [0.3.0] - 2016-10-26

- Add support for bash's `coproc` and extended globbing like `@(foo)`
- Improve test coverage, adding tests to `cmd/shfmt` and bringing `parser` and `printer` close to 100%
- Support empty C-style for loops like `for ((;;)) ...`
- Support for the `>|` redirect operand
- **cmd/shfmt**
  - Fix issue where `.sh` and `.bash` files might not be walked if running on a directory
  - Fix issue where `-p` was not obeyed when formatting stdin
- **parser**
  - `$''` now generates an `ast.SglQuoted`, not an `ast.Quoted`
  - Support for ambiguous `((` like with `$((`
  - Improve special parameter expansions like `$@` or `$!`
  - Improve bash's `export` `typeset`, `nameref` and `readonly`
  - `<>`, `>&` and `<&` are valid POSIX
  - Support for bash's `^`, `^^`, `,` and `,,` operands inside `${}`

## [0.2.0] - 2016-10-13

- Optimizations all around, making `shfmt` ~15% faster
- **cmd/shfmt**
  - Add `-p` flag to only accept POSIX Shell programs (`parser.PosixConformant`)
- **parser**
  - Add support for ambiguous `$((` as in `$((foo) | bar)`
  - Limit more bash features to `PosixConformant` being false
  - Don't parse heredoc bodies in nested expansions and contexts
  - Run tests through `bash` to confirm the presence of a parse error
- **ast**
  - Add `Walk(Visitor, Node)` function

## [0.1.0] - 2016-09-20

Initial release.

[3.12.0]: https://github.com/mvdan/sh/releases/tag/v3.12.0
[3.11.0]: https://github.com/mvdan/sh/releases/tag/v3.11.0
[3.10.0]: https://github.com/mvdan/sh/releases/tag/v3.10.0
[3.9.0]: https://github.com/mvdan/sh/releases/tag/v3.9.0
[3.8.0]: https://github.com/mvdan/sh/releases/tag/v3.8.0
[3.7.0]: https://github.com/mvdan/sh/releases/tag/v3.7.0

[3.6.0]: https://github.com/mvdan/sh/releases/tag/v3.6.0
[#779]: https://github.com/mvdan/sh/issues/779
[#803]: https://github.com/mvdan/sh/issues/803
[#814]: https://github.com/mvdan/sh/issues/814
[#835]: https://github.com/mvdan/sh/issues/835
[#841]: https://github.com/mvdan/sh/issues/841
[#849]: https://github.com/mvdan/sh/pull/849
[#863]: https://github.com/mvdan/sh/pull/863
[#866]: https://github.com/mvdan/sh/pull/866
[#873]: https://github.com/mvdan/sh/issues/873
[#877]: https://github.com/mvdan/sh/issues/877
[#879]: https://github.com/mvdan/sh/pull/879
[#881]: https://github.com/mvdan/sh/issues/881
[#884]: https://github.com/mvdan/sh/issues/884
[#885]: https://github.com/mvdan/sh/issues/885
[#886]: https://github.com/mvdan/sh/issues/886
[#892]: https://github.com/mvdan/sh/issues/892
[#900]: https://github.com/mvdan/sh/pull/900
[#917]: https://github.com/mvdan/sh/pull/917
[#928]: https://github.com/mvdan/sh/issues/928
[#929]: https://github.com/mvdan/sh/pull/929
[#955]: https://github.com/mvdan/sh/pull/955

[3.5.1]: https://github.com/mvdan/sh/releases/tag/v3.5.1
[#860]: https://github.com/mvdan/sh/issues/860
[#861]: https://github.com/mvdan/sh/pull/861
[#862]: https://github.com/mvdan/sh/pull/862

[3.5.0]: https://github.com/mvdan/sh/releases/tag/v3.5.0
[3.4.3]: https://github.com/mvdan/sh/releases/tag/v3.4.3
[3.4.2]: https://github.com/mvdan/sh/releases/tag/v3.4.2
[3.4.1]: https://github.com/mvdan/sh/releases/tag/v3.4.1
[3.4.0]: https://github.com/mvdan/sh/releases/tag/v3.4.0
[3.3.1]: https://github.com/mvdan/sh/releases/tag/v3.3.1
[3.3.0]: https://github.com/mvdan/sh/releases/tag/v3.3.0
[3.2.4]: https://github.com/mvdan/sh/releases/tag/v3.2.4
[3.2.2]: https://github.com/mvdan/sh/releases/tag/v3.2.2
[3.2.1]: https://github.com/mvdan/sh/releases/tag/v3.2.1
[3.2.0]: https://github.com/mvdan/sh/releases/tag/v3.2.0
[3.1.2]: https://github.com/mvdan/sh/releases/tag/v3.1.2
[3.1.1]: https://github.com/mvdan/sh/releases/tag/v3.1.1
[3.1.0]: https://github.com/mvdan/sh/releases/tag/v3.1.0
[3.0.2]: https://github.com/mvdan/sh/releases/tag/v3.0.2
[3.0.1]: https://github.com/mvdan/sh/releases/tag/v3.0.1
[3.0.0]: https://github.com/mvdan/sh/releases/tag/v3.0.0
[2.6.4]: https://github.com/mvdan/sh/releases/tag/v2.6.4
[2.6.3]: https://github.com/mvdan/sh/releases/tag/v2.6.3
[2.6.2]: https://github.com/mvdan/sh/releases/tag/v2.6.2
[2.6.1]: https://github.com/mvdan/sh/releases/tag/v2.6.1
[2.6.0]: https://github.com/mvdan/sh/releases/tag/v2.6.0
[2.5.1]: https://github.com/mvdan/sh/releases/tag/v2.5.1
[2.5.0]: https://github.com/mvdan/sh/releases/tag/v2.5.0
[2.4.0]: https://github.com/mvdan/sh/releases/tag/v2.4.0
[2.3.0]: https://github.com/mvdan/sh/releases/tag/v2.3.0
[2.2.1]: https://github.com/mvdan/sh/releases/tag/v2.2.1
[2.2.0]: https://github.com/mvdan/sh/releases/tag/v2.2.0
[2.1.0]: https://github.com/mvdan/sh/releases/tag/v2.1.0
[2.0.0]: https://github.com/mvdan/sh/releases/tag/v2.0.0
[1.3.1]: https://github.com/mvdan/sh/releases/tag/v1.3.1
[1.3.0]: https://github.com/mvdan/sh/releases/tag/v1.3.0
[1.2.0]: https://github.com/mvdan/sh/releases/tag/v1.2.0
[1.1.0]: https://github.com/mvdan/sh/releases/tag/v1.1.0
[1.0.0]: https://github.com/mvdan/sh/releases/tag/v1.0.0
[0.6.0]: https://github.com/mvdan/sh/releases/tag/v0.6.0
[0.5.0]: https://github.com/mvdan/sh/releases/tag/v0.5.0
[0.4.0]: https://github.com/mvdan/sh/releases/tag/v0.4.0
[0.3.0]: https://github.com/mvdan/sh/releases/tag/v0.3.0
[0.2.0]: https://github.com/mvdan/sh/releases/tag/v0.2.0
[0.1.0]: https://github.com/mvdan/sh/releases/tag/v0.1.0
07070100000007000081A4000000000000000000000001686AE5C0000005D0000000000000000000000000000000000000001200000000sh-3.12.0/LICENSECopyright (c) 2016, Daniel Martí. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
07070100000008000081A4000000000000000000000001686AE5C000001BAB000000000000000000000000000000000000001400000000sh-3.12.0/README.md# sh

[![Go Reference](https://pkg.go.dev/badge/mvdan.cc/sh/v3.svg)](https://pkg.go.dev/mvdan.cc/sh/v3)

A shell parser, formatter, and interpreter. Supports [POSIX Shell], [Bash], and
[mksh]. Requires Go 1.23 or later.

### Quick start

To parse shell scripts, inspect them, and print them out, see the [syntax
package examples](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#pkg-examples).

For high-level operations like performing shell expansions on strings, see the
[shell package examples](https://pkg.go.dev/mvdan.cc/sh/v3/shell#pkg-examples).

### shfmt

	go install mvdan.cc/sh/v3/cmd/shfmt@latest

`shfmt` formats shell programs. See [canonical.sh](syntax/canonical.sh) for a
quick look at its default style. For example:

	shfmt -l -w script.sh

For more information, see [its manpage](cmd/shfmt/shfmt.1.scd), which can be
viewed directly as Markdown or rendered with [scdoc].

Packages are available on [Alpine], [Arch], [Debian], [Docker], [Fedora], [FreeBSD],
[Homebrew], [MacPorts], [NixOS], [OpenSUSE], [Scoop], [Snapcraft], [Void] and [webi].

### gosh

	go install mvdan.cc/sh/v3/cmd/gosh@latest

Proof of concept shell that uses `interp`. Note that it's not meant to replace a
POSIX shell at the moment, and its options are intentionally minimalistic.

### Fuzzing

We use Go's native fuzzing support. For instance:

	cd syntax
	go test -run=- -fuzz=ParsePrint

### Caveats

* When indexing Bash associative arrays, always use quotes. The static parser
  will otherwise have to assume that the index is an arithmetic expression.

```sh
$ echo '${array[spaced string]}' | shfmt
<standard input>:1:16: not a valid arithmetic operator: string
$ echo '${array[weird!key]}' | shfmt
<standard input>:1:8: reached ! without matching [ with ]
$ echo '${array[dash-string]}' | shfmt
${array[dash - string]}
```

* `$((` and `((` ambiguity is not supported. Backtracking would complicate the
  parser and make streaming support via `io.Reader` impossible. The POSIX spec
  recommends to [space the operands][posix-ambiguity] if `$( (` is meant.

```sh
$ echo '$((foo); (bar))' | shfmt
1:1: reached ) without matching $(( with ))
```

* `export`, `let`, and `declare` are parsed as keywords.
  This allows statically building their syntax tree,
  as opposed to keeping the arguments as a slice of words.
  It is also required to support `declare foo=(bar)`.
  Note that this means expansions like `declare {a,b}=c` are not supported.

* The entire library is written in pure Go, which limits how closely the
  interpreter can follow POSIX Shell and Bash semantics.
  For example, Go does not support forking its own process, so subshells
  use a goroutine instead, meaning that real PIDs and file descriptors
  cannot be used directly.

### JavaScript

The parser and formatter are available as a third party npm package called [sh-syntax],
which bundles a version of this library compiled to WASM.

Previously, we maintained an npm package called [mvdan-sh] which used GopherJS
to bundle a JS version of this library. That npm package is now archived
given its poor performance and GopherJS not being as actively developed.
Any existing or new users should look at [sh-syntax] instead.

### Docker

All release tags are published via [Docker], such as `v3.5.1`.
The latest stable release is currently published as `v3`,
and the latest development version as `latest`.
The images only include `shfmt`; `-alpine` variants exist on Alpine Linux.

To build a Docker image, run:

	docker build -t my:tag -f cmd/shfmt/Dockerfile .

To use a Docker image, run:

	docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/mnt" -w /mnt my:tag <shfmt arguments>

### Related projects

The following editor integrations wrap `shfmt`:

- [BashSupport-Pro] - Bash plugin for JetBrains IDEs
- [dockerfmt] - Dockerfile formatter using shfmt
- [intellij-shellcript] - Intellij Jetbrains `shell script` plugin
- [micro] - Editor with a built-in plugin
- [neoformat] - (Neo)Vim plugin
- [shell-format] - VS Code plugin
- [vscode-shfmt] - VS Code plugin
- [shfmt.el] - Emacs package
- [Sublime-Pretty-Shell] - Sublime Text 3 plugin
- [Trunk] - Universal linter, available as a CLI, VS Code plugin, and GitHub action
- [vim-shfmt] - Vim plugin

Other noteworthy integrations include:

- [modd] - A developer tool that responds to filesystem changes
- [prettier-plugin-sh] - [Prettier] plugin using [sh-syntax]
- [sh-checker] - A GitHub Action that performs static analysis for shell scripts
- [mdformat-shfmt] - [mdformat] plugin to format shell scripts embedded in Markdown with shfmt
- [pre-commit-shfmt] - [pre-commit] shfmt hook

[alpine]: https://pkgs.alpinelinux.org/packages?name=shfmt
[arch]: https://archlinux.org/packages/extra/x86_64/shfmt/
[bash]: https://www.gnu.org/software/bash/
[BashSupport-Pro]: https://www.bashsupport.com/manual/editor/formatter/
[debian]: https://tracker.debian.org/pkg/golang-mvdan-sh
[docker]: https://hub.docker.com/r/mvdan/shfmt/
[dockerfmt]: https://github.com/reteps/dockerfmt
[editorconfig]: https://editorconfig.org/
[examples]: https://pkg.go.dev/mvdan.cc/sh/v3/syntax#pkg-examples
[fedora]: https://packages.fedoraproject.org/pkgs/golang-mvdan-sh-3/shfmt/
[freebsd]: https://www.freshports.org/devel/shfmt
[homebrew]: https://formulae.brew.sh/formula/shfmt
[intellij-shellcript]: https://www.jetbrains.com/help/idea/shell-scripts.html
[macports]: https://ports.macports.org/port/shfmt/details/
[mdformat-shfmt]: https://github.com/hukkin/mdformat-shfmt
[mdformat]: https://github.com/executablebooks/mdformat
[micro]: https://micro-editor.github.io/
[mksh]: http://www.mirbsd.org/mksh.htm
[modd]: https://github.com/cortesi/modd
[mvdan-sh]: https://www.npmjs.com/package/mvdan-sh
[neoformat]: https://github.com/sbdchd/neoformat
[nixos]: https://github.com/NixOS/nixpkgs/blob/HEAD/pkgs/tools/text/shfmt/default.nix
[OpenSUSE]: https://build.opensuse.org/package/show/openSUSE:Factory/shfmt
[posix shell]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
[posix-ambiguity]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_03
[pre-commit]: https://pre-commit.com
[pre-commit-shfmt]: https://github.com/scop/pre-commit-shfmt
[prettier-plugin-sh]: https://github.com/un-ts/prettier/tree/master/packages/sh
[prettier]: https://prettier.io
[scdoc]: https://sr.ht/~sircmpwn/scdoc/
[scoop]: https://github.com/ScoopInstaller/Main/blob/HEAD/bucket/shfmt.json
[sh-checker]: https://github.com/luizm/action-sh-checker
[sh-syntax]: https://github.com/un-ts/sh-syntax
[shell-format]: https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format
[shfmt.el]: https://github.com/purcell/emacs-shfmt/
[snapcraft]: https://snapcraft.io/shfmt
[sublime-pretty-shell]: https://github.com/aerobounce/Sublime-Pretty-Shell
[trunk]: https://trunk.io/check
[vim-shfmt]: https://github.com/z0mbix/vim-shfmt
[void]: https://github.com/void-linux/void-packages/blob/HEAD/srcpkgs/shfmt/template
[vscode-shfmt]: https://marketplace.visualstudio.com/items?itemName=mkhl.shfmt
[webi]: https://webinstall.dev/shfmt/
07070100000009000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000000E00000000sh-3.12.0/cmd0707010000000A000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001300000000sh-3.12.0/cmd/gosh0707010000000B000081A4000000000000000000000001686AE5C0000007C3000000000000000000000000000000000000001B00000000sh-3.12.0/cmd/gosh/main.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// gosh is a proof of concept shell built on top of [interp].
package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"

	"golang.org/x/term"

	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/syntax"
)

var command = flag.String("c", "", "command to be executed")

func main() {
	flag.Parse()
	err := runAll()
	var es interp.ExitStatus
	if errors.As(err, &es) {
		os.Exit(int(es))
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func runAll() error {
	r, err := interp.New(interp.Interactive(true), interp.StdIO(os.Stdin, os.Stdout, os.Stderr))
	if err != nil {
		return err
	}

	if *command != "" {
		return run(r, strings.NewReader(*command), "")
	}
	if flag.NArg() == 0 {
		if term.IsTerminal(int(os.Stdin.Fd())) {
			return runInteractive(r, os.Stdin, os.Stdout, os.Stderr)
		}
		return run(r, os.Stdin, "")
	}
	for _, path := range flag.Args() {
		if err := runPath(r, path); err != nil {
			return err
		}
	}
	return nil
}

func run(r *interp.Runner, reader io.Reader, name string) error {
	prog, err := syntax.NewParser().Parse(reader, name)
	if err != nil {
		return err
	}
	r.Reset()
	ctx := context.Background()
	return r.Run(ctx, prog)
}

func runPath(r *interp.Runner, path string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()
	return run(r, f, path)
}

func runInteractive(r *interp.Runner, stdin io.Reader, stdout, stderr io.Writer) error {
	parser := syntax.NewParser()
	fmt.Fprintf(stdout, "$ ")
	var runErr error
	fn := func(stmts []*syntax.Stmt) bool {
		if parser.Incomplete() {
			fmt.Fprintf(stdout, "> ")
			return true
		}
		ctx := context.Background()
		for _, stmt := range stmts {
			runErr = r.Run(ctx, stmt)
			if r.Exited() {
				return false
			}
		}
		fmt.Fprintf(stdout, "$ ")
		return true
	}
	if err := parser.Interactive(stdin, fn); err != nil {
		return err
	}
	return runErr
}
0707010000000C000081A4000000000000000000000001686AE5C0000013CE000000000000000000000000000000000000002000000000sh-3.12.0/cmd/gosh/main_test.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package main

import (
	"fmt"
	"io"
	"os"
	"testing"

	"github.com/go-quicktest/qt"
	"mvdan.cc/sh/v3/interp"
)

// Each test has an even number of strings, which form input-output pairs for
// the interactive shell. The input string is fed to the interactive shell, and
// bytes are read from its output until the expected output string is matched or
// an error is encountered.
//
// In other words, each first string is what the user types, and each following
// string is what the shell will print back. Note that the first "$ " output is
// implicit.

var interactiveTests = []struct {
	pairs   []string
	wantErr string
}{
	{},
	{
		pairs: []string{
			"\n",
			"$ ",
			"\n",
			"$ ",
		},
	},
	{
		pairs: []string{
			"echo foo\n",
			"foo\n",
		},
	},
	{
		pairs: []string{
			"echo foo\n",
			"foo\n$ ",
			"echo bar\n",
			"bar\n",
		},
	},
	{
		pairs: []string{
			"if true\n",
			"> ",
			"then echo bar; fi\n",
			"bar\n",
		},
	},
	{
		pairs: []string{
			"echo 'foo\n",
			"> ",
			"bar'\n",
			"foo\nbar\n",
		},
	},
	{
		pairs: []string{
			"echo foo; echo bar\n",
			"foo\nbar\n",
		},
	},
	{
		pairs: []string{
			"echo foo; echo 'bar\n",
			"> ",
			"baz'\n",
			"foo\nbar\nbaz\n",
		},
	},
	{
		pairs: []string{
			"(\n",
			"> ",
			"echo foo)\n",
			"foo\n",
		},
	},
	{
		pairs: []string{
			"[[\n",
			"> ",
			"true ]]\n",
			"$ ",
		},
	},
	{
		pairs: []string{
			"echo foo ||\n",
			"> ",
			"echo bar\n",
			"foo\n",
		},
	},
	{
		pairs: []string{
			"echo foo |\n",
			"> ",
			"read var; echo $var\n",
			"foo\n",
		},
	},
	{
		pairs: []string{
			"echo foo",
			"",
			" bar\n",
			"foo bar\n",
		},
	},
	{
		pairs: []string{
			"echo\\\n",
			"> ",
			" foo\n",
			"foo\n",
		},
	},
	{
		pairs: []string{
			"echo foo\\\n",
			"> ",
			"bar\n",
			"foobar\n",
		},
	},
	{
		pairs: []string{
			"echo 你好\n",
			"你好\n$ ",
		},
	},
	{
		pairs: []string{
			"echo *; :\n",
			"main.go main_test.go\n$ ",
			"echo *\n",
			"main.go main_test.go\n$ ",
			"shopt -s globstar; echo **\n",
			"main.go main_test.go\n$ ",
		},
	},
	{
		pairs: []string{
			"echo foo; exit 0; echo bar\n",
			"foo\n",
			"echo baz\n",
			"",
		},
	},
	{
		pairs: []string{
			"echo foo; exit 1; echo bar\n",
			"foo\n",
			"echo baz\n",
			"",
		},
		wantErr: "exit status 1",
	},
	{
		pairs: []string{
			"(\n",
			"> ",
		},
		wantErr: "1:1: reached EOF without matching ( with )",
	},
	{
		pairs: []string{
			"gosh_alias arg || true\n",
			"\"gosh_alias\": executable file not found in $PATH\n$ ",
			"alias gosh_alias=echo\n",
			"$ ",
			"gosh_alias arg || true\n",
			"arg\n$ ",
			"unalias gosh_alias\n",
			"$ ",
			"gosh_alias arg || true\n",
			"\"gosh_alias\": executable file not found in $PATH\n$ ",
		},
	},
}

func TestInteractive(t *testing.T) {
	t.Parallel()
	for _, tc := range interactiveTests {
		t.Run("", func(t *testing.T) {
			inReader, inWriter, err := os.Pipe()
			qt.Assert(t, qt.IsNil(err))
			outReader, outWriter, err := os.Pipe()
			qt.Assert(t, qt.IsNil(err))
			runner, _ := interp.New(interp.Interactive(true), interp.StdIO(inReader, outWriter, outWriter))
			errc := make(chan error, 1)
			go func() {
				errc <- runInteractive(runner, inReader, outWriter, outWriter)
				// Discard the rest of the input.
				io.Copy(io.Discard, inReader)
				inReader.Close()
				outWriter.Close()
			}()

			if err := readString(outReader, "$ "); err != nil {
				t.Fatal(err)
			}

			line := 1
			for len(tc.pairs) > 0 {
				t.Logf("write %q", tc.pairs[0])
				if _, err := io.WriteString(inWriter, tc.pairs[0]); err != nil {
					t.Fatal(err)
				}
				t.Logf("read %q", tc.pairs[1])
				if err := readString(outReader, tc.pairs[1]); err != nil {
					t.Fatal(err)
				}

				line++
				tc.pairs = tc.pairs[2:]
			}

			// Close the input pipe, so that the parser can stop.
			inWriter.Close()

			// Once the input pipe is closed, close the output pipe
			// so that any remaining prompt writes get discarded.
			outReader.Close()

			err = <-errc
			if err != nil && tc.wantErr == "" {
				t.Fatalf("unexpected error: %v", err)
			} else if tc.wantErr != "" && fmt.Sprint(err) != tc.wantErr {
				t.Fatalf("want error %q, got: %v", tc.wantErr, err)
			}
		})
	}
}

func TestInteractiveExit(t *testing.T) {
	inReader, inWriter, err := os.Pipe()
	qt.Assert(t, qt.IsNil(err))
	defer inReader.Close()
	go func() {
		io.WriteString(inWriter, "exit\n")
		inWriter.Close()
	}()
	w := io.Discard
	runner, _ := interp.New(interp.Interactive(true), interp.StdIO(inReader, w, w))
	if err := runInteractive(runner, inReader, w, w); err != nil {
		t.Fatal("expected a nil error")
	}
}

// readString will keep reading from a reader until all bytes from the supplied
// string are read.
func readString(r io.Reader, want string) error {
	p := make([]byte, len(want))
	_, err := io.ReadFull(r, p)
	if err != nil {
		return err
	}
	got := string(p)
	if got != want {
		return fmt.Errorf("ReadString: read %q, wanted %q", got, want)
	}
	return nil
}
0707010000000D000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001400000000sh-3.12.0/cmd/shfmt0707010000000E000081A4000000000000000000000001686AE5C0000001C1000000000000000000000000000000000000001F00000000sh-3.12.0/cmd/shfmt/DockerfileFROM golang:1.24.0-alpine AS build

WORKDIR /src
RUN apk add --no-cache git
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-w -s -extldflags '-static' -X main.version=$(git describe --always --dirty --tags)" ./cmd/shfmt

FROM alpine:3.21.2 AS alpine
COPY --from=build /src/shfmt /bin/shfmt
COPY "./cmd/shfmt/docker-entrypoint.sh" "/init"
ENTRYPOINT ["/init"]

FROM scratch
COPY --from=build /src/shfmt /bin/shfmt
ENTRYPOINT ["/bin/shfmt"]
CMD ["-h"]
0707010000000F000081ED000000000000000000000001686AE5C00000022C000000000000000000000000000000000000002900000000sh-3.12.0/cmd/shfmt/docker-entrypoint.sh#!/bin/sh
# SPDX-License-Identifier: BSD-3-Clause
#
# Copyright (C) 2019 Olliver Schinagl <oliver@schinagl.nl>
#
# A beginning user should be able to docker run image bash (or sh) without
# needing to learn about --entrypoint
# https://github.com/docker-library/official-images#consistency

set -eu

# run command if it is not starting with a "-" and is an executable in PATH
if [ "${#}" -gt 0 ] &&
	[ "${1#-}" = "${1}" ] &&
	command -v "${1}" >"/dev/null" 2>&1; then
	exec "${@}"
else
	# else default to run the command
	exec /bin/shfmt "${@}"
fi

exit 0
07070100000010000081A4000000000000000000000001686AE5C000004745000000000000000000000000000000000000001C00000000sh-3.12.0/cmd/shfmt/main.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// shfmt formats shell programs.
package main

import (
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"runtime/debug"
	"strings"

	maybeio "github.com/google/renameio/v2/maybe"
	diffpkg "github.com/rogpeppe/go-internal/diff"
	"golang.org/x/term"
	"mvdan.cc/editorconfig"

	"mvdan.cc/sh/v3/fileutil"
	"mvdan.cc/sh/v3/syntax"
	"mvdan.cc/sh/v3/syntax/typedjson"
)

type boolString string

func (s *boolString) Set(val string) error {
	*s = boolString(val)
	return nil
}
func (s *boolString) Get() any       { return string(*s) }
func (s *boolString) String() string { return string(*s) }
func (*boolString) IsBoolFlag() bool { return true }

type multiFlag[T any] struct {
	short, long string
	val         T
}

var (
	// Generic flags.
	versionFlag = &multiFlag[bool]{"", "version", false}
	list        = &multiFlag[boolString]{"l", "list", "false"}
	write       = &multiFlag[bool]{"w", "write", false}
	diff        = &multiFlag[bool]{"d", "diff", false}
	applyIgnore = &multiFlag[bool]{"", "apply-ignore", false}
	filename    = &multiFlag[string]{"", "filename", ""}

	// Parser flags.
	lang     = &multiFlag[syntax.LangVariant]{"ln", "language-dialect", syntax.LangAuto}
	posix    = &multiFlag[bool]{"p", "posix", false}
	simplify = &multiFlag[bool]{"s", "simplify", false}
	// TODO: when promoting exp.recover to a stable flag, add it as an EditorConfig knob too, and perhaps rename to recover-errors
	expRecover = &multiFlag[int]{"", "exp.recover", 0}

	// Printer flags.
	indent      = &multiFlag[uint]{"i", "indent", 0}
	binNext     = &multiFlag[bool]{"bn", "binary-next-line", false}
	caseIndent  = &multiFlag[bool]{"ci", "case-indent", false}
	spaceRedirs = &multiFlag[bool]{"sr", "space-redirects", false}
	keepPadding = &multiFlag[bool]{"kp", "keep-padding", false}
	funcNext    = &multiFlag[bool]{"fn", "func-next-line", false}
	minify      = &multiFlag[bool]{"mn", "minify", false}

	// Utility flags.
	find     = &multiFlag[boolString]{"f", "find", "false"}
	toJSON   = &multiFlag[bool]{"tojson", "to-json", false} // TODO(v4): remove "tojson" for consistency
	fromJSON = &multiFlag[bool]{"", "from-json", false}

	// useEditorConfig will be false if any parser or printer flags were used.
	useEditorConfig = true

	parser            *syntax.Parser
	printer           *syntax.Printer
	readBuf, writeBuf bytes.Buffer
	color             bool

	copyBuf = make([]byte, 32*1024)

	version = "(devel)" // to match the default from runtime/debug

	allFlags = []any{
		versionFlag, list, write, find, diff, applyIgnore,
		lang, posix, filename, simplify, expRecover,
		indent, binNext, caseIndent, spaceRedirs, keepPadding, funcNext, minify,
		toJSON, fromJSON,
	}
)

func init() {
	// TODO: the flag package has constructors like newBoolValue;
	// if we had access to something like that, we could use [flag.Value] everywhere,
	// and avoid this monstrosity of a type switch.
	for _, f := range allFlags {
		switch f := f.(type) {
		case *multiFlag[bool]:
			if name := f.short; name != "" {
				flag.BoolVar(&f.val, name, f.val, "")
			}
			if name := f.long; name != "" {
				flag.BoolVar(&f.val, name, f.val, "")
			}
		case *multiFlag[boolString]:
			if name := f.short; name != "" {
				flag.Var(&f.val, name, "")
			}
			if name := f.long; name != "" {
				flag.Var(&f.val, name, "")
			}
		case *multiFlag[string]:
			if name := f.short; name != "" {
				flag.StringVar(&f.val, name, f.val, "")
			}
			if name := f.long; name != "" {
				flag.StringVar(&f.val, name, f.val, "")
			}
		case *multiFlag[int]:
			if name := f.short; name != "" {
				flag.IntVar(&f.val, name, f.val, "")
			}
			if name := f.long; name != "" {
				flag.IntVar(&f.val, name, f.val, "")
			}
		case *multiFlag[uint]:
			if name := f.short; name != "" {
				flag.UintVar(&f.val, name, f.val, "")
			}
			if name := f.long; name != "" {
				flag.UintVar(&f.val, name, f.val, "")
			}
		case *multiFlag[syntax.LangVariant]:
			if name := f.short; name != "" {
				flag.Var(&f.val, name, "")
			}
			if name := f.long; name != "" {
				flag.Var(&f.val, name, "")
			}
		default:
			panic(fmt.Sprintf("%T", f))
		}
	}
}

func main() {
	flag.Usage = func() {
		fmt.Fprint(os.Stderr, `usage: shfmt [flags] [path ...]

shfmt formats shell programs. If the only argument is a dash ('-') or no
arguments are given, standard input will be used. If a given path is a
directory, all shell scripts found under that directory will be used.

  --version  show version and exit

  -l[=0], --list[=0]  list files whose formatting differs from shfmt;
                      paths are separated by a newline or a null character if -l=0
  -w,     --write     write result to file instead of stdout
  -d,     --diff      error with a diff when the formatting differs
  --apply-ignore      always apply EditorConfig ignore rules
  --filename str      provide a name for the standard input file

Parser options:

  -ln, --language-dialect str  bash/posix/mksh/bats, default "auto"
  -p,  --posix                 shorthand for -ln=posix
  -s,  --simplify              simplify the code

Printer options:

  -i,  --indent uint       0 for tabs (default), >0 for number of spaces
  -bn, --binary-next-line  binary ops like && and | may start a line
  -ci, --case-indent       switch cases will be indented
  -sr, --space-redirects   redirect operators will be followed by a space
  -kp, --keep-padding      keep column alignment paddings
  -fn, --func-next-line    function opening braces are placed on a separate line
  -mn, --minify             minify the code to reduce its size (implies -s)

Utilities:

  -f[=0], --find[=0]  recursively find all shell files and print the paths;
                      paths are separated by a newline or a null character if -f=0
  --to-json           print syntax tree to stdout as a typed JSON
  --from-json         read syntax tree from stdin as a typed JSON

Formatting options can also be read from EditorConfig files; see 'man shfmt'
for a detailed description of the tool's behavior.
For more information and to report bugs, see https://github.com/mvdan/sh.
`)
	}
	flag.Parse()

	if versionFlag.val {
		// don't overwrite the version if it was set by -ldflags=-X
		if info, ok := debug.ReadBuildInfo(); ok && version == "(devel)" {
			mod := &info.Main
			if mod.Replace != nil {
				mod = mod.Replace
			}
			version = mod.Version
		}
		fmt.Println(version)
		return
	}
	if posix.val && lang.val != syntax.LangAuto {
		fmt.Fprintf(os.Stderr, "-p and -ln=lang cannot coexist\n")
		os.Exit(1)
	}
	if list.val != "true" && list.val != "false" && list.val != "0" {
		fmt.Fprintf(os.Stderr, "only -l and -l=0 allowed\n")
		os.Exit(1)
	}
	if find.val != "true" && find.val != "false" && find.val != "0" {
		fmt.Fprintf(os.Stderr, "only -f and -f=0 allowed\n")
		os.Exit(1)
	}
	simplify.val = simplify.val || minify.val
	flag.Visit(func(f *flag.Flag) {
		// This list should be in sync with the grouping of parser and printer options
		// as shown by ./shfmt.1.scd.
		switch f.Name {
		case lang.short, lang.long,
			posix.short, posix.long,
			simplify.short, simplify.long,
			indent.short, indent.long,
			binNext.short, binNext.long,
			caseIndent.short, caseIndent.long,
			spaceRedirs.short, spaceRedirs.long,
			keepPadding.short, keepPadding.long,
			funcNext.short, funcNext.long,
			minify.short, minify.long:
			useEditorConfig = false
		}
	})
	parser = syntax.NewParser(syntax.KeepComments(true))
	printer = syntax.NewPrinter(syntax.Minify(minify.val))

	syntax.RecoverErrors(expRecover.val)(parser)

	if !useEditorConfig {
		if posix.val {
			// -p equals -ln=posix
			lang.val = syntax.LangPOSIX
		}

		syntax.Indent(indent.val)(printer)
		syntax.BinaryNextLine(binNext.val)(printer)
		syntax.SwitchCaseIndent(caseIndent.val)(printer)
		syntax.SpaceRedirects(spaceRedirs.val)(printer)
		syntax.KeepPadding(keepPadding.val)(printer)
		syntax.FunctionNextLine(funcNext.val)(printer)
	}

	// Decide whether or not to use color for the diff output,
	// as described in shfmt.1.scd.
	if os.Getenv("FORCE_COLOR") != "" {
		color = true
	} else if os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" {
	} else if term.IsTerminal(int(os.Stdout.Fd())) {
		color = true
	}
	if flag.NArg() == 0 || (flag.NArg() == 1 && flag.Arg(0) == "-") {
		name := "<standard input>"
		if toJSON.val {
			name = "" // the default is not useful there
		}
		if filename.val != "" {
			name = filename.val
		}
		if err := formatStdin(name); err != nil {
			if err != errChangedWithDiff {
				fmt.Fprintln(os.Stderr, err)
			}
			os.Exit(1)
		}
		return
	}
	if filename.val != "" {
		fmt.Fprintln(os.Stderr, "-filename can only be used with stdin")
		os.Exit(1)
	}
	if toJSON.val {
		fmt.Fprintln(os.Stderr, "--to-json can only be used with stdin")
		os.Exit(1)
	}
	status := 0
	for _, path := range flag.Args() {
		if info, err := os.Stat(path); err == nil && !info.IsDir() && !applyIgnore.val && find.val == "false" {
			// When given paths to files directly, always format them,
			// no matter their extension or shebang.
			//
			// One exception is --apply-ignore, which explicitly changes this behavior.
			// Another is --find, whose logic depends on walkPath being called.
			if err := formatPath(path, false); err != nil {
				if err != errChangedWithDiff {
					fmt.Fprintln(os.Stderr, err)
				}
				status = 1
			}
			continue
		}
		if err := filepath.WalkDir(path, func(path string, entry fs.DirEntry, err error) error {
			if err != nil {
				return err
			}
			switch err := walkPath(path, entry); err {
			case nil:
			case filepath.SkipDir:
				return err
			case errChangedWithDiff:
				status = 1
			default:
				fmt.Fprintln(os.Stderr, err)
				status = 1
			}
			return nil
		}); err != nil {
			fmt.Fprintln(os.Stderr, err)
			status = 1
		}
	}
	os.Exit(status)
}

var errChangedWithDiff = fmt.Errorf("")

func formatStdin(name string) error {
	if write.val {
		return fmt.Errorf("-w cannot be used on standard input")
	}
	if applyIgnore.val {
		// Mimic the logic from walkPath to apply the ignore rules.
		props, err := ecQuery.Find(name, []string{"shell"})
		if err != nil {
			return err
		}
		if props.Get("ignore") == "true" {
			return nil
		}
	}
	src, err := io.ReadAll(os.Stdin)
	if err != nil {
		return err
	}
	fileLang := lang.val
	if fileLang == syntax.LangAuto {
		extensionLang := strings.TrimPrefix(filepath.Ext(name), ".")
		if err := fileLang.Set(extensionLang); err != nil || fileLang == syntax.LangPOSIX {
			shebangLang := fileutil.Shebang(src)
			if err := fileLang.Set(shebangLang); err != nil {
				// Fall back to bash.
				fileLang = syntax.LangBash
			}
		}
	}
	return formatBytes(src, name, fileLang)
}

var vcsDir = regexp.MustCompile(`^\.(git|svn|hg)$`)

func walkPath(path string, entry fs.DirEntry) error {
	if entry.IsDir() && vcsDir.MatchString(entry.Name()) {
		return filepath.SkipDir
	}
	// We don't know the language variant at this point yet, as we are walking directories
	// and we first want to tell if we should skip a path entirely.
	//
	// TODO: Should the call to Find with the language name check "ignore" too, then?
	// Otherwise, a [[bash]] section with ignore=true is effectively never used.
	//
	// TODO: Should there be a way to explicitly turn off ignore rules when walking?
	// Perhaps swapping the default to --apply-ignore=auto and allowing --apply-ignore=false?
	// I don't imagine it's a particularly useful scenario for now.
	props, err := ecQuery.Find(path, []string{"shell"})
	if err != nil {
		return err
	}
	if props.Get("ignore") == "true" {
		if entry.IsDir() {
			return filepath.SkipDir
		} else {
			return nil
		}
	}
	conf := fileutil.CouldBeScript2(entry)
	if conf == fileutil.ConfNotScript {
		return nil
	}
	err = formatPath(path, conf == fileutil.ConfIfShebang)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	return nil
}

var ecQuery = editorconfig.Query{
	FileCache:   make(map[string]*editorconfig.File),
	RegexpCache: make(map[string]*regexp.Regexp),
}

func propsOptions(lang syntax.LangVariant, props editorconfig.Section) (_ syntax.LangVariant, validLang bool) {
	// if shell_variant is set to a valid string, it will take precedence
	langErr := lang.Set(props.Get("shell_variant"))
	syntax.Variant(lang)(parser)

	size := uint(0)
	if props.Get("indent_style") == "space" {
		size = 8
		if n := props.IndentSize(); n > 0 {
			size = uint(n)
		}
	}
	syntax.Indent(size)(printer)

	syntax.BinaryNextLine(props.Get("binary_next_line") == "true")(printer)
	// TODO(v4): rename to case_indent for consistency with flags
	syntax.SwitchCaseIndent(props.Get("switch_case_indent") == "true")(printer)
	syntax.SpaceRedirects(props.Get("space_redirects") == "true")(printer)
	syntax.KeepPadding(props.Get("keep_padding") == "true")(printer)
	// TODO(v4): rename to func_next_line for consistency with flags
	syntax.FunctionNextLine(props.Get("function_next_line") == "true")(printer)

	minify := props.Get("minify") == "true"
	syntax.Minify(minify)(printer)
	// Note that --simplify is not actually a parser option, so we use a global var.
	// Just like the CLI flags, minify=true implies simplify=true.
	simplify.val = minify || props.Get("simplify") == "true"

	return lang, langErr == nil
}

func formatPath(path string, checkShebang bool) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	fileLang := lang.val
	shebangForAuto := false
	if fileLang == syntax.LangAuto {
		extensionLang := strings.TrimPrefix(filepath.Ext(path), ".")
		if err := fileLang.Set(extensionLang); err != nil || fileLang == syntax.LangPOSIX {
			shebangForAuto = true
		}
	}
	readBuf.Reset()
	if checkShebang || shebangForAuto {
		n, err := io.ReadAtLeast(f, copyBuf[:32], len("#!/bin/sh\n"))
		switch {
		case !checkShebang:
			// only wanted the shebang for LangAuto
		case err == io.EOF, errors.Is(err, io.ErrUnexpectedEOF):
			return nil // too short to have a shebang
		case err != nil:
			return err // some other read error
		}
		shebangLang := fileutil.Shebang(copyBuf[:n])
		if checkShebang && shebangLang == "" {
			return nil // not a shell script
		}
		if shebangForAuto {
			if err := fileLang.Set(shebangLang); err != nil {
				// Fall back to bash.
				fileLang = syntax.LangBash
			}
		}
		readBuf.Write(copyBuf[:n])
	}
	switch find.val {
	case "true":
		fmt.Println(path)
		return nil
	case "0":
		fmt.Print(path)
		fmt.Print("\000")
		return nil
	}
	if _, err := io.CopyBuffer(&readBuf, f, copyBuf); err != nil {
		return err
	}
	f.Close()
	return formatBytes(readBuf.Bytes(), path, fileLang)
}

func editorConfigLangs(l syntax.LangVariant) []string {
	// All known shells match [[shell]].
	// As a special case, bash and the bash-like bats also match [[bash]]
	// We can later consider others like [[mksh]] or [[posix-shell]],
	// just consider what list of languages the EditorConfig spec might eventually use.
	switch l {
	case syntax.LangBash, syntax.LangBats:
		return []string{"shell", "bash"}
	case syntax.LangPOSIX, syntax.LangMirBSDKorn, syntax.LangAuto:
		return []string{"shell"}
	}
	return nil
}

func formatBytes(src []byte, path string, fileLang syntax.LangVariant) error {
	fileLangFromEditorConfig := false
	if useEditorConfig {
		props, err := ecQuery.Find(path, editorConfigLangs(fileLang))
		if err != nil {
			return err
		}
		fileLang, fileLangFromEditorConfig = propsOptions(fileLang, props)
	} else {
		syntax.Variant(fileLang)(parser)
	}
	var node syntax.Node
	var err error
	if fromJSON.val {
		node, err = typedjson.Decode(bytes.NewReader(src))
		if err != nil {
			return err
		}
	} else {
		node, err = parser.Parse(bytes.NewReader(src), path)
		if err != nil {
			if s, ok := err.(syntax.LangError); ok && lang.val == syntax.LangAuto {
				if fileLangFromEditorConfig {
					return fmt.Errorf("%w (parsed as %s via EditorConfig)", s, fileLang)
				}
				return fmt.Errorf("%w (parsed as %s via -%s=%s)", s, fileLang, lang.short, lang.val)
			}
			return err
		}
	}
	// Note that --simplify is treated as a parser option as it happens
	// immediately after parsing, even if it's not a [syntax.ParserOption] today.
	if simplify.val {
		syntax.Simplify(node)
	}
	if toJSON.val {
		// must be standard input; fine to return
		// TODO: change the default behavior to be compact,
		// and allow using --to-json=pretty or --to-json=indent.
		return typedjson.EncodeOptions{Indent: "\t"}.Encode(os.Stdout, node)
	}
	writeBuf.Reset()
	printer.Print(&writeBuf, node)
	res := writeBuf.Bytes()
	if !bytes.Equal(src, res) {
		switch list.val {
		case "true":
			fmt.Println(path)
		case "0":
			fmt.Print(path)
			fmt.Print("\000")
		}
		if write.val {
			info, err := os.Lstat(path)
			if err != nil {
				return err
			}
			if !info.Mode().IsRegular() {
				return fmt.Errorf("refusing to atomically replace %q with a regular file as it is not one already", path)
			}
			perm := info.Mode().Perm()
			// TODO: support atomic writes on Windows?
			if err := maybeio.WriteFile(path, res, perm); err != nil {
				return err
			}
		}
		if diff.val {
			diffBytes := diffpkg.Diff(path+".orig", src, path, res)
			if !color {
				os.Stdout.Write(diffBytes)
				return errChangedWithDiff
			}
			// The first three lines are the header with the filenames, including --- and +++,
			// and are marked in bold.
			current := terminalBold
			os.Stdout.WriteString(current)
			for i, line := range bytes.SplitAfter(diffBytes, []byte("\n")) {
				last := current
				switch {
				case i < 3: // the first three lines are bold
				case bytes.HasPrefix(line, []byte("@@")):
					current = terminalCyan
				case bytes.HasPrefix(line, []byte("-")):
					current = terminalRed
				case bytes.HasPrefix(line, []byte("+")):
					current = terminalGreen
				default:
					current = terminalReset
				}
				if current != last {
					os.Stdout.WriteString(current)
				}
				os.Stdout.Write(line)
			}
			return errChangedWithDiff
		}
	}
	if list.val == "false" && !write.val && !diff.val {
		os.Stdout.Write(res)
	}
	return nil
}

const (
	terminalGreen = "\u001b[32m"
	terminalRed   = "\u001b[31m"
	terminalCyan  = "\u001b[36m"
	terminalReset = "\u001b[0m"
	terminalBold  = "\u001b[1m"
)
07070100000011000081A4000000000000000000000001686AE5C000000243000000000000000000000000000000000000002100000000sh-3.12.0/cmd/shfmt/main_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package main

import (
	"flag"
	"path/filepath"
	"testing"

	"github.com/rogpeppe/go-internal/testscript"
)

func TestMain(m *testing.M) {
	testscript.Main(m, map[string]func(){
		"shfmt": main,
	})
}

var update = flag.Bool("u", false, "update testscript output files")

func TestScript(t *testing.T) {
	t.Parallel()
	testscript.Run(t, testscript.Params{
		Dir:                 filepath.Join("testdata", "script"),
		UpdateScripts:       *update,
		RequireExplicitExec: true,
	})
}
07070100000012000081A4000000000000000000000001686AE5C00000146A000000000000000000000000000000000000002000000000sh-3.12.0/cmd/shfmt/shfmt.1.scdshfmt(1)

; To render and view: scdoc <shfmt.1.scd | man -l -

# NAME

shfmt - Format shell programs

# SYNOPSIS

*shfmt* [flags] [path...]

# DESCRIPTION

shfmt formats shell programs. If the only argument is a dash (*-*) or no
arguments are given, standard input will be used. If a given path is a
directory, all shell scripts found under that directory will be used.

If any EditorConfig files are found, they will be used to apply formatting
options. If any parser or printer flags are given to the tool, no EditorConfig
formatting options will be used. A default like *-i=0* can be used for this purpose.

shfmt's default shell formatting was chosen to be consistent, common, and
predictable. Some aspects of the format can be configured via printer flags.

# OPTIONS

## Generic flags

*--version*
	Show version and exit.

*-l[=0]*, *--list[=0]*
	List files whose formatting differs from shfmt's;
	paths are separated by a newline or a null character if -l=0

*-w*, *--write*
	Write result to file instead of stdout.

*-d*, *--diff*
	Error with a diff when the formatting differs.

	The diff uses color when the output is a terminal.
	To never use color, set a non-empty *NO_COLOR* or *TERM=dumb*.
	To always use color, set a non-empty *FORCE_COLOR*.

*--apply-ignore*
	Always apply EditorConfig ignore rules.

	When formatting files directly, ignore rules are skipped without this flag.
	Should be useful to any tools or editors which format stdin or a single file.
	When printing results to stdout, an ignored file results in no output at all.

*--filename* str
	Provide a name for the standard input file.

	Use of this flag is necessary for EditorConfig support to work with stdin,
	since EditorConfig files are found relative to the location of a script.

## Parser flags

*-ln*, *--language-dialect* <str>
	Language dialect (*bash*/*posix*/*mksh*/*bats*, default *auto*).

	When set to *auto*, the language is detected from the input filename,
	as long as it has a shell extension like *foo.mksh*. Otherwise, if the input
	begins with a shell shebang like *#!/bin/sh*, that's used instead.
	If neither come up with a result, *bash* is used as a fallback.

	The filename extension *.sh* is a special case: it implies *posix*,
	but may be overridden by a valid shell shebang.

*-p*, *--posix*
	Shorthand for *-ln=posix*.

*-s*, *--simplify*
	Simplify the code.

## Printer flags

*-i*, *--indent* <uint>
	Indent: *0* for tabs (default), *>0* for number of spaces.

*-bn*, *--binary-next-line*
	Binary ops like *&&* and *|* may start a line.

*-ci*, *--case-indent*
	Switch cases will be indented.

*-sr*, *--space-redirects*
	Redirect operators will be followed by a space.

*-kp*, *--keep-padding*
	Keep column alignment paddings.

	This flag is *DEPRECATED* and will be removed in the next major version.
	For more information, see: https://github.com/mvdan/sh/issues/658

*-fn*, *--func-next-line*
	Function opening braces are placed on a separate line.

*-mn*, *--minify*
	Minify the code to reduce its size (implies *-s*).

## Utility flags

*-f[=0]*, *--find[=0]*
	Recursively find all shell files and print the paths;
	paths are separated by a newline or a null character if -f=0.

*--to-json*
	Print syntax tree to stdout as a typed JSON.

*--from-json*
	Read syntax tree from stdin as a typed JSON.

# EXAMPLES

Format all the scripts under the current directory, printing which are modified:

	shfmt -l -w .

For CI, one can use a variant where formatting changes are just shown as diffs:

	shfmt -d .

The following formatting flags closely resemble Google's shell style defined in
<https://google.github.io/styleguide/shellguide.html>:

	shfmt -i 2 -ci -bn

Below is a sample EditorConfig file as defined by <https://editorconfig.org/>,
showing how to set any option:

```
[*.sh]
# like -i=4
indent_style = space
indent_size = 4

# --language-variant
shell_variant      = posix
binary_next_line   = true
# --case-indent
switch_case_indent = true
space_redirects    = true
keep_padding       = true
# --func-next-line
function_next_line = true

# Ignore the entire "third_party" directory when calling shfmt on directories,
# such as "shfmt -l -w .". When formatting files directly,
# like "shfmt -w third_party/foo.sh" or "shfmt --filename=third_party/foo.sh",
# the ignore logic is applied only when the --apply-ignore flag is given.
[third_party/**]
ignore = true
```

EditorConfig sections may also use `[[shell]]` or `[[bash]]` to match any shell or bash scripts,
which is particularly useful when scripts use a shebang but no extension.
Note that this feature is outside of the EditorConfig spec and may be changed in the future.

shfmt can also replace *bash -n* to check shell scripts for syntax errors. It is
more exhaustive, as it parses all syntax statically and requires valid UTF-8:

```
$ echo '${foo:1 2}' | bash -n
$ echo '${foo:1 2}' | shfmt >/dev/null
1:9: not a valid arithmetic operator: 2
$ echo 'foo=(1 2)' | bash --posix -n
$ echo 'foo=(1 2)' | shfmt -p >/dev/null
1:5: arrays are a bash feature
```

# AUTHORS

Maintained by Daniel Martí <mvdan@mvdan.cc>, who is assisted by other open
source contributors. For more information and development, see
<https://github.com/mvdan/sh>.
07070100000013000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001D00000000sh-3.12.0/cmd/shfmt/testdata07070100000014000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000002400000000sh-3.12.0/cmd/shfmt/testdata/script07070100000015000081A4000000000000000000000001686AE5C000000219000000000000000000000000000000000000003100000000sh-3.12.0/cmd/shfmt/testdata/script/atomic.txtar[windows] skip 'atomic writes aren''t supported on Windows'
[!exec:sh] skip 'sh is required to run this test'

# If we don't do atomic writes, most shells will error when shfmt overwrites the
# very script it's running from. This is because the shell doesn't read all of
# the input script upfront.
exec sh input.sh
cmp stdout stdout.golden
! stderr .

cmp input.sh input.sh.golden

-- input.sh --
echo foo
  shfmt -l -w input.sh
echo bar
-- input.sh.golden --
echo foo
shfmt -l -w input.sh
echo bar
-- stdout.golden --
foo
input.sh
bar
07070100000016000081A4000000000000000000000001686AE5C00000028B000000000000000000000000000000000000003000000000sh-3.12.0/cmd/shfmt/testdata/script/basic.txtarcp input.sh input.sh.orig

stdin input.sh
exec shfmt
cmp stdout input.sh.golden
! stderr .

stdin input.sh
exec shfmt -
cmp stdout input.sh.golden
! stderr .

exec shfmt input.sh
cmp stdout input.sh.golden
! stderr .

exec shfmt -l input.sh
stdout 'input\.sh'
! stdout foo
! stderr .
cmp input.sh input.sh.orig

exec shfmt -l input.sh input.sh
stdout -count=2 'input.sh'

exec shfmt -l -w input.sh
stdout 'input\.sh'
! stdout foo
! stderr .
cmp input.sh input.sh.golden

cp input.sh.orig input.sh
exec shfmt --list --write input.sh
stdout 'input\.sh'
! stdout foo
! stderr .
cmp input.sh input.sh.golden

-- input.sh --
 foo
-- input.sh.golden --
foo
07070100000017000081A4000000000000000000000001686AE5C000000307000000000000000000000000000000000000002F00000000sh-3.12.0/cmd/shfmt/testdata/script/diff.txtarstdin input.sh
! exec shfmt -d
cmp stdout input.sh.stdindiff
! stderr .

stdin input.sh
! exec shfmt --diff
cmp stdout input.sh.stdindiff
! stderr .

! exec shfmt -d input.sh
cmp stdout input.sh.filediff
! stderr .

! exec shfmt -d input.sh input.sh
stdout -count=4 'input.sh.orig'

env FORCE_COLOR=true
stdin input.sh
! exec shfmt -d
stdout '\x1b\[31m- foo'
! stderr .
env FORCE_COLOR=

! exec shfmt -d .
cmp stdout input.sh.filediff
! stderr .

-- input.sh --
 foo


bar
-- input.sh.golden --
foo

bar
-- input.sh.stdindiff --
diff <standard input>.orig <standard input>
--- <standard input>.orig
+++ <standard input>
@@ -1,4 +1,3 @@
- foo
-
+foo
 
 bar
-- input.sh.filediff --
diff input.sh.orig input.sh
--- input.sh.orig
+++ input.sh
@@ -1,4 +1,3 @@
- foo
-
+foo
 
 bar
07070100000018000081A4000000000000000000000001686AE5C00000166F000000000000000000000000000000000000003700000000sh-3.12.0/cmd/shfmt/testdata/script/editorconfig.txtarcp input.sh input.sh.orig

# Using stdin should use EditorConfig.
stdin input.sh
exec shfmt
cmp stdout input.sh.golden
! stderr .

# Verify that -filename works well with EditorConfig.
stdin stdin-filename-bash
exec shfmt

stdin stdin-filename-bash
! exec shfmt -filename=foo_posix.sh
stderr '^foo_posix.sh:.* arrays are a bash.*parsed as posix via EditorConfig'

stdin stdin-filename-bash
! exec shfmt -filename=${WORK}/foo_posix.sh
stderr ^${WORK@R}/'foo_posix.sh:.* arrays are a bash.*parsed as posix via EditorConfig'

# Using a file path should use EditorConfig, including with the use of flags
# like -l.
exec shfmt input.sh
cmp stdout input.sh.golden
! stderr .

exec shfmt -l input.sh
stdout 'input\.sh'
! stderr .

# Using any formatting option should skip all EditorConfig usage.
exec shfmt -p input.sh
cmp stdout input.sh.orig
! stderr .

exec shfmt -l -p input.sh
! stdout .
! stderr .

exec shfmt -sr input.sh
cmp stdout input.sh.orig
! stderr .

# Check that EditorConfig files merge properly.
exec shfmt morespaces/input.sh
cmp stdout morespaces/input.sh.golden
! stderr .

# Check a folder with all other knobs.
exec shfmt -l otherknobs
! stdout .
! stderr .

# Files found by walking directories are skipped if they match ignore=true properties.
exec shfmt -l ignored
stdout 'regular\.sh'
! stdout 'ignored\.sh'
! stderr .

# EditorConfig ignore=true properties are obeyed even when any formatting flags
# are used, which cause formatting options from EditorConfig files to be skipped.
exec shfmt -i=0 -l ignored
stdout 'regular\.sh'
! stdout 'ignored\.sh'
! stderr .

# Formatting files directly does not obey ignore=true properties by default.
# Test the various modes in which shfmt can run.
! exec shfmt -l input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh
stdout -count=1 'input\.sh$'
stdout -count=1 'ignored\.sh$'
stderr -count=1 'ignored\.sh.* must be followed by'
! exec shfmt -d input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh
stdout -count=2 'input\.sh$'
stdout -count=2 'ignored\.sh$'
stderr -count=1 'ignored\.sh.* must be followed by'
! exec shfmt input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh
stdout -count=1 'indented'
stdout -count=1 'echo foo'
stderr -count=1 'ignored\.sh.* must be followed by'
stdin ignored/1_lone_ignored.sh
exec shfmt --filename=ignored/1_lone_ignored.sh
stdout -count=1 'echo foo'
! stderr .

# Formatting files directly obeys ignore=true when --apply-ignore is given.
# Test the same modes that the earlier section does.
exec shfmt --apply-ignore -l input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh
stdout -count=1 'input\.sh$'
! stdout 'ignored\.sh'
! stderr .
! exec shfmt --apply-ignore -d input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh
stdout -count=2 'input\.sh$'
! stdout 'ignored\.sh'
! stderr .
exec shfmt --apply-ignore input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh
stdout -count=1 'indented'
! stdout 'echo foo'
! stderr .
stdin ignored/1_lone_ignored.sh
exec shfmt --apply-ignore --filename=ignored/1_lone_ignored.sh
! stdout .
! stderr .

# Check EditorConfig [[language]] sections, used primarily for extension-less strings with shebangs.
exec shfmt -d shebang
! stdout .
! stderr .

# Verify that sibling EditorConfig files do not get their settings mixed up,
# which could happen if we incrementally use their flags without care.
exec shfmt -d multiconfig
! stdout .
! stderr .

-- .editorconfig --
root = true

[*]
indent_style = space
indent_size = 3

[*_posix.sh]
shell_variant = posix
-- input.sh --
{
	indented
}
-- input.sh.golden --
{
   indented
}
-- stdin-filename-bash --
array=(
	element
)
-- morespaces/.editorconfig --
[*.sh]
indent_size = 6
-- morespaces/input.sh --
{
	indented
}
-- morespaces/input.sh.golden --
{
      indented
}
-- otherknobs/.editorconfig --
root = true

[*_bash.sh]
shell_variant = bash

[*_mksh.sh]
shell_variant = mksh

[indent.sh]
# check its default; we tested "space" above.

[binary_next_line.sh]
binary_next_line = true

[switch_case_indent.sh]
switch_case_indent = true

[space_redirects.sh]
space_redirects = true

[keep_padding.sh]
keep_padding = true

[function_next_line.sh]
function_next_line = true

[simplify.sh]
simplify = true

[minify.sh]
minify = true

-- otherknobs/shell_variant_bash.sh --
array=(elem)
-- otherknobs/shell_variant_mksh.sh --
coprocess |&
-- otherknobs/indent.sh --
{
	indented
}
-- otherknobs/binary_next_line.sh --
foo \
	| bar
-- otherknobs/switch_case_indent.sh --
case "$1" in
	A) echo foo ;;
esac
-- otherknobs/space_redirects.sh --
echo foo > bar
-- otherknobs/keep_padding.sh --
echo  foo    bar
-- otherknobs/function_next_line.sh --
foo()
{
	echo foo
}
-- otherknobs/simplify.sh --
foo() {
	((bar))
}
-- otherknobs/minify.sh --
foo(){
((bar))
}
-- ignored/.editorconfig --
root = true

[third_party/**]
ignore = true

[1_lone_ignored.sh]
ignore = true

[2_dir_ignored]
ignore = true

-- ignored/third_party/bad_syntax_ignored.sh --
bad (syntax
-- ignored/1_lone_ignored.sh --
echo   foo
-- ignored/2_dir_ignored/ignored.sh --
echo   foo
-- ignored/3_regular/regular.sh --
echo   foo
-- shebang/.editorconfig --
root = true

[*]
indent_style = space
indent_size = 1

[[shell]]
indent_size = 2

[[bash]]
indent_size = 4

-- shebang/binsh --
#!/bin/sh

{
  indented
}
-- shebang/binbash --
#!/bin/bash

{
    indented
}
array=(elem)
-- multiconfig/space_redirects/.editorconfig --
[*]
space_redirects = true
-- multiconfig/space_redirects/f.sh --
foo > bar
foo &&
   bar
-- multiconfig/binary_next_line/.editorconfig --
[*]
binary_next_line = true
-- multiconfig/binary_next_line/f.sh --
foo >bar
foo \
   && bar
07070100000019000081A4000000000000000000000001686AE5C0000010D7000000000000000000000000000000000000003000000000sh-3.12.0/cmd/shfmt/testdata/script/flags.txtarexec shfmt -h
! stderr 'flag provided but not defined'
stderr 'usage: shfmt'
stderr 'Utilities' # definitely includes our help text
! stderr 'help requested' # don't duplicate usage output
! stderr '-test\.' # don't show the test binary's usage func

exec shfmt --help
stderr 'usage: shfmt'

exec shfmt -version
stdout 'devel|v3'
! stderr .

exec shfmt --version
stdout 'devel|v3'
! stderr .

! exec shfmt -ln=bash -p
stderr 'cannot coexist'

! exec shfmt --language-dialect=bash --posix
stderr 'cannot coexist'

! exec shfmt -ln=bad
stderr 'unknown shell language'

! exec shfmt --to-json file
stderr '--to-json can only be used with stdin'

! exec shfmt -filename=foo file
stderr '-filename can only be used with stdin'

# Check all the -ln variations.

stdin input-posix
! exec shfmt

stdin input-posix
exec shfmt -ln=posix
stdout 'let'

stdin input-posix
exec shfmt -p
stdout 'let'

stdin input-posix
! exec shfmt -ln=mksh

stdin input-posix
! exec shfmt -ln=bash

stdin input-mksh
exec shfmt -ln=mksh
stdout 'coprocess'

stdin input-mksh
! exec shfmt

# Ensure that the default "bash" language works with and without flags.
stdin input-bash
exec shfmt
stdout loop

# Ensure that -ln=auto works on stdin via filename.
stdin input-mksh
exec shfmt -filename=input.mksh
stdout 'coprocess'

# Ensure that -ln=auto works on stdin via shebang.
stdin input-mksh-shebang
exec shfmt
stdout 'coprocess'

# Ensure that -ln=auto works on stdin using the fallback.
stdin input-bash
exec shfmt
stdout 'loop'

# The default -ln=auto shouldn't require an extension or shebang,
# as long as we're explicitly formatting a file.
exec shfmt input-tiny
stdout foo

# -ln=auto should prefer a shebang if the extension is only ".sh".
stdin input-mksh-shebang
exec shfmt -filename=input.sh
stdout 'coprocess'

# An explicit -ln=auto should still work.
stdin input-mksh
exec shfmt -ln=auto -filename=input.mksh
stdout 'coprocess'

# Explicitly state language on parse errors
stdin input-bash-arrays
! exec shfmt -ln=auto -filename=input.sh
stderr 'parsed as posix via -ln=auto'

stdin input-bash-extglobs
! exec shfmt -filename=input.sh
stderr 'parsed as posix via -ln=auto'

stdin flags-input
exec shfmt -i 2
cmp stdout flags-output.indent-golden

stdin flags-input
exec shfmt --indent 2
cmp stdout flags-output.indent-golden

stdin flags-input
exec shfmt -bn
cmp stdout flags-output.binary-next-line-golden

stdin flags-input
exec shfmt --binary-next-line
cmp stdout flags-output.binary-next-line-golden

stdin flags-input
exec shfmt -ci
cmp stdout flags-output.case-indent-golden

stdin flags-input
exec shfmt --case-indent
cmp stdout flags-output.case-indent-golden

stdin flags-input
exec shfmt -sr
cmp stdout flags-output.space-redirects-golden

stdin flags-input
exec shfmt --space-redirects
cmp stdout flags-output.space-redirects-golden

stdin flags-input
exec shfmt -kp
cmp stdout flags-output.keep-padding-golden

stdin flags-input
exec shfmt --keep-padding
cmp stdout flags-output.keep-padding-golden

stdin flags-input
exec shfmt -fn
cmp stdout flags-output.func-next-line-golden

stdin flags-input
exec shfmt --func-next-line
cmp stdout flags-output.func-next-line-golden

-- input-posix --
let a+
-- input-bash --
for ((;;)); do loop; done
-- input-tiny --
foo
-- input-mksh --
coprocess |&
-- input-mksh-shebang --
#!/bin/mksh
coprocess |&
-- input-bash-extglobs --
#!/bin/sh
echo !(a)
-- input-bash-arrays --
#!/bin/sh
foo=(bar)

-- flags-input --
foo() {
	bar &&
		baz

	case $i in
	j)
		z
		;;
	esac

	space >redirs

	keep    padding
}
-- flags-output.indent-golden --
foo() {
  bar &&
    baz

  case $i in
  j)
    z
    ;;
  esac

  space >redirs

  keep padding
}
-- flags-output.binary-next-line-golden --
foo() {
	bar \
		&& baz

	case $i in
	j)
		z
		;;
	esac

	space >redirs

	keep padding
}
-- flags-output.case-indent-golden --
foo() {
	bar &&
		baz

	case $i in
		j)
			z
			;;
	esac

	space >redirs

	keep padding
}
-- flags-output.space-redirects-golden --
foo() {
	bar &&
		baz

	case $i in
	j)
		z
		;;
	esac

	space > redirs

	keep padding
}
-- flags-output.keep-padding-golden --
foo() {
	bar &&
		baz

	case $i in
	j)
		z
		;;
	esac

	space >redirs

	keep  padding
}
-- flags-output.func-next-line-golden --
foo()
{
	bar &&
		baz

	case $i in
	j)
		z
		;;
	esac

	space >redirs

	keep padding
}
0707010000001A000081A4000000000000000000000001686AE5C000000180000000000000000000000000000000000000003300000000sh-3.12.0/cmd/shfmt/testdata/script/simplify.txtarexec shfmt -s input.sh
cmp stdout input.sh.simplify-golden

exec shfmt --simplify input.sh
cmp stdout input.sh.simplify-golden

exec shfmt -mn input.sh
cmp stdout input.sh.minify-golden

exec shfmt --minify input.sh
cmp stdout input.sh.minify-golden

-- input.sh --
foo() {
	(( $bar ))
}
-- input.sh.simplify-golden --
foo() {
	((bar))
}
-- input.sh.minify-golden --
foo(){
((bar))
}
0707010000001B000081A4000000000000000000000001686AE5C000000CAD000000000000000000000000000000000000003100000000sh-3.12.0/cmd/shfmt/testdata/script/tojson.txtarstdin empty.sh
exec shfmt -tojson # old flag name
cmp stdout empty.sh.json
! stderr .

stdin simple.sh
exec shfmt --to-json
cmp stdout simple.sh.json

stdin arithmetic.sh
exec shfmt --to-json
cmp stdout arithmetic.sh.json

stdin comment.sh
exec shfmt --to-json
cmp stdout comment.sh.json

-- empty.sh --
-- empty.sh.json --
{
	"Type": "File"
}
-- simple.sh --
foo
-- simple.sh.json --
{
	"Type": "File",
	"Pos": {
		"Offset": 0,
		"Line": 1,
		"Col": 1
	},
	"End": {
		"Offset": 3,
		"Line": 1,
		"Col": 4
	},
	"Stmts": [
		{
			"Pos": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			},
			"End": {
				"Offset": 3,
				"Line": 1,
				"Col": 4
			},
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 0,
					"Line": 1,
					"Col": 1
				},
				"End": {
					"Offset": 3,
					"Line": 1,
					"Col": 4
				},
				"Args": [
					{
						"Pos": {
							"Offset": 0,
							"Line": 1,
							"Col": 1
						},
						"End": {
							"Offset": 3,
							"Line": 1,
							"Col": 4
						},
						"Parts": [
							{
								"Type": "Lit",
								"Pos": {
									"Offset": 0,
									"Line": 1,
									"Col": 1
								},
								"End": {
									"Offset": 3,
									"Line": 1,
									"Col": 4
								},
								"ValuePos": {
									"Offset": 0,
									"Line": 1,
									"Col": 1
								},
								"ValueEnd": {
									"Offset": 3,
									"Line": 1,
									"Col": 4
								},
								"Value": "foo"
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			}
		}
	]
}
-- arithmetic.sh --
((2))
-- arithmetic.sh.json --
{
	"Type": "File",
	"Pos": {
		"Offset": 0,
		"Line": 1,
		"Col": 1
	},
	"End": {
		"Offset": 5,
		"Line": 1,
		"Col": 6
	},
	"Stmts": [
		{
			"Pos": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			},
			"End": {
				"Offset": 5,
				"Line": 1,
				"Col": 6
			},
			"Cmd": {
				"Type": "ArithmCmd",
				"Pos": {
					"Offset": 0,
					"Line": 1,
					"Col": 1
				},
				"End": {
					"Offset": 5,
					"Line": 1,
					"Col": 6
				},
				"Left": {
					"Offset": 0,
					"Line": 1,
					"Col": 1
				},
				"Right": {
					"Offset": 3,
					"Line": 1,
					"Col": 4
				},
				"X": {
					"Type": "Word",
					"Pos": {
						"Offset": 2,
						"Line": 1,
						"Col": 3
					},
					"End": {
						"Offset": 3,
						"Line": 1,
						"Col": 4
					},
					"Parts": [
						{
							"Type": "Lit",
							"Pos": {
								"Offset": 2,
								"Line": 1,
								"Col": 3
							},
							"End": {
								"Offset": 3,
								"Line": 1,
								"Col": 4
							},
							"ValuePos": {
								"Offset": 2,
								"Line": 1,
								"Col": 3
							},
							"ValueEnd": {
								"Offset": 3,
								"Line": 1,
								"Col": 4
							},
							"Value": "2"
						}
					]
				}
			},
			"Position": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			}
		}
	]
}
-- comment.sh --
#
-- comment.sh.json --
{
	"Type": "File",
	"Pos": {
		"Offset": 0,
		"Line": 1,
		"Col": 1
	},
	"End": {
		"Offset": 1,
		"Line": 1,
		"Col": 2
	},
	"Last": [
		{
			"Pos": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			},
			"End": {
				"Offset": 1,
				"Line": 1,
				"Col": 2
			},
			"Hash": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			}
		}
	]
}
0707010000001C000081A4000000000000000000000001686AE5C000001351000000000000000000000000000000000000002F00000000sh-3.12.0/cmd/shfmt/testdata/script/walk.txtarmkdir symlink/subdir
# Remember that the symlink target is relative to the symlink directory.
[symlink] symlink symlink/subdir/symlink-file -> ../../modify/ext-shebang.sh
[symlink] symlink symlink/symlink-shebang.sh -> ../modify/ext-shebang.sh
[symlink] cp modify/ext.bash symlink/target-ext-bash # a copy that won't be formatted on its own
[symlink] symlink symlink/symlink-ext.bash -> target-ext-bash
[symlink] symlink symlink/symlink-dir -> subdir
[symlink] symlink symlink/symlink-none -> nonexistent

# Other non-regular files like FIFOs could cause issues like blocking reads
# when we look for shebangs. We use the "mkfifo" tool here, available on Linux.
[exec:mkfifo] exec mkfifo named-pipe

exec shfmt -f .
! stderr .
cmpenv stdout find.golden

exec shfmt --find .
! stderr .
cmpenv stdout find.golden

# try to format missing paths
! exec shfmt nonexistent
stderr -count=1 nonexistent

! exec shfmt nonexistent-1 nonexistent-2 nonexistent-3
stderr -count=1 nonexistent-1
stderr -count=1 nonexistent-2
stderr -count=1 nonexistent-3

# format an entire directory without -l or -w
! exec shfmt .
stdout 'foo'
stdout 'bin/env'
stderr -count=1 'parse-error\.sh'

# just -l, as a dry run
! exec shfmt --list .
stderr -count=1 'parse-error\.sh'
! stderr '^modify'
cmpenv stdout modify.golden

# format an entire directory with -l and -w
! exec shfmt -l -w .
stderr -count=1 'parse-error\.sh'
! stderr '^modify'
cmpenv stdout modify.golden

# parse-error again, but now as a lone file
! exec shfmt error/parse-error.sh
stderr -count=1 'parse-error\.sh'

# format files directly which we would ignore when walking directories
exec shfmt none/ext-shebang.other
stdout 'foo'
exec shfmt none/noext-noshebang
stdout 'foo'
[symlink] exec shfmt symlink/symlink-shebang.sh
[symlink] stdout 'foo'
[symlink] exec shfmt symlink/symlink-dir
[symlink] ! stdout . # note that filepath.WalkDir does not follow symlinks

# writing to non-regular files is forbidden as they'd be replaced by a regular file.
[symlink] exec shfmt -l symlink/symlink-ext.bash
[symlink] stdout 'symlink-ext.bash'
[symlink] ! exec shfmt -w symlink/symlink-ext.bash
[symlink] stderr 'refusing to atomically replace'
[symlink] exec shfmt -l symlink/symlink-ext.bash
[symlink] stdout 'symlink-ext.bash'

# -f on files should still check extension and shebang
exec shfmt -f modify/ext.sh modify/shebang-sh none/ext-shebang.other none/noext-noshebang
stdout -count=2 '^modify'
! stdout '^none'

# -ln shouldn't be overwritten by a filename
mkdir modify-ln
cp modify/ext.mksh modify-ln
! exec shfmt -ln=bash modify-ln
stderr '|& must be followed by a statement'
rm modify-ln

# -ln shouldn't be overwritten by a shebang
mkdir modify-ln
cp modify/shebang-mksh modify-ln
! exec shfmt -ln=bash modify-ln
stderr '|& must be followed by a statement'
rm modify-ln

-- find.golden --
error${/}parse-error.sh
modify${/}dir${/}ext.sh
modify${/}ext-shebang.sh
modify${/}ext.bash
modify${/}ext.bats
modify${/}ext.mksh
modify${/}ext.sh
modify${/}shebang-args
modify${/}shebang-bash
modify${/}shebang-bash.sh
modify${/}shebang-env-bash
modify${/}shebang-env-bats
modify${/}shebang-env-sh
modify${/}shebang-mksh
modify${/}shebang-sh
modify${/}shebang-space
modify${/}shebang-tabs
modify${/}shebang-usr-sh
-- modify.golden --
modify${/}dir${/}ext.sh
modify${/}ext-shebang.sh
modify${/}ext.bash
modify${/}ext.bats
modify${/}ext.mksh
modify${/}ext.sh
modify${/}shebang-args
modify${/}shebang-bash
modify${/}shebang-bash.sh
modify${/}shebang-env-bash
modify${/}shebang-env-bats
modify${/}shebang-env-sh
modify${/}shebang-mksh
modify${/}shebang-sh
modify${/}shebang-space
modify${/}shebang-tabs
modify${/}shebang-usr-sh
-- modify/shebang-sh --
#!/bin/sh
 foo
-- modify/shebang-bash --
#!/bin/bash
 foo=(bar)
-- modify/shebang-bash.sh --
#!/bin/bash
 foo=(bar)
-- modify/shebang-usr-sh --
#!/usr/bin/sh
 foo
-- modify/shebang-env-bash --
#!/usr/bin/env bash
 foo=(bar)
-- modify/shebang-env-sh --
#!/bin/env sh
 foo
-- modify/shebang-mksh --
#!/bin/mksh
 foo |&
-- modify/shebang-env-bats --
#!/usr/bin/env bats
 @test "foo" { bar; }
-- modify/shebang-space --
#! /bin/sh
 foo
-- modify/shebang-tabs --
#!	/bin/env	sh
 foo
-- modify/shebang-args --
#!/bin/bash -e -x
 foo
-- modify/ext.sh --
 foo
-- modify/ext.bash --
 foo=(bar)
-- modify/ext.mksh --
 foo |&
-- modify/ext.bats --
 @test "foo" { bar; }
-- modify/ext-shebang.sh --
#!/bin/sh
 foo
-- modify/dir/ext.sh --
foo

-- none/.hidden --
foo long enough
-- none/.hidden-shebang --
#!/bin/sh
 foo
-- none/..hidden-shebang --
#!/bin/sh
 foo
-- none/noext-empty --
foo
-- none/noext-noshebang --
foo long enough
-- none/shebang-nonewline --
#!/bin/shfoo
-- none/ext.other --
foo
-- none/empty --
-- none/ext-shebang.other --
#!/bin/sh
 foo
-- none/shebang-nospace --
#!/bin/envsh
 foo

-- skip/ext.zsh
foo
-- skip/shebang-zsh
#!/bin/zsh
foo
-- skip/.git/ext.sh --
foo
-- skip/.svn/ext.sh --
foo
-- skip/.hg/ext.sh --
foo

-- error/parse-error.sh --
foo(
0707010000001D000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001100000000sh-3.12.0/expand0707010000001E000081A4000000000000000000000001686AE5C000001246000000000000000000000000000000000000001A00000000sh-3.12.0/expand/arith.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

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 expr := expr.(type) {
	case *syntax.Word:
		str, err := Literal(cfg, expr)
		if err != nil {
			return 0, err
		}
		// recursively fetch vars
		i := 0
		for syntax.ValidName(str) {
			val := cfg.envGet(str)
			if val == "" {
				break
			}
			if i++; i >= maxNameRefDepth {
				break
			}
			str = val
		}
		// default to 0
		return int(atoi(str)), nil
	case *syntax.ParenArithm:
		return Arithm(cfg, expr.X)
	case *syntax.UnaryArithm:
		switch expr.Op {
		case syntax.Inc, syntax.Dec:
			name := expr.X.(*syntax.Word).Lit()
			old := atoi(cfg.envGet(name))
			val := old
			if expr.Op == syntax.Inc {
				val++
			} else {
				val--
			}
			if err := cfg.envSet(name, strconv.FormatInt(val, 10)); err != nil {
				return 0, err
			}
			if expr.Post {
				return int(old), nil
			}
			return int(val), nil
		}
		val, err := Arithm(cfg, expr.X)
		if err != nil {
			return 0, err
		}
		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
			return -val, nil
		}
	case *syntax.BinaryArithm:
		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(expr)
		case syntax.TernQuest: // TernColon can't happen here
			cond, err := Arithm(cfg, expr.X)
			if err != nil {
				return 0, err
			}
			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, expr.X)
		if err != nil {
			return 0, err
		}
		right, err := Arithm(cfg, expr.Y)
		if err != nil {
			return 0, err
		}
		return binArit(expr.Op, left, right)
	default:
		panic(fmt.Sprintf("unexpected arithm expr: %T", expr))
	}
}

func oneIf(b bool) int {
	if b {
		return 1
	}
	return 0
}

// 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)
	if err != nil {
		return 0, err
	}
	arg := int64(arg_)
	switch b.Op {
	case syntax.Assgn:
		val = arg
	case syntax.AddAssgn:
		val += arg
	case syntax.SubAssgn:
		val -= arg
	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
	case syntax.OrAssgn:
		val |= arg
	case syntax.XorAssgn:
		val ^= arg
	case syntax.ShlAssgn:
		val <<= uint(arg)
	case syntax.ShrAssgn:
		val >>= uint(arg)
	}
	if err := cfg.envSet(name, strconv.FormatInt(val, 10)); err != nil {
		return 0, err
	}
	return int(val), nil
}

func intPow(a, b int) int {
	p := 1
	for b > 0 {
		if b&1 != 0 {
			p *= a
		}
		b >>= 1
		a *= a
	}
	return p
}

func binArit(op syntax.BinAritOperator, x, y int) (int, error) {
	switch op {
	case syntax.Add:
		return x + y, nil
	case syntax.Sub:
		return x - y, nil
	case syntax.Mul:
		return x * y, nil
	case syntax.Quo:
		if y == 0 {
			return 0, fmt.Errorf("division by zero")
		}
		return x / y, nil
	case syntax.Rem:
		if y == 0 {
			return 0, fmt.Errorf("division by zero")
		}
		return x % y, nil
	case syntax.Pow:
		return intPow(x, y), nil
	case syntax.Eql:
		return oneIf(x == y), nil
	case syntax.Gtr:
		return oneIf(x > y), nil
	case syntax.Lss:
		return oneIf(x < y), nil
	case syntax.Neq:
		return oneIf(x != y), nil
	case syntax.Leq:
		return oneIf(x <= y), nil
	case syntax.Geq:
		return oneIf(x >= y), nil
	case syntax.And:
		return x & y, nil
	case syntax.Or:
		return x | y, nil
	case syntax.Xor:
		return x ^ y, nil
	case syntax.Shr:
		return x >> uint(y), nil
	case syntax.Shl:
		return x << uint(y), nil
	case syntax.AndArit:
		return oneIf(x != 0 && y != 0), nil
	case syntax.OrArit:
		return oneIf(x != 0 || y != 0), nil
	default: // syntax.Comma
		// x is executed but its result discarded
		return y, nil
	}
}
0707010000001F000081A4000000000000000000000001686AE5C0000008CB000000000000000000000000000000000000001B00000000sh-3.12.0/expand/braces.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

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
// "foo{bar,baz}" will return two literal words, "foobar" and "foobaz".
//
// Note that the resulting words may share word parts.
func Braces(word *syntax.Word) []*syntax.Word {
	var all []*syntax.Word
	var left []syntax.WordPart
	for i, wp := range word.Parts {
		br, ok := wp.(*syntax.BraceExp)
		if !ok {
			left = append(left, wp)
			continue
		}
		if br.Sequence {
			chars := false

			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])
				to = int(br.Elems[1].Lit()[0])
			}
			upward := from <= to
			incr := 1
			if !upward {
				incr = -1
			}
			if len(br.Elems) > 2 {
				n, _ := strconv.Atoi(br.Elems[2].Lit())
				if n != 0 && n > 0 == upward {
					incr = n
				}
			}
			n := from
			for {
				if upward && n > to {
					break
				}
				if !upward && n < to {
					break
				}
				next := *word
				next.Parts = next.Parts[i+1:]
				lit := &syntax.Lit{}
				if chars {
					lit.Value = string(rune(n))
				} else {
					lit.Value = strings.Repeat("0", zeros) + strconv.Itoa(n)
				}
				next.Parts = append([]syntax.WordPart{lit}, next.Parts...)
				exp := Braces(&next)
				for _, w := range exp {
					w.Parts = append(left, w.Parts...)
				}
				all = append(all, exp...)
				n += incr
			}
			return all
		}
		for _, elem := range br.Elems {
			next := *word
			next.Parts = next.Parts[i+1:]
			next.Parts = append(elem.Parts, next.Parts...)
			exp := Braces(&next)
			for _, w := range exp {
				w.Parts = append(left, w.Parts...)
			}
			all = append(all, exp...)
		}
		return all
	}
	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
}
07070100000020000081A4000000000000000000000001686AE5C000000E98000000000000000000000000000000000000002000000000sh-3.12.0/expand/braces_test.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"bytes"
	"testing"

	"mvdan.cc/sh/v3/syntax"
)

func lit(s string) *syntax.Lit                { return &syntax.Lit{Value: s} }
func word(ps ...syntax.WordPart) *syntax.Word { return &syntax.Word{Parts: ps} }
func litWord(s string) *syntax.Word           { return word(lit(s)) }
func litWords(strs ...string) []*syntax.Word {
	l := make([]*syntax.Word, 0, len(strs))
	for _, s := range strs {
		l = append(l, litWord(s))
	}
	return l
}

var braceTests = []struct {
	in   *syntax.Word
	want []*syntax.Word
}{
	{
		litWord("a{b"),
		litWords("a{b"),
	},
	{
		litWord("a}b"),
		litWords("a}b"),
	},
	{
		litWord("{a,b{c,d}"),
		litWords("{a,bc", "{a,bd"),
	},
	{
		litWord("{a{b"),
		litWords("{a{b"),
	},
	{
		litWord("a{}"),
		litWords("a{}"),
	},
	{
		litWord("a{b}"),
		litWords("a{b}"),
	},
	{
		litWord("a{b,c}"),
		litWords("ab", "ac"),
	},
	{
		litWord("a{à,世界}"),
		litWords("aà", "a世界"),
	},
	{
		litWord("a{b,c}d{e,f}g"),
		litWords("abdeg", "abdfg", "acdeg", "acdfg"),
	},
	{
		litWord("a{b{x,y},c}d"),
		litWords("abxd", "abyd", "acd"),
	},
	{
		litWord("a{1,2,3,4,5}"),
		litWords("a1", "a2", "a3", "a4", "a5"),
	},
	{
		litWord("a{1.."),
		litWords("a{1.."),
	},
	{
		litWord("a{1..4"),
		litWords("a{1..4"),
	},
	{
		litWord("a{1.4}"),
		litWords("a{1.4}"),
	},
	{
		litWord("{a,b}{1..4"),
		litWords("a{1..4", "b{1..4"),
	},
	{
		litWord("a{1..4}"),
		litWords("a1", "a2", "a3", "a4"),
	},
	{
		litWord("a{1..2}b{4..5}c"),
		litWords("a1b4c", "a1b5c", "a2b4c", "a2b5c"),
	},
	{
		litWord("a{1..f}"),
		litWords("a{1..f}"),
	},
	{
		litWord("a{c..f}"),
		litWords("ac", "ad", "ae", "af"),
	},
	{
		litWord("a{H..K}"),
		litWords("aH", "aI", "aJ", "aK"),
	},
	{
		litWord("a{-..f}"),
		litWords("a{-..f}"),
	},
	{
		litWord("a{3..-}"),
		litWords("a{3..-}"),
	},
	{
		litWord("a{1..10..3}"),
		litWords("a1", "a4", "a7", "a10"),
	},
	{
		litWord("a{1..4..0}"),
		litWords("a1", "a2", "a3", "a4"),
	},
	{
		litWord("a{4..1}"),
		litWords("a4", "a3", "a2", "a1"),
	},
	{
		litWord("a{4..1..-2}"),
		litWords("a4", "a2"),
	},
	{
		litWord("a{4..1..1}"),
		litWords("a4", "a3", "a2", "a1"),
	},
	{
		litWord("{1..005}"),
		litWords("001", "002", "003", "004", "005"),
	},
	{
		litWord("{0001..05..2}"),
		litWords("0001", "0003", "0005"),
	},
	{
		litWord("{0..1}"),
		litWords("0", "1"),
	},
	{
		litWord("a{d..k..3}"),
		litWords("ad", "ag", "aj"),
	},
	{
		litWord("a{d..k..n}"),
		litWords("a{d..k..n}"),
	},
	{
		litWord("a{k..d..-2}"),
		litWords("ak", "ai", "ag", "ae"),
	},
	{
		litWord("{1..1}"),
		litWords("1"),
	},
}

func TestBraces(t *testing.T) {
	t.Parallel()
	for _, tc := range braceTests {
		t.Run("", func(t *testing.T) {
			inStr := printWords(tc.in)
			wantStr := printWords(tc.want...)
			wantBraceExpParts(t, tc.in, false)

			inBraces := *tc.in
			syntax.SplitBraces(&inBraces)
			wantBraceExpParts(t, &inBraces, inStr != wantStr)

			got := Braces(&inBraces)
			gotStr := printWords(got...)
			if gotStr != wantStr {
				t.Fatalf("mismatch in %q\nwant:\n%s\ngot: %s",
					inStr, wantStr, gotStr)
			}
		})
	}
}

func wantBraceExpParts(t *testing.T, word *syntax.Word, want bool) {
	t.Helper()
	anyBrace := false
	for _, part := range word.Parts {
		if _, anyBrace = part.(*syntax.BraceExp); anyBrace {
			break
		}
	}
	if anyBrace && !want {
		t.Fatalf("didn't want any BraceExp node, but found one")
	} else if !anyBrace && want {
		t.Fatalf("wanted a BraceExp node, but found none")
	}
}

func printWords(words ...*syntax.Word) string {
	p := syntax.NewPrinter()
	var buf bytes.Buffer
	call := &syntax.CallExpr{Args: words}
	p.Print(&buf, call)
	return buf.String()
}
07070100000021000081A4000000000000000000000001686AE5C0000000B4000000000000000000000000000000000000001800000000sh-3.12.0/expand/doc.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package expand contains code to perform various shell expansions.
package expand
07070100000022000081A4000000000000000000000001686AE5C0000020E2000000000000000000000000000000000000001C00000000sh-3.12.0/expand/environ.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"cmp"
	"runtime"
	"slices"
	"strings"
)

// Environ is the base interface for a shell's environment, allowing it to fetch
// variables by name and to iterate over all the currently set variables.
type Environ interface {
	// Get retrieves a variable by its name. To check if the variable is
	// 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.
	//
	// The names used in the calls aren't required to be unique or sorted.
	// If a variable name appears twice, the latest occurrence takes
	// priority.
	//
	// Each is required to forward exported variables when executing
	// programs.
	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 {
	Environ
	// Set sets a variable by name. If !vr.IsSet(), the variable is being
	// unset; otherwise, the variable is being replaced.
	//
	// The given variable can have the kind [KeepValue] to replace an existing
	// variable's attributes without changing its value at all.
	// This is helpful to implement `readonly foo=bar; export foo`,
	// as the second declaration needs to clearly signal that the value is not modified.
	//
	// An error may be returned if the operation is invalid, such as if the
	// name is empty or if we're trying to overwrite a read-only variable.
	Set(name string, vr Variable) error
}

//go:generate stringer -type=ValueKind

// ValueKind describes which kind of value the variable holds.
// While most unset variables will have an [Unknown] kind, an unset variable may
// have a kind associated too, such as via `declare -a foo` resulting in [Indexed].
type ValueKind uint8

const (
	// 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.
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.
	List []string          // Used when Kind is Indexed.
	Map  map[string]string // Used when Kind is Associative.
}

// 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.Set
}

// Declared reports whether the variable has been declared.
// Declared variables may not be set; `export foo` is exported but not set to a value,
// and `declare -a foo` is an indexed array but not set to a value.
func (v Variable) Declared() bool {
	return v.Set || v.Local || v.Exported || v.ReadOnly || v.Kind != Unknown
}

// String returns the variable's value as a string. In general, this only makes
// sense if the variable has a string value or no value at all.
func (v Variable) String() string {
	switch v.Kind {
	case String:
		return v.Str
	case Indexed:
		if len(v.List) > 0 {
			return v.List[0]
		}
	case Associative:
		// nothing to do
	}
	return ""
}

// maxNameRefDepth defines the maximum number of times to follow references when
// resolving a variable. Otherwise, simple name reference loops could crash a
// program quite easily.
const maxNameRefDepth = 100

// Resolve follows a number of nameref variables, returning the last reference
// name that was followed and the variable that it points to.
func (v Variable) Resolve(env Environ) (string, Variable) {
	name := ""
	for range maxNameRefDepth {
		if v.Kind != NameRef {
			return name, v
		}
		name = v.Str // keep name for the next iteration
		v = env.Get(name)
	}
	return name, Variable{}
}

// FuncEnviron wraps a function mapping variable names to their string values,
// 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.
func FuncEnviron(fn func(string) string) Environ {
	return funcEnviron(fn)
}

type funcEnviron func(string) string

func (f funcEnviron) Get(name string) Variable {
	value := f(name)
	if value == "" {
		return Variable{}
	}
	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
// "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...)
}

// listEnvironWithUpper implements [ListEnviron], but letting the tests specify
// whether to uppercase all names or not.
func listEnvironWithUpper(upper bool, pairs ...string) Environ {
	list := slices.Clone(pairs)
	if upper {
		// Uppercase before sorting, so that we can remove duplicates
		// without the need for linear search nor a map.
		for i, s := range list {
			if name, val, ok := strings.Cut(s, "="); ok {
				list[i] = strings.ToUpper(name) + "=" + val
			}
		}
	}

	slices.SortStableFunc(list, func(a, b string) int {
		isep := strings.IndexByte(a, '=')
		jsep := strings.IndexByte(b, '=')
		if isep < 0 {
			isep = 0
		} else {
			isep += 1
		}
		if jsep < 0 {
			jsep = 0
		} else {
			jsep += 1
		}
		return strings.Compare(a[:isep], b[:jsep])
	})

	last := ""
	for i := 0; i < len(list); {
		name, _, ok := strings.Cut(list[i], "=")
		if name == "" || !ok {
			// invalid element; remove it
			list = slices.Delete(list, i, i+1)
			continue
		}
		if last == name {
			// duplicate; the last one wins
			list = slices.Delete(list, i-1, i)
			continue
		}
		last = name
		i++
	}
	return listEnviron(list)
}

// listEnviron is a sorted list of "name=value" strings.
type listEnviron []string

func (l listEnviron) Get(name string) Variable {
	eqpos := len(name)
	endpos := len(name) + 1
	i, ok := slices.BinarySearchFunc(l, name, func(l, name string) int {
		if len(l) < endpos {
			// Too short; see if we are before or after the name.
			return strings.Compare(l, name)
		}
		// Compare the name prefix, then the equal character.
		c := strings.Compare(l[:eqpos], name)
		eq := l[eqpos]
		if c == 0 {
			return cmp.Compare(eq, '=')
		}
		return c
	})
	if ok {
		return Variable{Set: true, Exported: true, Kind: String, Str: l[i][endpos:]}
	}
	return Variable{}
}

func (l listEnviron) Each(fn func(name string, vr Variable) bool) {
	for _, pair := range l {
		name, value, ok := strings.Cut(pair, "=")
		if !ok {
			// should never happen; see listEnvironWithUpper
			panic("expand.listEnviron: did not expect malformed name-value pair: " + pair)
		}
		if !fn(name, Variable{Set: true, Exported: true, Kind: String, Str: value}) {
			return
		}
	}
}
07070100000023000081A4000000000000000000000001686AE5C000000712000000000000000000000000000000000000002100000000sh-3.12.0/expand/environ_test.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"reflect"
	"testing"
)

func TestListEnviron(t *testing.T) {
	tests := []struct {
		name  string
		upper bool
		pairs []string
		want  []string
	}{
		{
			name:  "Empty",
			pairs: nil,
			want:  nil,
		},
		{
			name:  "Simple",
			pairs: []string{"A=b", "c="},
			want:  []string{"A=b", "c="},
		},
		{
			name:  "MissingEqual",
			pairs: []string{"A=b", "invalid", "c="},
			want:  []string{"A=b", "c="},
		},
		{
			name:  "DuplicateNames",
			pairs: []string{"A=x", "A=b", "c=", "c=y"},
			want:  []string{"A=b", "c=y"},
		},
		{
			name:  "NoName",
			pairs: []string{"=b", "=c"},
			want:  []string{},
		},
		{
			name:  "EmptyElements",
			pairs: []string{"A=b", "", "", "c="},
			want:  []string{"A=b", "c="},
		},
		{
			name:  "MixedCaseNoUpper",
			pairs: []string{"A=b1", "Path=foo", "a=b2"},
			want:  []string{"A=b1", "Path=foo", "a=b2"},
		},
		{
			name:  "MixedCaseUpper",
			upper: true,
			pairs: []string{"A=b1", "Path=foo", "a=b2"},
			want:  []string{"A=b2", "PATH=foo"},
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			gotEnv := listEnvironWithUpper(tc.upper, tc.pairs...)
			got := []string(gotEnv.(listEnviron))
			if !reflect.DeepEqual(got, tc.want) {
				t.Fatalf("ListEnviron(%t, %q) wanted %#v, got %#v",
					tc.upper, tc.pairs, tc.want, got)
			}
		})
	}
}

func TestGetWithSameSubPrefix(t *testing.T) {
	gotEnv := ListEnviron("GREETING=text1", "GREETING2=text2")
	got := gotEnv.Get("GREETING2").String()
	if got != "text2" {
		t.Fatalf("ListEnviron.Get(GREETING2) wanted text2, got %q", got)
	}
	got = gotEnv.Get("GREETING").String()
	if got != "text1" {
		t.Fatalf("ListEnviron.Get(GREETING) wanted text1, got %q", got)
	}
}
07070100000024000081A4000000000000000000000001686AE5C000006FD7000000000000000000000000000000000000001B00000000sh-3.12.0/expand/expand.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"cmp"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"iter"
	"maps"
	"os"
	"os/user"
	"path/filepath"
	"regexp"
	"runtime"
	"slices"
	"strconv"
	"strings"

	"mvdan.cc/sh/v3/pattern"
	"mvdan.cc/sh/v3/syntax"
)

// A Config specifies details about how shell expansion should be performed. The
// zero value is a valid configuration.
type Config struct {
	// Env is used to get and set environment variables when performing
	// shell expansions. Some special parameters are also expanded via this
	// interface, such as:
	//
	//   * "#", "@", "*", "0"-"9" for the shell's parameters
	//   * "?", "$", "PPID" for the shell's status and process
	//   * "HOME foo" to retrieve user foo's home directory (if unset,
	//     os/user.Lookup will be used)
	//
	// If nil, there are no environment variables set. Use
	// ListEnviron(os.Environ()...) to use the system's environment
	// variables.
	Env Environ

	// CmdSubst expands a command substitution node, writing its standard
	// 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): replace ReadDir with ReadDir2.

	// ReadDir is the older form of [ReadDir2], before io/fs.
	//
	// Deprecated: use ReadDir2 instead.
	ReadDir func(string) ([]fs.FileInfo, error)

	// ReadDir2 is used for file path globbing.
	// If nil, and [ReadDir] is nil as well, globbing is disabled.
	// Use [os.ReadDir] to use the filesystem directly.
	ReadDir2 func(string) ([]fs.DirEntry, error)

	// GlobStar corresponds to the shell option that allows globbing with
	// "**".
	GlobStar bool

	// NoCaseGlob corresponds to the shell option that causes case-insensitive
	// pattern matching in pathname expansion.
	NoCaseGlob bool

	// NullGlob corresponds to the shell option that allows globbing
	// patterns which match nothing to result in zero fields.
	NullGlob bool

	// NoUnset corresponds to the shell option that treats unset variables
	// as errors.
	NoUnset bool

	bufferAlloc strings.Builder
	fieldAlloc  [4]fieldPart
	fieldsAlloc [4][]fieldPart

	ifs string
	// A pointer to a parameter expansion node, if we're inside one.
	// Necessary for ${LINENO}.
	curParam *syntax.ParamExp
}

// UnexpectedCommandError is returned if a command substitution is encountered
// when [Config.CmdSubst] is nil.
type UnexpectedCommandError struct {
	Node *syntax.CmdSubst
}

func (u UnexpectedCommandError) Error() string {
	return fmt.Sprintf("unexpected command substitution at %s", u.Node.Pos())
}

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 {
	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
}

func (cfg *Config) ifsRune(r rune) bool {
	for _, r2 := range cfg.ifs {
		if r == r2 {
			return true
		}
	}
	return false
}

func (cfg *Config) ifsJoin(strs []string) string {
	sep := ""
	if cfg.ifs != "" {
		sep = cfg.ifs[:1]
	}
	return strings.Join(strs, sep)
}

func (cfg *Config) strBuilder() *strings.Builder {
	b := &cfg.bufferAlloc
	b.Reset()
	return b
}

func (cfg *Config) envGet(name string) string {
	return cfg.Env.Get(name).String()
}

func (cfg *Config) envSet(name, value string) error {
	wenv, ok := cfg.Env.(WriteEnviron)
	if !ok {
		return fmt.Errorf("environment is read-only")
	}
	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
// is a single string. This is the behavior when a word is used as the value in
// a shell variable assignment, for example.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Literal(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
	}
	return cfg.fieldJoin(field), nil
}

// 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
// empty config.
func Document(cfg *Config, word *syntax.Word) (string, error) {
	if word == nil {
		return "", nil
	}
	cfg = prepareConfig(cfg)
	field, err := cfg.wordField(word.Parts, quoteHeredoc)
	if err != nil {
		return "", err
	}
	return cfg.fieldJoin(field), nil
}

// 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
// [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
	}
	sb := cfg.strBuilder()
	for _, part := range field {
		if part.quote > quoteNone {
			sb.WriteString(pattern.QuoteMeta(part.val, 0))
		} else {
			sb.WriteString(part.val)
		}
	}
	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.
//
// 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)
	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++ {
				c := format[i+j]
				if (c >= '0' && c <= '9') ||
					(hex && c >= 'a' && c <= 'f') ||
					(hex && c >= 'A' && c <= 'F') {
					// valid octal or hex char
				} else {
					break
				}
			}
			digits := format[i : i+j]
			i += j - 1 // -1 since the outer loop does i++
			return digits
		}
		c := format[i]
		switch {
		case c == '\\': // escaped
			i++
			switch c = format[i]; c {
			case 'a': // bell
				sb.WriteByte('\a')
			case 'b': // backspace
				sb.WriteByte('\b')
			case 'e', 'E': // escape
				sb.WriteByte('\x1b')
			case 'f': // form feed
				sb.WriteByte('\f')
			case 'n': // new line
				sb.WriteByte('\n')
			case 'r': // carriage return
				sb.WriteByte('\r')
			case 't': // horizontal tab
				sb.WriteByte('\t')
			case 'v': // vertical tab
				sb.WriteByte('\v')
			case '\\', '\'', '"', '?': // just the character
				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)
				sb.WriteByte(byte(n))
			case 'x', 'u', 'U':
				i++
				max := 2
				switch c {
				case 'u':
					max = 4
				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
						sb.WriteByte(byte(n))
					} else {
						sb.WriteRune(rune(n))
					}
					break
				}
				fallthrough
			default: // no escape sequence
				sb.WriteByte('\\')
				sb.WriteByte(c)
			}
		case len(fmts) > 0:
			switch c {
			case '%':
				sb.WriteByte('%')
				fmts = nil
			case 'c':
				var b byte
				if len(args) > 0 {
					arg := ""
					arg, args = args[0], args[1:]
					if len(arg) > 0 {
						b = arg[0]
					}
				}
				sb.WriteByte(b)
				fmts = nil
			case '+', '-', ' ':
				if len(fmts) > 1 {
					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', 'b', 'd', 'i', 'u', 'o', 'x':
				arg := ""
				if len(args) > 0 {
					arg, args = args[0], args[1:]
				}
				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)
					} else {
						farg = uint(n)
					}
					if c == 'i' || c == 'u' {
						c = 'd'
					}
				} else {
					farg = arg
				}
				if farg != nil {
					fmts = append(fmts, c)
					fmt.Fprintf(sb, string(fmts), farg)
				}
				fmts = nil
			default:
				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:
			sb.WriteByte(c)
		}
	}
	if len(fmts) > 0 {
		return 0, fmt.Errorf("missing format char")
	}
	return initialArgs - len(args), nil
}

func (cfg *Config) fieldJoin(parts []fieldPart) string {
	switch len(parts) {
	case 0:
		return ""
	case 1: // short-cut without a string copy
		return parts[0].val
	}
	sb := cfg.strBuilder()
	for _, part := range parts {
		sb.WriteString(part.val)
	}
	return sb.String()
}

func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob bool) {
	sb := cfg.strBuilder()
	for _, part := range parts {
		if part.quote > quoteNone {
			sb.WriteString(pattern.QuoteMeta(part.val, 0))
			continue
		}
		sb.WriteString(part.val)
		if pattern.HasMeta(part.val, 0) {
			glob = true
		}
	}
	if glob { // only copy the string if it will be used
		escaped = sb.String()
	}
	return escaped, glob
}

// Fields is a pre-iterators API which now wraps [FieldsSeq].
func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
	var fields []string
	for s, err := range FieldsSeq(cfg, words...) {
		if err != nil {
			return nil, err
		}
		fields = append(fields, s)
	}
	return fields, nil
}

// FieldsSeq expands a number of words as if they were arguments in a shell
// command. This includes brace expansion, tilde expansion, parameter expansion,
// command substitution, arithmetic expansion, and quote removal.
func FieldsSeq(cfg *Config, words ...*syntax.Word) iter.Seq2[string, error] {
	cfg = prepareConfig(cfg)
	dir := cfg.envGet("PWD")
	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 _, 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 {
							// 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
					}
				}
			}
		}
	}
}

type fieldPart struct {
	val   string
	quote quoteLevel
}

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 wp := wp.(type) {
		case *syntax.Lit:
			s := wp.Value
			if i == 0 && ql == quoteNone {
				if prefix, rest := cfg.expandUser(s, len(wps) > 1); prefix != "" {
					// TODO: return two separate fieldParts,
					// like in wordFields?
					s = prefix + rest
				}
			}
			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 '"':
							if ql != quoteDouble {
								break
							}
							fallthrough
						case '\\', '$', '`': // special chars
							i++
							b = s[i] // write the special char, skipping the backslash
						}
					}
					sb.WriteByte(b)
				}
				s = sb.String()
			}
			s, _, _ = strings.Cut(s, "\x00")
			field = append(field, fieldPart{val: s})
		case *syntax.SglQuoted:
			fp := fieldPart{quote: quoteSingle, val: wp.Value}
			if wp.Dollar {
				fp.val, _, _ = Format(cfg, fp.val, nil)
			}
			field = append(field, fp)
		case *syntax.DblQuoted:
			wfield, err := cfg.wordField(wp.Parts, quoteDouble)
			if err != nil {
				return nil, err
			}
			for _, part := range wfield {
				part.quote = quoteDouble
				field = append(field, part)
			}
		case *syntax.ParamExp:
			val, err := cfg.paramExp(wp)
			if err != nil {
				return nil, err
			}
			field = append(field, fieldPart{val: val})
		case *syntax.CmdSubst:
			val, err := cfg.cmdSubst(wp)
			if err != nil {
				return nil, err
			}
			field = append(field, fieldPart{val: val})
		case *syntax.ArithmExp:
			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(wp)
			if err != nil {
				return nil, err
			}
			field = append(field, fieldPart{val: path})
		case *syntax.ExtGlob:
			return nil, fmt.Errorf("extended globbing is not supported")
		default:
			panic(fmt.Sprintf("unhandled word part: %T", wp))
		}
	}
	return field, nil
}

func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) {
	if cfg.CmdSubst == nil {
		return "", UnexpectedCommandError{Node: cs}
	}
	sb := cfg.strBuilder()
	if err := cfg.CmdSubst(sb, cs); err != nil {
		return "", err
	}
	out := sb.String()
	out = strings.ReplaceAll(out, "\x00", "")
	return strings.TrimRight(out, "\n"), nil
}

func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
	fields := cfg.fieldsAlloc[:0]
	curField := cfg.fieldAlloc[:0]
	allowEmpty := false
	flush := func() {
		if len(curField) == 0 {
			return
		}
		fields = append(fields, curField)
		curField = nil
	}
	splitAdd := func(val string) {
		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
				}
			}
		}
		if fieldStart >= 0 { // ending a field without IFS
			curField = append(curField, fieldPart{val: val[fieldStart:]})
		}
	}
	for i, wp := range wps {
		switch wp := wp.(type) {
		case *syntax.Lit:
			s := wp.Value
			if i == 0 {
				prefix, rest := cfg.expandUser(s, len(wps) > 1)
				curField = append(curField, fieldPart{
					quote: quoteSingle,
					val:   prefix,
				})
				s = rest
			}
			if strings.Contains(s, "\\") {
				sb := cfg.strBuilder()
				for i := 0; i < len(s); i++ {
					b := s[i]
					if b == '\\' {
						if i++; i >= len(s) {
							break
						}
						b = s[i]
					}
					sb.WriteByte(b)
				}
				s = sb.String()
			}
			curField = append(curField, fieldPart{val: s})
		case *syntax.SglQuoted:
			allowEmpty = true
			fp := fieldPart{quote: quoteSingle, val: wp.Value}
			if wp.Dollar {
				fp.val, _, _ = Format(cfg, fp.val, nil)
			}
			curField = append(curField, fp)
		case *syntax.DblQuoted:
			if len(wp.Parts) == 1 {
				pe, _ := wp.Parts[0].(*syntax.ParamExp)
				if elems := cfg.quotedElemFields(pe); elems != nil {
					for i, elem := range elems {
						if i > 0 {
							flush()
						}
						curField = append(curField, fieldPart{
							quote: quoteDouble,
							val:   elem,
						})
					}
					continue
				}
			}
			allowEmpty = true
			wfield, err := cfg.wordField(wp.Parts, quoteDouble)
			if err != nil {
				return nil, err
			}
			for _, part := range wfield {
				part.quote = quoteDouble
				curField = append(curField, part)
			}
		case *syntax.ParamExp:
			val, err := cfg.paramExp(wp)
			if err != nil {
				return nil, err
			}
			splitAdd(val)
		case *syntax.CmdSubst:
			val, err := cfg.cmdSubst(wp)
			if err != nil {
				return nil, err
			}
			splitAdd(val)
		case *syntax.ArithmExp:
			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(wp)
			if err != nil {
				return nil, err
			}
			splitAdd(path)
		case *syntax.ExtGlob:
			return nil, fmt.Errorf("extended globbing is not supported")
		default:
			panic(fmt.Sprintf("unhandled word part: %T", wp))
		}
	}
	flush()
	if allowEmpty && len(fields) == 0 {
		fields = append(fields, curField)
	}
	return fields, nil
}

// quotedElemFields returns the list of elements resulting from a quoted
// parameter expansion that should be treated especially, like "${foo[@]}".
func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string {
	if pe == nil || pe.Length || pe.Width {
		return nil
	}
	name := pe.Param.Value
	if pe.Excl {
		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
	}
	switch name {
	case "*": // "${*}"
		return []string{cfg.ifsJoin(cfg.Env.Get(name).List)}
	case "@": // "${@}"
		return cfg.Env.Get(name).List
	}
	switch nodeLit(pe.Index) {
	case "@": // "${name[@]}"
		switch vr := cfg.Env.Get(name); vr.Kind {
		case Indexed:
			return vr.List
		case Associative:
			return slices.Collect(maps.Values(vr.Map))
		}
	case "*": // "${name[*]}"
		if vr := cfg.Env.Get(name); vr.Kind == Indexed {
			return []string{cfg.ifsJoin(vr.List)}
		}
	}
	return nil
}

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
	}
	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
		// to use cfg.Env, and we always want to check "HOME" first.

		if vr := cfg.Env.Get("HOME"); vr.IsSet() {
			return vr.String(), rest
		}

		if runtime.GOOS == "windows" {
			if vr := cfg.Env.Get("USERPROFILE"); vr.IsSet() {
				return vr.String(), rest
			}
		}
		return "", field
	}

	// Not the current user; try via "HOME <name>", otherwise fall back to
	// os/user. There isn't a way to lookup user home dirs without cgo.

	if vr := cfg.Env.Get("HOME " + name); vr.IsSet() {
		return vr.String(), rest
	}

	u, err := user.Lookup(name)
	if err != nil {
		return "", field
	}
	return u.HomeDir, rest
}

func findAllIndex(pat, name string, n int) [][]int {
	expr, err := pattern.Regexp(pat, 0)
	if err != nil {
		return nil
	}
	rx := regexp.MustCompile(expr)
	return rx.FindAllStringIndex(name, n)
}

var rxGlobStar = regexp.MustCompile(".*")

// 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 == "" {
		return elem2
	}
	if strings.HasSuffix(elem1, string(filepath.Separator)) {
		return elem1 + elem2
	}
	return elem1 + string(filepath.Separator) + elem2
}

// pathSplit splits a file path into its elements, retaining empty ones. Before
// 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)
	return strings.Split(path, string(filepath.Separator))
}

func (cfg *Config) glob(base, pat string) ([]string, error) {
	parts := pathSplit(pat)
	matches := []string{""}
	if filepath.IsAbs(pat) {
		if parts[0] == "" {
			// unix-like
			matches[0] = string(filepath.Separator)
		} else {
			// windows (for some reason it won't work without the
			// trailing separator)
			matches[0] = parts[0] + string(filepath.Separator)
		}
		parts = parts[1:]
	}
	// 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 ReadDir2 calls.
	// For example, /foo/* can end up doing one duplicate call:
	//
	//    ReadDir2("/foo") to ensure that "/foo/" exists and only matches a directory
	//    ReadDir2("/foo") glob "*"

	for i, part := range parts {
		// Keep around for debugging.
		// log.Printf("matches %q part %d %q", matches, i, part)

		wantDir := i < len(parts)-1
		switch {
		case part == "", part == ".", part == "..":
			for i, dir := range matches {
				matches[i] = pathJoin2(dir, part)
			}
			continue
		case !pattern.HasMeta(part, 0):
			var newMatches []string
			for _, dir := range matches {
				match := dir
				if !filepath.IsAbs(match) {
					match = filepath.Join(base, match)
				}
				match = pathJoin2(match, part)
				// 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 [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.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.
						// See https://github.com/golang/go/issues/46734.
						// TODO: remove when the Go issue above is resolved.
					} else if errors.Is(err, fs.ErrNotExist) {
						continue // simply doesn't exist
					}
					if wantDir {
						continue // exists but not a directory
					}
				}
				newMatches = append(newMatches, pathJoin2(dir, part))
			}
			matches = newMatches
			continue
		case part == "**" && cfg.GlobStar:
			// Find all recursive matches for "**".
			// Note that we need the results to be in depth-first order,
			// 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 _, 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(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)
				}

				// 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 _, match := range slices.Backward(newMatches) {
					stack = append(stack, match)
				}
			}
			continue
		}
		mode := pattern.Filenames | pattern.EntireString | pattern.NoGlobStar
		if cfg.NoCaseGlob {
			mode |= pattern.NoGlobCase
		}
		expr, err := pattern.Regexp(part, mode)
		if err != nil {
			return nil, err
		}
		rx := regexp.MustCompile(expr)
		var newMatches []string
		for _, dir := range matches {
			newMatches, err = cfg.globDir(base, dir, rx, wantDir, newMatches)
			if err != nil {
				return nil, err
			}
		}
		matches = newMatches
	}
	return matches, nil
}

func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, matches []string) ([]string, error) {
	fullDir := dir
	if !filepath.IsAbs(dir) {
		fullDir = filepath.Join(base, dir)
	}
	infos, err := cfg.ReadDir2(fullDir)
	if err != nil {
		// We still want to return matches, for the sake of reusing slices.
		return matches, err
	}
	for _, info := range infos {
		name := info.Name()
		if !wantDir {
			// No filtering.
		} 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 [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.ReadDir2(filepath.Join(fullDir, info.Name())); err != nil {
				continue
			}
		} else if !mode.IsDir() {
			// Not a symlink nor a directory.
			continue
		}
		if rx.MatchString(name) {
			matches = append(matches, pathJoin2(dir, name))
		}
	}
	return matches, nil
}

// 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.
func ReadFields(cfg *Config, s string, n int, raw bool) []string {
	cfg = prepareConfig(cfg)
	type pos struct {
		start, end int
	}
	var fpos []pos

	runes := make([]rune, 0, len(s))
	infield := false
	esc := false
	for _, r := range s {
		if infield {
			if cfg.ifsRune(r) && (raw || !esc) {
				fpos[len(fpos)-1].end = len(runes)
				infield = false
			}
		} else {
			if !cfg.ifsRune(r) && (raw || !esc) {
				fpos = append(fpos, pos{start: len(runes), end: -1})
				infield = true
			}
		}
		if r == '\\' {
			if raw || esc {
				runes = append(runes, r)
			}
			esc = !esc
			continue
		}
		runes = append(runes, r)
		esc = false
	}
	if len(fpos) == 0 {
		return nil
	}
	if infield {
		fpos[len(fpos)-1].end = len(runes)
	}

	switch {
	case n == 1:
		// include heading/trailing IFSs
		fpos[0].start, fpos[0].end = 0, len(runes)
		fpos = fpos[:1]
	case n != -1 && n < len(fpos):
		// combine to max n fields
		fpos[n-1].end = fpos[len(fpos)-1].end
		fpos = fpos[:n]
	}

	fields := make([]string, len(fpos))
	for i, p := range fpos {
		fields[i] = string(runes[p.start:p.end])
	}
	return fields
}
07070100000025000081A4000000000000000000000001686AE5C0000000C0000000000000000000000000000000000000002600000000sh-3.12.0/expand/expand_nonwindows.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

//go:build !windows

package expand

func isWindowsErrPathNotFound(error) bool { return false }
07070100000026000081A4000000000000000000000001686AE5C000000AD6000000000000000000000000000000000000002000000000sh-3.12.0/expand/expand_test.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"io/fs"
	"os"
	"reflect"
	"strings"
	"testing"

	"mvdan.cc/sh/v3/syntax"
)

func parseWord(t *testing.T, src string) *syntax.Word {
	t.Helper()
	p := syntax.NewParser()
	word, err := p.Document(strings.NewReader(src))
	if err != nil {
		t.Fatal(err)
	}
	return word
}

func TestConfigNils(t *testing.T) {
	os.Setenv("EXPAND_GLOBAL", "value")
	tests := []struct {
		name string
		cfg  *Config
		src  string
		want string
	}{
		{
			"NilConfig",
			nil,
			"$EXPAND_GLOBAL",
			"",
		},
		{
			"ZeroConfig",
			&Config{},
			"$EXPAND_GLOBAL",
			"",
		},
		{
			"EnvConfig",
			&Config{Env: ListEnviron(os.Environ()...)},
			"$EXPAND_GLOBAL",
			"value",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			word := parseWord(t, tc.src)
			got, err := Literal(tc.cfg, word)
			if err != nil {
				t.Fatalf("did not want error, got %v", err)
			}
			if got != tc.want {
				t.Fatalf("wanted %q, got %q", tc.want, got)
			}
		})
	}
}

func TestFieldsIdempotency(t *testing.T) {
	tests := []struct {
		src  string
		want []string
	}{
		{
			"{1..4}",
			[]string{"1", "2", "3", "4"},
		},
		{
			"a{1..4}",
			[]string{"a1", "a2", "a3", "a4"},
		},
	}
	for _, tc := range tests {
		word := parseWord(t, tc.src)
		for range 2 {
			got, err := Fields(nil, word)
			if err != nil {
				t.Fatalf("did not want error, got %v", err)
			}
			if !reflect.DeepEqual(got, tc.want) {
				t.Fatalf("wanted %q, got %q", tc.want, got)
			}
		}
	}
}

func Test_glob(t *testing.T) {
	cfg := &Config{
		ReadDir2: func(string) ([]fs.DirEntry, error) {
			return []fs.DirEntry{
				&mockFileInfo{name: "a"},
				&mockFileInfo{name: "ab"},
				&mockFileInfo{name: "A"},
				&mockFileInfo{name: "AB"},
			}, nil
		},
	}

	tests := []struct {
		noCaseGlob bool
		pat        string
		want       []string
	}{
		{false, "a*", []string{"a", "ab"}},
		{false, "A*", []string{"A", "AB"}},
		{false, "*b", []string{"ab"}},
		{false, "b*", nil},
		{true, "a*", []string{"a", "ab", "A", "AB"}},
		{true, "A*", []string{"a", "ab", "A", "AB"}},
		{true, "*b", []string{"ab", "AB"}},
		{true, "b*", nil},
	}
	for _, tc := range tests {
		cfg.NoCaseGlob = tc.noCaseGlob
		got, err := cfg.glob("/", tc.pat)
		if err != nil {
			t.Fatalf("did not want error, got %v", err)
		}
		if !reflect.DeepEqual(got, tc.want) {
			t.Fatalf("wanted %q, got %q", tc.want, got)
		}
	}
}

type mockFileInfo struct {
	name        string
	typ         fs.FileMode
	fs.DirEntry // Stub out everything but Name() & Type()
}

var _ fs.DirEntry = (*mockFileInfo)(nil)

func (fi *mockFileInfo) Name() string      { return fi.name }
func (fi *mockFileInfo) Type() fs.FileMode { return fi.typ }
07070100000027000081A4000000000000000000000001686AE5C000000134000000000000000000000000000000000000002300000000sh-3.12.0/expand/expand_windows.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// 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
}
07070100000028000081A4000000000000000000000001686AE5C000002388000000000000000000000000000000000000001A00000000sh-3.12.0/expand/param.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"fmt"
	"maps"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"mvdan.cc/sh/v3/pattern"
	"mvdan.cc/sh/v3/syntax"
)

func nodeLit(node syntax.Node) string {
	if word, ok := node.(*syntax.Word); ok {
		return word.Lit()
	}
	return ""
}

type UnsetParameterError struct {
	Node    *syntax.ParamExp
	Message string
}

func (u UnsetParameterError) Error() string {
	return fmt.Sprintf("%s: %s", u.Node.Param.Value, u.Message)
}

func overridingUnset(pe *syntax.ParamExp) bool {
	if pe.Exp == nil {
		return false
	}
	switch pe.Exp.Op {
	case syntax.AlternateUnset, syntax.AlternateUnsetOrNull,
		syntax.DefaultUnset, syntax.DefaultUnsetOrNull,
		syntax.ErrorUnset, syntax.ErrorUnsetOrNull,
		syntax.AssignUnset, syntax.AssignUnsetOrNull:
		return true
	}
	return false
}

func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) {
	oldParam := cfg.curParam
	cfg.curParam = pe
	defer func() { cfg.curParam = oldParam }()

	name := pe.Param.Value
	index := pe.Index
	switch name {
	case "@", "*":
		index = &syntax.Word{Parts: []syntax.WordPart{
			&syntax.Lit{Value: name},
		}}
	}
	var vr Variable
	switch name {
	case "LINENO":
		// This is the only parameter expansion that the environment
		// interface cannot satisfy.
		line := uint64(cfg.curParam.Pos().Line())
		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.IsSet() && !overridingUnset(pe) {
		return "", UnsetParameterError{
			Node:    pe,
			Message: "unbound variable",
		}
	}

	var sliceOffset, sliceLen int
	if pe.Slice != nil {
		var err error
		if pe.Slice.Offset != nil {
			sliceOffset, err = Arithm(cfg, pe.Slice.Offset)
			if err != nil {
				return "", err
			}
		}
		if pe.Slice.Length != nil {
			sliceLen, err = Arithm(cfg, pe.Slice.Length)
			if err != nil {
				return "", err
			}
		}
	}

	var (
		str   string
		elems []string

		indexAllElements bool // true if var has been accessed with * or @ index
		callVarInd       = true
	)

	switch nodeLit(index) {
	case "@", "*":
		switch vr.Kind {
		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)]
			}
			str = strings.Join(elems, " ")
		}
	}
	if callVarInd {
		var err error
		str, err = cfg.varInd(vr, index)
		if err != nil {
			return "", err
		}
	}
	if !indexAllElements {
		elems = []string{str}
	}

	switch {
	case pe.Length:
		n := len(elems)
		switch nodeLit(index) {
		case "@", "*":
		default:
			n = utf8.RuneCountInString(str)
		}
		str = strconv.Itoa(n)
	case pe.Excl:
		var strs []string
		switch {
		case pe.Names != 0:
			strs = cfg.namesByPrefix(pe.Param.Value)
		case orig.Kind == NameRef:
			strs = append(strs, orig.Str)
		case pe.Index != nil && vr.Kind == Indexed:
			for i, e := range vr.List {
				if e != "" {
					strs = append(strs, strconv.Itoa(i))
				}
			}
		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())
		}
		slices.Sort(strs)
		str = strings.Join(strs, " ")
	case pe.Slice != nil:
		if callVarInd {
			slicePos := func(n int) int {
				if n < 0 {
					n = len(str) + n
					if n < 0 {
						n = len(str)
					}
				} else if n > len(str) {
					n = len(str)
				}
				return n
			}
			if pe.Slice.Offset != nil {
				str = str[slicePos(sliceOffset):]
			}
			if pe.Slice.Length != nil {
				str = str[:slicePos(sliceLen)]
			}
		} // 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
		}
		n := 1
		if pe.Repl.All {
			n = -1
		}
		locs := findAllIndex(orig, str, n)
		sb := cfg.strBuilder()
		last := 0
		for _, loc := range locs {
			sb.WriteString(str[last:loc[0]])
			sb.WriteString(with)
			last = loc[1]
		}
		sb.WriteString(str[last:])
		str = sb.String()
	case pe.Exp != nil:
		arg, err := Literal(cfg, pe.Exp.Word)
		if err != nil {
			return "", err
		}
		switch op := pe.Exp.Op; op {
		case syntax.AlternateUnsetOrNull:
			if str == "" {
				break
			}
			fallthrough
		case syntax.AlternateUnset:
			if vr.IsSet() {
				str = arg
			}
		case syntax.DefaultUnset:
			if vr.IsSet() {
				break
			}
			fallthrough
		case syntax.DefaultUnsetOrNull:
			if str == "" {
				str = arg
			}
		case syntax.ErrorUnset:
			if vr.IsSet() {
				break
			}
			fallthrough
		case syntax.ErrorUnsetOrNull:
			if str == "" {
				return "", UnsetParameterError{
					Node:    pe,
					Message: arg,
				}
			}
		case syntax.AssignUnset:
			if vr.IsSet() {
				break
			}
			fallthrough
		case syntax.AssignUnsetOrNull:
			if str == "" {
				if err := cfg.envSet(name, arg); err != nil {
					return "", err
				}
				str = arg
			}
		case syntax.RemSmallPrefix, syntax.RemLargePrefix,
			syntax.RemSmallSuffix, syntax.RemLargeSuffix:
			suffix := op == syntax.RemSmallSuffix || op == syntax.RemLargeSuffix
			small := op == syntax.RemSmallPrefix || op == syntax.RemSmallSuffix
			for i, elem := range elems {
				elems[i] = removePattern(elem, arg, suffix, small)
			}
			str = strings.Join(elems, " ")
		case syntax.UpperFirst, syntax.UpperAll,
			syntax.LowerFirst, syntax.LowerAll:

			caseFunc := unicode.ToLower
			if op == syntax.UpperFirst || op == syntax.UpperAll {
				caseFunc = unicode.ToUpper
			}
			all := op == syntax.UpperAll || op == syntax.LowerAll

			// empty string means '?'; nothing to do there
			expr, err := pattern.Regexp(arg, 0)
			if err != nil {
				return str, nil
			}
			rx := regexp.MustCompile(expr)

			for i, elem := range elems {
				rs := []rune(elem)
				for ri, r := range rs {
					if rx.MatchString(string(r)) {
						rs[ri] = caseFunc(r)
						if !all {
							break
						}
					}
				}
				elems[i] = string(rs)
			}
			str = strings.Join(elems, " ")
		case syntax.OtherParamOps:
			switch arg {
			case "Q":
				str, err = syntax.Quote(str, syntax.LangBash)
				if err != nil {
					// Is this even possible? If a user runs into this panic,
					// it's most likely a bug we need to fix.
					panic(err)
				}
			case "E":
				tail := str
				var rns []rune
				for tail != "" {
					var rn rune
					rn, _, tail, _ = strconv.UnquoteChar(tail, 0)
					rns = append(rns, rn)
				}
				str = string(rns)
			case "P", "A", "a":
				panic(fmt.Sprintf("unhandled @%s param expansion", arg))
			default:
				panic(fmt.Sprintf("unexpected @%s param expansion", arg))
			}
		}
	}
	return str, nil
}

func removePattern(str, pat string, fromEnd, shortest bool) string {
	var mode pattern.Mode
	if shortest {
		mode |= pattern.Shortest
	}
	expr, err := pattern.Regexp(pat, mode)
	if err != nil {
		return str
	}
	switch {
	case fromEnd && shortest:
		// use .* to get the right-most shortest match
		expr = ".*(" + expr + ")$"
	case fromEnd:
		// simple suffix
		expr = "(" + expr + ")$"
	default:
		// simple prefix
		expr = "^(" + expr + ")"
	}
	// no need to check error as Translate returns one
	rx := regexp.MustCompile(expr)
	if loc := rx.FindStringSubmatchIndex(str); loc != nil {
		// remove the original pattern (the submatch)
		str = str[:loc[2]] + str[loc[3]:]
	}
	return str
}

func (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, error) {
	if idx == nil {
		return vr.String(), nil
	}
	switch vr.Kind {
	case String:
		n, err := Arithm(cfg, idx)
		if err != nil {
			return "", err
		}
		if n == 0 {
			return vr.Str, nil
		}
	case Indexed:
		switch nodeLit(idx) {
		case "*", "@":
			return strings.Join(vr.List, " "), nil
		}
		i, err := Arithm(cfg, idx)
		if err != nil {
			return "", err
		}
		if i < 0 {
			return "", fmt.Errorf("negative array index")
		}
		if i < len(vr.List) {
			return vr.List[i], nil
		}
	case Associative:
		switch lit := nodeLit(idx); lit {
		case "@", "*":
			strs := slices.Sorted(maps.Values(vr.Map))
			if lit == "*" {
				return cfg.ifsJoin(strs), nil
			}
			return strings.Join(strs, " "), nil
		}
		val, err := Literal(cfg, idx.(*syntax.Word))
		if err != nil {
			return "", err
		}
		return vr.Map[val], nil
	}
	return "", nil
}

func (cfg *Config) namesByPrefix(prefix string) []string {
	var names []string
	for name := range cfg.Env.Each {
		if strings.HasPrefix(name, prefix) {
			names = append(names, name)
		}
	}
	return names
}
07070100000029000081A4000000000000000000000001686AE5C0000002E9000000000000000000000000000000000000002500000000sh-3.12.0/expand/valuekind_string.go// Code generated by "stringer -type=ValueKind"; DO NOT EDIT.

package expand

import "strconv"

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[Unknown-0]
	_ = x[String-1]
	_ = x[NameRef-2]
	_ = x[Indexed-3]
	_ = x[Associative-4]
	_ = x[KeepValue-5]
}

const _ValueKind_name = "UnknownStringNameRefIndexedAssociativeKeepValue"

var _ValueKind_index = [...]uint8{0, 7, 13, 20, 27, 38, 47}

func (i ValueKind) String() string {
	if i >= ValueKind(len(_ValueKind_index)-1) {
		return "ValueKind(" + strconv.FormatInt(int64(i), 10) + ")"
	}
	return _ValueKind_name[_ValueKind_index[i]:_ValueKind_index[i+1]]
}
0707010000002A000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001300000000sh-3.12.0/fileutil0707010000002B000081A4000000000000000000000001686AE5C000000AD0000000000000000000000000000000000000001B00000000sh-3.12.0/fileutil/file.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package fileutil allows inspecting shell files, such as detecting whether a
// file may be shell or extracting its shebang.
package fileutil

import (
	"io/fs"
	"regexp"
	"strings"
)

var (
	shebangRe = regexp.MustCompile(`^#![ \t]*/(usr/)?bin/(env[ \t]+)?(sh|bash|mksh|bats|zsh)(\s|$)`)
	extRe     = regexp.MustCompile(`\.(sh|bash|mksh|bats|zsh)$`)
)

// TODO: consider removing HasShebang in favor of Shebang in v4

// HasShebang reports whether bs begins with a valid shell shebang.
// It supports variations with /usr and env.
func HasShebang(bs []byte) bool {
	return Shebang(bs) != ""
}

// Shebang parses a "#!" sequence from the beginning of the input bytes,
// and returns the shell that it points to.
//
// For instance, it returns "sh" for "#!/bin/sh",
// and "bash" for "#!/usr/bin/env bash".
func Shebang(bs []byte) string {
	m := shebangRe.FindSubmatch(bs)
	if m == nil {
		return ""
	}
	return string(m[3])
}

// ScriptConfidence defines how likely a file is to be a shell script,
// from complete certainty that it is not one to complete certainty that
// it is one.
type ScriptConfidence int

const (
	// ConfNotScript describes files which are definitely not shell scripts,
	// such as non-regular files or files with a non-shell extension.
	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 [fs.FileInfo], the answer in this case can't be final.
	ConfIfShebang

	// ConfIsScript describes files which are definitely shell scripts,
	// which are regular files with a valid shell extension.
	ConfIsScript
)

// CouldBeScript is a shortcut for CouldBeScript2(fs.FileInfoToDirEntry(info)).
//
// 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 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 name[0] == '.':
		return ConfNotScript // '.' prefix (hidden file)
	case !entry.Type().IsRegular():
		return ConfNotScript // dir, symlink, named pipes, etc
	case extRe.MatchString(name):
		return ConfIsScript // shell extension
	case strings.IndexByte(name, '.') > 0:
		return ConfNotScript // non-shell extension
	default:
		return ConfIfShebang // no extension; read and look for a shebang
	}
}
0707010000002C000081A4000000000000000000000001686AE5C0000003E0000000000000000000000000000000000000002000000000sh-3.12.0/fileutil/file_test.go// Copyright (c) 2025, Ville Skyttä <ville.skytta@iki.fi>
// See LICENSE for licensing information

package fileutil

import (
	"strings"
	"testing"
)

func TestShebang(t *testing.T) {
	t.Parallel()
	tests := []struct {
		in   []byte
		want string
	}{
		{
			in:   []byte("#!/usr/bin/env bash"),
			want: "bash",
		},
		{
			in:   []byte("#!/bin/bash"),
			want: "bash",
		},
		{
			in:   []byte("#!foo bar"),
			want: "",
		},
		{
			in:   []byte("#!/bin/zsh"),
			want: "zsh",
		},
		{
			in:   []byte("#! /bin/zsh true"),
			want: "zsh",
		},
		{
			in:   []byte("#!  /bin/zsh"),
			want: "zsh",
		},
		{
			in:   []byte("#!\t/bin/zsh"),
			want: "zsh",
		},
		{
			in:   []byte("#!\f/bin/zsh"),
			want: "",
		},
	}

	for _, test := range tests {
		name := strings.ReplaceAll(strings.ReplaceAll(string(test.in), "\f", "\\f"), "\t", "\\t")
		t.Run(name, func(t *testing.T) {
			if got := Shebang(test.in); got != test.want {
				t.Fatalf("want %q, got %q", test.want, got)
			}
		})
	}
}
0707010000002D000081A4000000000000000000000001686AE5C0000001BA000000000000000000000000000000000000001100000000sh-3.12.0/go.modmodule mvdan.cc/sh/v3

go 1.23.0

require (
	github.com/creack/pty v1.1.24
	github.com/go-quicktest/qt v1.101.0
	github.com/google/go-cmp v0.7.0
	github.com/google/renameio/v2 v2.0.0
	github.com/rogpeppe/go-internal v1.14.1
	golang.org/x/sys v0.33.0
	golang.org/x/term v0.32.0
	mvdan.cc/editorconfig v0.3.0
)

require (
	github.com/kr/pretty v0.3.1 // indirect
	github.com/kr/text v0.2.0 // indirect
	golang.org/x/tools v0.31.0 // indirect
)
0707010000002E000081A4000000000000000000000001686AE5C00000082D000000000000000000000000000000000000001100000000sh-3.12.0/go.sumgithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
mvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=
mvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=
0707010000002F000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001100000000sh-3.12.0/interp07070100000030000081A4000000000000000000000001686AE5C000006BAB000000000000000000000000000000000000001800000000sh-3.12.0/interp/api.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// 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.
//
// The interpreter generally aims to behave like Bash,
// but it does not support all of its features.
//
// The interpreter currently aims to behave like a non-interactive shell,
// which is how most shells run scripts, and is more useful to machines.
// In the future, it may gain an option to behave like an interactive shell.
package interp

import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"maps"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"time"

	"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. 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
// isn't safe for concurrent use, consider a workaround like hiding writes
// behind a mutex.
//
// 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
	// 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. 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
	// func simultaneously.
	// Vars is mostly superseded by Env at this point.
	// TODO(v4): remove these

	Vars  map[string]expand.Variable
	Funcs map[string]*syntax.Stmt

	alias map[string]alias

	// callHandler is a function allowing to replace a simple command's
	// arguments. It may be nil.
	callHandler CallHandlerFunc

	// execHandler is responsible for executing programs. It must not be nil.
	execHandler ExecHandlerFunc

	// 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 ReadDirHandlerFunc2

	// statHandler is a function responsible for getting file stat. It must be non-nil.
	statHandler StatHandlerFunc

	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

	// 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.
	didReset bool

	usedNew bool

	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
	handlingTrap bool // whether we're currently in a trap callback

	// track if a sourced script set positional parameters
	sourceSetParams bool

	// noErrExit prevents failing commands from triggering [optErrExit],
	// such as the condition in a [syntax.IfClause].
	noErrExit bool

	// 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 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

	// 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  *os.File
	origStdout io.Writer
	origStderr io.Writer

	// Most scripts don't use pushd/popd, so make space for the initial PWD
	// without requiring an extra allocation.
	dirStack     []string
	dirBootstrap [1]string

	optState getopts

	// keepRedirs is used so that "exec" can make any redirections
	// apply to the current shell, and not just the command.
	keepRedirs bool

	// Fake signal callbacks
	callbackErr  string
	callbackExit string
}

// exitStatus holds the state of the shell after running one command.
// Beyond the exit status code, it also holds whether the shell should return or exit,
// as well as any Go error values that should be given back to the user.
//
// TODO(v4): consider replacing ExitStatus with a struct like this,
// so that an [ExecHandlerFunc] can e.g. mimic `exit 0` or fatal errors
// with specific exit codes.
type exitStatus struct {
	// code is the exit status code.
	code uint8

	// TODO: consider an enum, as only one of these should be set at a time
	returning bool // whether the current function `return`ed
	exiting   bool // whether the current shell is exiting
	fatalExit bool // whether the current shell is exiting due to a fatal error; err below must not be nil

	// err is a fatal error if fatal is true, or a non-fatal custom error from a handler.
	// Used so that running a single statement with a custom handler
	// which returns a non-fatal Go error, such as a Go error wrapping [NewExitStatus],
	// can be returned by [Runner.Run] without being lost entirely.
	err error
}

func (e *exitStatus) ok() bool { return e.code == 0 }

func (e *exitStatus) oneIf(b bool) {
	if b {
		e.code = 1
	} else {
		e.code = 0
	}
}

func (e *exitStatus) fatal(err error) {
	if !e.fatalExit && err != nil {
		e.exiting = true
		e.fatalExit = true
		e.err = err
		if e.code == 0 {
			e.code = 1
		}
	}
}

func (e *exitStatus) fromHandlerError(err error) {
	if err != nil {
		var exit errBuiltinExitStatus
		var es ExitStatus
		if errors.As(err, &exit) {
			*e = exitStatus(exit)
		} else if errors.As(err, &es) {
			e.err = err
			e.code = uint8(es)
		} else {
			e.fatal(err) // handler's custom fatal error
		}
	} else {
		e.code = 0
	}
}

type bgProc struct {
	// closed when the background process finishes,
	// after which point the result fields below are set.
	done chan struct{}

	exit *exitStatus
}

type alias struct {
	args  []*syntax.Word
	blank bool
}

func (r *Runner) optByFlag(flag byte) *bool {
	for i, opt := range &shellOptsTable {
		if opt.flag == flag {
			return &r.opts[i]
		}
	}
	return nil
}

// New creates a new Runner, applying a number of options. If applying any of
// the options results in an error, it is returned.
//
// Any unset options fall back to their defaults. For example, not supplying the
// environment falls back to the process's environment, and not supplying the
// standard output writer means that the output will be discarded.
func New(opts ...RunnerOption) (*Runner, error) {
	r := &Runner{
		usedNew:        true,
		openHandler:    DefaultOpenHandler(),
		readDirHandler: DefaultReadDirHandler2(),
		statHandler:    DefaultStatHandler(),
	}
	r.dirStack = r.dirBootstrap[:0]
	// turn "on" the default Bash options
	for i, opt := range bashOptsTable {
		r.opts[len(shellOptsTable)+i] = opt.defaultState
	}

	for _, opt := range opts {
		if err := opt(r); err != nil {
			return nil, err
		}
	}

	// Set the default fallbacks, if necessary.
	if r.Env == nil {
		Env(nil)(r)
	}
	if r.Dir == "" {
		if err := Dir("")(r); err != nil {
			return nil, err
		}
	}
	if r.stdout == nil || r.stderr == nil {
		StdIO(r.stdin, r.stdout, r.stderr)(r)
	}
	return r, nil
}

// 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 {
	return func(r *Runner) error {
		if env == nil {
			env = expand.ListEnviron(os.Environ()...)
		}
		r.Env = env
		return nil
	}
}

// Dir sets the interpreter's working directory. If empty, the process's current
// directory is used.
func Dir(path string) RunnerOption {
	return func(r *Runner) error {
		if path == "" {
			path, err := os.Getwd()
			if err != nil {
				return fmt.Errorf("could not get current dir: %w", err)
			}
			r.Dir = path
			return nil
		}
		path, err := filepath.Abs(path)
		if err != nil {
			return fmt.Errorf("could not get absolute dir: %w", err)
		}
		info, err := os.Stat(path)
		if err != nil {
			return fmt.Errorf("could not stat: %w", err)
		}
		if !info.IsDir() {
			return fmt.Errorf("%s is not a directory", path)
		}
		r.Dir = path
		return nil
	}
}

// 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.
//
// This is similar to what the interpreter's "set" builtin does.
func Params(args ...string) RunnerOption {
	return func(r *Runner) error {
		fp := flagParser{remaining: args}
		for fp.more() {
			flag := fp.flag()
			if flag == "-" {
				// TODO: implement "The -x and -v options are turned off."
				if args := fp.args(); len(args) > 0 {
					r.Params = args
				}
				return nil
			}
			enable := flag[0] == '-'
			if flag[1] != 'o' {
				opt := r.optByFlag(flag[1])
				if opt == nil {
					return fmt.Errorf("invalid option: %q", flag)
				}
				*opt = enable
				continue
			}
			value := fp.value()
			if value == "" && enable {
				for i, opt := range &shellOptsTable {
					r.printOptLine(opt.name, r.opts[i], true)
				}
				continue
			}
			if value == "" && !enable {
				for i, opt := range &shellOptsTable {
					setFlag := "+o"
					if r.opts[i] {
						setFlag = "-o"
					}
					r.outf("set %s %s\n", setFlag, opt.name)
				}
				continue
			}
			_, opt := r.optByName(value, false)
			if opt == nil {
				return fmt.Errorf("invalid option: %q", value)
			}
			*opt = enable
		}
		if args := fp.args(); args != nil {
			// If "--" wasn't given and there were zero arguments,
			// we don't want to override the current parameters.
			r.Params = args

			// Record whether a sourced script sets the parameters.
			if r.inSource {
				r.sourceSetParams = true
			}
		}
		return nil
	}
}

// CallHandler sets the call handler. See [CallHandlerFunc] for more info.
func CallHandler(f CallHandlerFunc) RunnerOption {
	return func(r *Runner) error {
		r.callHandler = f
		return nil
	}
}

// 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
		return nil
	}
}

// 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
		return nil
	}
}

// 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.
func StatHandler(f StatHandlerFunc) RunnerOption {
	return func(r *Runner) error {
		r.statHandler = f
		return nil
	}
}

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 {
		stdin, _err := stdinFile(in)
		if _err != nil {
			return _err
		}
		r.stdin = stdin
		if out == nil {
			out = io.Discard
		}
		r.stdout = out
		if err == nil {
			err = io.Discard
		}
		r.stderr = err
		return nil
	}
}

// optByName returns the matching runner's option index and status
func (r *Runner) optByName(name string, bash bool) (index int, status *bool) {
	if bash {
		for i, opt := range bashOptsTable {
			if opt.name == name {
				index = len(shellOptsTable) + i
				return index, &r.opts[index]
			}
		}
	}
	for i, opt := range &shellOptsTable {
		if opt.name == name {
			return i, &r.opts[i]
		}
	}
	return 0, nil
}

type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool

type shellOpt struct {
	flag byte
	name string
}

type bashOpt struct {
	name         string
	defaultState bool // Bash's default value for this option
	supported    bool // whether we support the option's non-default state
}

var shellOptsTable = [...]shellOpt{
	// sorted alphabetically by name; use a space for the options
	// that have no flag form
	{'a', "allexport"},
	{'e', "errexit"},
	{'n', "noexec"},
	{'f', "noglob"},
	{'u', "nounset"},
	{'x', "xtrace"},
	{' ', "pipefail"},
}

var bashOptsTable = [...]bashOpt{
	// supported options, sorted alphabetically by name
	{
		name:         "expand_aliases",
		defaultState: false,
		supported:    true,
	},
	{
		name:         "globstar",
		defaultState: false,
		supported:    true,
	},
	{
		name:         "nocaseglob",
		defaultState: false,
		supported:    true,
	},
	{
		name:         "nullglob",
		defaultState: false,
		supported:    true,
	},
	// unsupported options, sorted alphabetically by name
	{name: "assoc_expand_once"},
	{name: "autocd"},
	{name: "cdable_vars"},
	{name: "cdspell"},
	{name: "checkhash"},
	{name: "checkjobs"},
	{
		name:         "checkwinsize",
		defaultState: true,
	},
	{
		name:         "cmdhist",
		defaultState: true,
	},
	{name: "compat31"},
	{name: "compat32"},
	{name: "compat40"},
	{name: "compat41"},
	{name: "compat42"},
	{name: "compat44"},
	{name: "compat43"},
	{name: "compat44"},
	{
		name:         "complete_fullquote",
		defaultState: true,
	},
	{name: "direxpand"},
	{name: "dirspell"},
	{name: "dotglob"},
	{name: "execfail"},
	{name: "extdebug"},
	{name: "extglob"},
	{
		name:         "extquote",
		defaultState: true,
	},
	{name: "failglob"},
	{
		name:         "force_fignore",
		defaultState: true,
	},
	{name: "globasciiranges"},
	{name: "gnu_errfmt"},
	{name: "histappend"},
	{name: "histreedit"},
	{name: "histverify"},
	{
		name:         "hostcomplete",
		defaultState: true,
	},
	{name: "huponexit"},
	{
		name:         "inherit_errexit",
		defaultState: true,
	},
	{
		name:         "interactive_comments",
		defaultState: true,
	},
	{name: "lastpipe"},
	{name: "lithist"},
	{name: "localvar_inherit"},
	{name: "localvar_unset"},
	{name: "login_shell"},
	{name: "mailwarn"},
	{name: "no_empty_cmd_completion"},
	{name: "nocasematch"},
	{
		name:         "progcomp",
		defaultState: true,
	},
	{name: "progcomp_alias"},
	{
		name:         "promptvars",
		defaultState: true,
	},
	{name: "restricted_shell"},
	{name: "shift_verbose"},
	{
		name:         "sourcepath",
		defaultState: true,
	},
	{name: "xpg_echo"},
}

// To access the shell options arrays without a linear search when we
// know which option we're after at compile time. First come the shell options,
// then the bash options.
const (
	// These correspond to indexes in [shellOptsTable]
	optAllExport = iota
	optErrExit
	optNoExec
	optNoGlob
	optNoUnset
	optXTrace
	optPipeFail

	// These correspond to indexes (offset by the above seven items) of
	// supported options in [bashOptsTable]
	optExpandAliases
	optGlobStar
	optNoCaseGlob
	optNullGlob
)

// Reset returns a runner to its initial state, right before the first call to
// Run or Reset.
//
// Typically, this function only needs to be called if a runner is reused to run
// multiple programs non-incrementally. Not calling Reset between each run will
// mean that the shell state will be kept, including variables, options, and the
// current directory.
func (r *Runner) Reset() {
	if !r.usedNew {
		panic("use interp.New to construct a Runner")
	}
	if !r.didReset {
		r.origDir = r.Dir
		r.origParams = r.Params
		r.origOpts = r.opts
		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
		// builtins can overwrite them; reset the fields to whatever the
		// constructor set up.
		Dir:    r.origDir,
		Params: r.origParams,
		opts:   r.origOpts,
		stdin:  r.origStdin,
		stdout: r.origStdout,
		stderr: r.origStderr,

		origDir:    r.origDir,
		origParams: r.origParams,
		origOpts:   r.origOpts,
		origStdin:  r.origStdin,
		origStdout: r.origStdout,
		origStderr: r.origStderr,

		// emptied below, to reuse the space
		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 {
		clear(r.Vars)
	}
	// TODO(v4): Use the supplied Env directly if it implements enough methods.
	r.writeEnv = &overlayEnviron{parent: r.Env}
	if !r.writeEnv.Get("HOME").IsSet() {
		home, _ := os.UserHomeDir()
		r.setVarString("HOME", home)
	}
	if !r.writeEnv.Get("UID").IsSet() {
		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", expand.Variable{
			Set:      true,
			Kind:     expand.String,
			ReadOnly: true,
			Str:      strconv.Itoa(os.Getgid()),
		})
	}
	r.setVarString("PWD", r.Dir)
	r.setVarString("IFS", " \t\n")
	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

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)
}

// 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 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
// error is returned, it will typically contain a command's exit status, which
// 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,
// call Reset.
//
// 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.exit = exitStatus{}
	r.filename = ""
	switch node := node.(type) {
	case *syntax.File:
		r.filename = node.Name
		r.stmts(ctx, node.Stmts)
	case *syntax.Stmt:
		r.stmt(ctx, node)
	case syntax.Command:
		r.cmd(ctx, node)
	default:
		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 {
		return err
	}
	if code := r.exit.code; code != 0 {
		return ExitStatus(code)
	}
	return nil
}

// Exited reports whether the last Run call should exit an entire shell. This
// can be triggered by the "exit" built-in command, for example.
//
// 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.exit.exiting
}

// 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
// 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
// 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.
	r2 := &Runner{
		Dir:            r.Dir,
		tempDir:        r.tempDir,
		Params:         r.Params,
		callHandler:    r.callHandler,
		execHandler:    r.execHandler,
		openHandler:    r.openHandler,
		readDirHandler: r.readDirHandler,
		statHandler:    r.statHandler,
		stdin:          r.stdin,
		stdout:         r.stdout,
		stderr:         r.stderr,
		filename:       r.filename,
		opts:           r.opts,
		usedNew:        r.usedNew,
		exit:           r.exit,
		lastExit:       r.lastExit,

		origStdout: r.origStdout, // used for process substitutions
	}
	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)
	r2.alias = maps.Clone(r.alias)

	r2.dirStack = append(r2.dirBootstrap[:0], r.dirStack...)
	r2.fillExpandConfig(r.ectx)
	r2.didReset = true
	return r2
}
07070100000031000081A4000000000000000000000001686AE5C000006507000000000000000000000000000000000000001C00000000sh-3.12.0/interp/builtin.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp

import (
	"bufio"
	"bytes"
	"cmp"
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"syscall"
	"time"

	"golang.org/x/term"

	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/syntax"
)

// IsBuiltin returns true if the given word is a shell builtin.
func IsBuiltin(name string) bool {
	switch name {
	case ":", "true", "false", "exit", "set", "shift", "unset",
		"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", "mapfile", "readarray", "shopt":
		return true
	}
	return false
}

// TODO: atoi is duplicated in the expand package.

// 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
}

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 "false":
		exit.code = 1
	case "exit":
		switch len(args) {
		case 0:
			exit = r.lastExit
		case 1:
			n, err := strconv.Atoi(args[0])
			if err != nil {
				return failf(2, "invalid exit status code: %q\n", args[0])
			}
			exit.code = uint8(n)
		default:
			return failf(1, "exit cannot take multiple arguments\n")
		}
		exit.exiting = true
	case "set":
		if err := Params(args...)(r); err != nil {
			return failf(2, "set: %v\n", err)
		}
		r.updateExpandOpts()
	case "shift":
		n := 1
		switch len(args) {
		case 0:
		case 1:
			if n2, err := strconv.Atoi(args[0]); err == nil {
				n = n2
				break
			}
			fallthrough
		default:
			return failf(2, "usage: shift [n]\n")
		}
		if n >= len(r.Params) {
			r.Params = nil
		} else {
			r.Params = r.Params[n:]
		}
	case "unset":
		vars := true
		funcs := true
	unsetOpts:
		for i, arg := range args {
			switch arg {
			case "-v":
				funcs = false
			case "-f":
				vars = false
			default:
				args = args[i:]
				break unsetOpts
			}
		}

		for _, arg := range args {
			if vars && r.lookupVar(arg).IsSet() {
				r.delVar(arg)
			} else if _, ok := r.Funcs[arg]; ok && funcs {
				delete(r.Funcs, arg)
			}
		}
	case "echo":
		newline, doExpand := true, false
	echoOpts:
		for len(args) > 0 {
			switch args[0] {
			case "-n":
				newline = false
			case "-e":
				doExpand = true
			case "-E": // default
			default:
				break echoOpts
			}
			args = args[1:]
		}
		for i, arg := range args {
			if i > 0 {
				r.out(" ")
			}
			if doExpand {
				arg, _, _ = expand.Format(r.ecfg, arg, nil)
			}
			r.out(arg)
		}
		if newline {
			r.out("\n")
		}
	case "printf":
		if len(args) == 0 {
			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 {
				return failf(1, "%v\n", err)
			}
			r.out(s)
			args = args[n:]
			if n == 0 || len(args) == 0 {
				break
			}
		}
	case "break", "continue":
		if !r.inLoop {
			return failf(0, "%s is only useful in a loop\n", name)
		}
		enclosing := &r.breakEnclosing
		if name == "continue" {
			enclosing = &r.contnEnclosing
		}
		switch len(args) {
		case 0:
			*enclosing = 1
		case 1:
			if n, err := strconv.Atoi(args[0]); err == nil {
				*enclosing = n
				break
			}
			fallthrough
		default:
			return failf(2, "usage: %s [n]\n", name)
		}
	case "pwd":
		evalSymlinks := false
		for len(args) > 0 {
			switch args[0] {
			case "-L":
				evalSymlinks = false
			case "-P":
				evalSymlinks = true
			default:
				return failf(2, "invalid option: %q\n", args[0])
			}
			args = args[1:]
		}
		pwd := r.envGet("PWD")
		if evalSymlinks {
			var err error
			pwd, err = filepath.EvalSymlinks(pwd)
			if err != nil {
				exit.fatal(err) // perhaps overly dramatic?
				return exit
			}
		}
		r.outf("%s\n", pwd)
	case "cd":
		var path string
		switch len(args) {
		case 0:
			path = r.envGet("HOME")
		case 1:
			path = args[0]

			// replicate the commonly implemented behavior of `cd -`
			// ref: https://www.man7.org/linux/man-pages/man1/cd.1p.html#OPERANDS
			if path == "-" {
				path = r.envGet("OLDPWD")
				r.outf("%s\n", path)
			}
		default:
			return failf(2, "usage: cd [dir]\n")
		}
		exit.code = r.changeDir(ctx, path)
	case "wait":
		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
		}
		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]) {
			exit.code = 1
			return exit
		}
		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":
				return failf(3, "command: NOT IMPLEMENTED\n")
			case "-p", "-t":
				mode = flag
			default:
				return failf(2, "command: invalid option %q\n", flag)
			}
		}
		args := fp.args()
		for _, arg := range args {
			if mode == "-p" {
				if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {
					r.outf("%s\n", path)
				} else {
					anyNotFound = true
				}
				continue
			}
			if syntax.IsKeyword(arg) {
				if mode == "-t" {
					r.out("keyword\n")
				} else {
					r.outf("%s is a shell keyword\n", arg)
				}
				continue
			}
			if als, ok := r.alias[arg]; ok && r.opts[optExpandAliases] {
				var buf bytes.Buffer
				if len(als.args) > 0 {
					printer := syntax.NewPrinter()
					printer.Print(&buf, &syntax.CallExpr{
						Args: als.args,
					})
				}
				if als.blank {
					buf.WriteByte(' ')
				}
				if mode == "-t" {
					r.out("alias\n")
				} else {
					r.outf("%s is aliased to `%s'\n", arg, &buf)
				}
				continue
			}
			if _, ok := r.Funcs[arg]; ok {
				if mode == "-t" {
					r.out("function\n")
				} else {
					r.outf("%s is a function\n", arg)
				}
				continue
			}
			if IsBuiltin(arg) {
				if mode == "-t" {
					r.out("builtin\n")
				} else {
					r.outf("%s is a shell builtin\n", arg)
				}
				continue
			}
			if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {
				if mode == "-t" {
					r.out("file\n")
				} else {
					r.outf("%s is %s\n", arg, path)
				}
				continue
			}
			if mode != "-t" {
				r.errf("type: %s: not found\n", arg)
			}
			anyNotFound = true
		}
		if anyNotFound {
			exit.code = 1
		}
	case "eval":
		src := strings.Join(args, " ")
		p := syntax.NewParser()
		file, err := p.Parse(strings.NewReader(src), "")
		if err != nil {
			return failf(1, "eval: %v\n", err)
		}
		r.stmts(ctx, file.Stmts)
		exit = r.exit
	case "source", ".":
		if len(args) < 1 {
			return failf(2, "%v: source: need filename\n", pos)
		}
		path, err := scriptFromPathDir(r.Dir, r.writeEnv, args[0])
		if err != nil {
			// If the script was not found in PATH or there was any error, pass
			// the source path to the open handler so it has a chance to look
			// at files it manages (eg: virtual filesystem), and also allow
			// it to look for the sourced script in the current directory.
			path = args[0]
		}
		f, err := r.open(ctx, path, os.O_RDONLY, 0, false)
		if err != nil {
			return failf(1, "source: %v\n", err)
		}
		defer f.Close()
		p := syntax.NewParser()
		file, err := p.Parse(f, path)
		if err != nil {
			return failf(1, "source: %v\n", err)
		}

		// Keep the current versions of some fields we might modify.
		oldParams := r.Params
		oldSourceSetParams := r.sourceSetParams
		oldInSource := r.inSource

		// If we run "source file args...", set said args as parameters.
		// Otherwise, keep the current parameters.
		sourceArgs := len(args[1:]) > 0
		if sourceArgs {
			r.Params = args[1:]
			r.sourceSetParams = false
		}
		// We want to track if the sourced file explicitly sets the
		// parameters.
		r.sourceSetParams = false
		r.inSource = true // know that we're inside a sourced script.
		r.stmts(ctx, file.Stmts)

		// If we modified the parameters and the sourced file didn't
		// explicitly set them, we restore the old ones.
		if sourceArgs && !r.sourceSetParams {
			r.Params = oldParams
		}
		r.sourceSetParams = oldSourceSetParams
		r.inSource = oldInSource

		exit = r.exit
		exit.returning = false
	case "[":
		if len(args) == 0 || args[len(args)-1] != "]" {
			return failf(2, "%v: [: missing matching ]\n", pos)
		}
		args = args[:len(args)-1]
		fallthrough
	case "test":
		parseErr := false
		p := testParser{
			rem: args,
			err: func(err error) {
				r.errf("%v: %v\n", pos, err)
				parseErr = true
			},
		}
		p.next()
		expr := p.classicTest("[", false)
		if parseErr {
			exit.code = 2
			return exit
		}
		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,
		// but in practice it would kill the entire Go process
		// and it's not available on Windows.
		if len(args) == 0 {
			r.keepRedirs = true
			break
		}
		r.exit.exiting = true
		r.exec(ctx, pos, args)
		exit = r.exit
	case "command":
		show := false
		fp := flagParser{remaining: args}
		for fp.more() {
			switch flag := fp.flag(); flag {
			case "-v":
				show = true
			default:
				return failf(2, "command: invalid option %q\n", flag)
			}
		}
		args := fp.args()
		if len(args) == 0 {
			break
		}
		if !show {
			if IsBuiltin(args[0]) {
				return r.builtin(ctx, pos, args[0], args[1:])
			}
			r.exec(ctx, pos, args)
			exit = r.exit
			return exit
		}
		last := uint8(0)
		for _, arg := range args {
			last = 0
			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)
			} else {
				last = 1
			}
		}
		exit.code = last
	case "dirs":
		for i, dir := range slices.Backward(r.dirStack) {
			r.outf("%s", dir)
			if i > 0 {
				r.out(" ")
			}
		}
		r.out("\n")
	case "pushd":
		change := true
		if len(args) > 0 && args[0] == "-n" {
			change = false
			args = args[1:]
		}
		swap := func() string {
			oldtop := r.dirStack[len(r.dirStack)-1]
			top := r.dirStack[len(r.dirStack)-2]
			r.dirStack[len(r.dirStack)-1] = top
			r.dirStack[len(r.dirStack)-2] = oldtop
			return top
		}
		switch len(args) {
		case 0:
			if !change {
				break
			}
			if len(r.dirStack) < 2 {
				return failf(1, "pushd: no other directory\n")
			}
			newtop := swap()
			if code := r.changeDir(ctx, newtop); code != 0 {
				exit.code = code
				return exit
			}
			r.builtin(ctx, syntax.Pos{}, "dirs", nil)
		case 1:
			if change {
				if code := r.changeDir(ctx, 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.builtin(ctx, syntax.Pos{}, "dirs", nil)
		default:
			return failf(2, "pushd: too many arguments\n")
		}
	case "popd":
		change := true
		if len(args) > 0 && args[0] == "-n" {
			change = false
			args = args[1:]
		}
		switch len(args) {
		case 0:
			if len(r.dirStack) < 2 {
				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 {
					exit.code = code
					return exit
				}
			} else {
				r.dirStack[len(r.dirStack)-1] = oldtop
			}
			r.builtin(ctx, syntax.Pos{}, "dirs", nil)
		default:
			return failf(2, "popd: invalid argument\n")
		}
	case "return":
		if !r.inFunc && !r.inSource {
			return failf(1, "return: can only be done from a func or sourced script\n")
		}
		switch len(args) {
		case 0:
		case 1:
			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:
			return failf(2, "return: too many arguments\n")
		}
		exit.returning = true
	case "read":
		var prompt string
		raw := false
		silent := false
		fp := flagParser{remaining: args}
		for fp.more() {
			switch flag := fp.flag(); flag {
			case "-s":
				silent = true
			case "-r":
				raw = true
			case "-p":
				prompt = fp.value()
				if prompt == "" {
					return failf(2, "read: -p: option requires an argument\n")
				}
			default:
				return failf(2, "read: invalid option %q\n", flag)
			}
		}

		args := fp.args()
		for _, name := range args {
			if !syntax.ValidName(name) {
				return failf(2, "read: invalid identifier %q\n", name)
			}
		}

		if prompt != "" {
			r.out(prompt)
		}

		var line []byte
		var err error
		if silent {
			// Note that on Windows, syscall.Stdin is of type uintptr.
			line, err = term.ReadPassword(int(syscall.Stdin))
		} else {
			line, err = r.readLine(ctx, raw)
		}
		if len(args) == 0 {
			args = append(args, shellReplyVar)
		}

		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)
		}

		// 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 {
			return failf(2, "getopts: usage: getopts optstring name [arg ...]\n")
		}
		optind, _ := strconv.Atoi(r.envGet("OPTIND"))
		if optind-1 != r.optState.argidx {
			if optind < 1 {
				optind = 1
			}
			r.optState = getopts{argidx: optind - 1}
		}
		optstr := args[0]
		name := args[1]
		if !syntax.ValidName(name) {
			return failf(2, "getopts: invalid identifier: %q\n", name)
		}
		args = args[2:]
		if len(args) == 0 {
			args = r.Params
		}
		diagnostics := !strings.HasPrefix(optstr, ":")

		opt, optarg, done := r.optState.next(optstr, args)

		r.setVarString(name, string(opt))
		r.delVar("OPTARG")
		switch {
		case opt == '?' && diagnostics && !done:
			r.errf("getopts: illegal option -- %q\n", optarg)
		case opt == ':' && diagnostics:
			r.errf("getopts: option requires an argument -- %q\n", optarg)
		default:
			if optarg != "" {
				r.setVarString("OPTARG", optarg)
			}
		}
		if optind-1 != r.optState.argidx {
			r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10))
		}

		exit.oneIf(done)

	case "shopt":
		mode := ""
		posixOpts := false
		fp := flagParser{remaining: args}
		for fp.more() {
			switch flag := fp.flag(); flag {
			case "-s", "-u":
				mode = flag
			case "-o":
				posixOpts = true
			case "-p", "-q":
				panic(fmt.Sprintf("unhandled shopt flag: %s", flag))
			default:
				return failf(2, "shopt: invalid option %q\n", flag)
			}
		}
		args := fp.args()
		bash := !posixOpts
		if len(args) == 0 {
			if bash {
				for i, opt := range bashOptsTable {
					r.printOptLine(opt.name, r.opts[len(shellOptsTable)+i], opt.supported)
				}
				break
			}
			for i, opt := range &shellOptsTable {
				r.printOptLine(opt.name, r.opts[i], true)
			}
			break
		}
		for _, arg := range args {
			i, opt := r.optByName(arg, bash)
			if opt == nil {
				return failf(1, "shopt: invalid option name %q\n", arg)
			}

			var (
				bo        *bashOpt
				supported = true // default for shell options
			)
			if bash {
				bo = &bashOptsTable[i-len(shellOptsTable)]
				supported = bo.supported
			}

			switch mode {
			case "-s", "-u":
				if bash && !supported {
					return failf(1, "shopt: invalid option name %q %q (%q not supported)\n", arg, r.optStatusText(bo.defaultState), r.optStatusText(!bo.defaultState))
				}
				*opt = mode == "-s"
			default: // ""
				r.printOptLine(arg, *opt, supported)
			}
		}
		r.updateExpandOpts()

	case "alias":
		show := func(name string, als alias) {
			var buf bytes.Buffer
			if len(als.args) > 0 {
				printer := syntax.NewPrinter()
				printer.Print(&buf, &syntax.CallExpr{
					Args: als.args,
				})
			}
			if als.blank {
				buf.WriteByte(' ')
			}
			r.outf("alias %s='%s'\n", name, &buf)
		}

		if len(args) == 0 {
			for name, als := range r.alias {
				show(name, als)
			}
		}
	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)
					continue
				}
				show(name, als)
				continue
			}

			// TODO: parse any CallExpr perhaps, or even any Stmt
			parser := syntax.NewParser()
			var words []*syntax.Word
			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)
			}

			if r.alias == nil {
				r.alias = make(map[string]alias)
			}
			r.alias[name] = alias{
				args:  words,
				blank: strings.TrimRight(src, " \t") != src,
			}
		}
	case "unalias":
		for _, name := range args {
			delete(r.alias, name)
		}

	case "trap":
		fp := flagParser{remaining: args}
		callback := "-"
		for fp.more() {
			switch flag := fp.flag(); flag {
			case "-l", "-p":
				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")
				exit.code = 2
				return exit
			}
		}
		args := fp.args()
		switch len(args) {
		case 0:
			// Print non-default signals
			if r.callbackExit != "" {
				r.outf("trap -- %q EXIT\n", r.callbackExit)
			}
			if r.callbackErr != "" {
				r.outf("trap -- %q ERR\n", r.callbackErr)
			}
		case 1:
			// assume it's a signal, the default will be restored
		default:
			callback = args[0]
			args = args[1:]
		}
		// For now, treat both empty and - the same since ERR and EXIT have no
		// default callback.
		if callback == "-" {
			callback = ""
		}
		for _, arg := range args {
			switch arg {
			case "ERR":
				r.callbackErr = callback
			case "EXIT":
				r.callbackExit = callback
			default:
				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",
		return failf(2, "%s: unimplemented builtin\n", name)
	}
	return exit
}

// mapfileSplit returns a suitable Split function for a [bufio.Scanner];
// the code is mostly stolen from [bufio.ScanLines].
func mapfileSplit(delim byte, dropDelim bool) bufio.SplitFunc {
	return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
		if atEOF && len(data) == 0 {
			return 0, nil, nil
		}
		if i := bytes.IndexByte(data, delim); i >= 0 {
			// We have a full newline-terminated line.
			if dropDelim {
				return i + 1, data[0:i], nil
			} else {
				return i + 1, data[0 : i+1], nil
			}
		}
		// If we're at EOF, we have a final, non-terminated line. Return it.
		if atEOF {
			return len(data), data, nil
		}
		// Request more data.
		return 0, nil, nil
	}
}

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\t(%q not supported)\n", name, state, r.optStatusText(!enabled))
}

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")
	}

	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[:])
		if n > 0 {
			b := buf[0]
			switch {
			case !raw && b == '\\':
				line = append(line, b)
				esc = !esc
			case !raw && b == '\n' && esc:
				// line continuation
				line = line[len(line)-1:]
				esc = false
			case b == '\n':
				return line, nil
			default:
				line = append(line, b)
				esc = false
			}
		}
		if err != nil {
			return line, err
		}
	}
}

func (r *Runner) changeDir(ctx context.Context, path string) uint8 {
	path = cmp.Or(path, ".")
	path = r.absPath(path)
	info, err := r.stat(ctx, path)
	if err != nil || !info.IsDir() {
		return 1
	}
	if r.access(ctx, path, access_X_OK) != nil {
		return 1
	}
	r.Dir = path
	r.setVarString("OLDPWD", r.envGet("PWD"))
	r.setVarString("PWD", path)
	return 0
}

func absPath(dir, path string) string {
	if path == "" {
		return ""
	}
	if !filepath.IsAbs(path) {
		path = filepath.Join(dir, path)
	}
	return filepath.Clean(path) // TODO: this clean is likely unnecessary
}

func (r *Runner) absPath(path string) string {
	return absPath(r.Dir, path)
}

// flagParser is used to parse builtin flags.
//
// It's similar to the getopts implementation, but with some key differences.
// First, the API is designed for Go loops, making it easier to use directly.
// Second, it doesn't require the awkward ":ab" syntax that getopts uses.
// Third, it supports "-a" flags as well as "+a".
type flagParser struct {
	current   string
	remaining []string
}

func (p *flagParser) more() bool {
	if p.current != "" {
		// We're still parsing part of "-ab".
		return true
	}
	if len(p.remaining) == 0 {
		// Nothing left.
		p.remaining = nil
		return false
	}
	arg := p.remaining[0]
	if arg == "--" {
		// We explicitly stop parsing flags.
		p.remaining = p.remaining[1:]
		return false
	}
	if len(arg) == 0 || (arg[0] != '-' && arg[0] != '+') {
		// The next argument is not a flag.
		return false
	}
	// More flags to come.
	return true
}

func (p *flagParser) flag() string {
	arg := p.current
	if arg == "" {
		arg = p.remaining[0]
		p.remaining = p.remaining[1:]
	} else {
		p.current = ""
	}
	if len(arg) > 2 {
		// We have "-ab", so return "-a" and keep "-b".
		p.current = arg[:1] + arg[2:]
		arg = arg[:2]
	}
	return arg
}

func (p *flagParser) value() string {
	if len(p.remaining) == 0 {
		return ""
	}
	arg := p.remaining[0]
	p.remaining = p.remaining[1:]
	return arg
}

func (p *flagParser) args() []string { return p.remaining }

type getopts struct {
	argidx  int
	runeidx int
}

func (g *getopts) next(optstr string, args []string) (opt rune, optarg string, done bool) {
	if len(args) == 0 || g.argidx >= len(args) {
		return '?', "", true
	}
	arg := []rune(args[g.argidx])
	if len(arg) < 2 || arg[0] != '-' || arg[1] == '-' {
		return '?', "", true
	}

	opts := arg[1:]
	opt = opts[g.runeidx]
	if g.runeidx+1 < len(opts) {
		g.runeidx++
	} else {
		g.argidx++
		g.runeidx = 0
	}

	i := strings.IndexRune(optstr, opt)
	if i < 0 {
		// invalid option
		return '?', string(opt), false
	}

	if i+1 < len(optstr) && optstr[i+1] == ':' {
		if g.argidx >= len(args) {
			// missing argument
			return ':', string(opt), false
		}
		optarg = args[g.argidx]
		g.argidx++
		g.runeidx = 0
	}

	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"
}
07070100000032000081A4000000000000000000000001686AE5C000000A87000000000000000000000000000000000000002100000000sh-3.12.0/interp/example_test.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp_test

import (
	"context"
	"fmt"
	"io"
	"os"
	"strings"

	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/syntax"
)

func Example() {
	src := `
		foo=abc
		for i in 1 2 3; do
			foo+=$i
		done
		let bar=(2 + 3)
		echo $foo $bar
		echo $GLOBAL
	`
	file, _ := syntax.NewParser().Parse(strings.NewReader(src), "")
	runner, _ := interp.New(
		// Use [interp.Interactive] to enable interactive shell defaults like expanding aliases.
		interp.Env(expand.ListEnviron("GLOBAL=global_value")),
		interp.StdIO(nil, os.Stdout, os.Stdout),
	)
	runner.Run(context.TODO(), file)
	// Output:
	// abc123 5
	// global_value
}

func ExampleExecHandlers() {
	src := "echo foo; join ! foo bar baz; missing-program bar"
	file, _ := syntax.NewParser().Parse(strings.NewReader(src), "")

	execJoin := func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
		return func(ctx context.Context, args []string) error {
			hc := interp.HandlerCtx(ctx)
			if args[0] == "join" {
				fmt.Fprintln(hc.Stdout, strings.Join(args[2:], args[1]))
				return nil
			}
			return next(ctx, args)
		}
	}
	execNotInstalled := func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
		return func(ctx context.Context, args []string) error {
			hc := interp.HandlerCtx(ctx)
			if _, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]); err != nil {
				fmt.Printf("%s is not installed\n", args[0])
				return interp.ExitStatus(1)
			}
			return next(ctx, args)
		}
	}
	runner, _ := interp.New(
		interp.StdIO(nil, os.Stdout, os.Stdout),
		interp.ExecHandlers(execJoin, execNotInstalled),
	)
	runner.Run(context.TODO(), file)
	// Output:
	// foo
	// foo!bar!baz
	// missing-program is not installed
}

type nopWriterCloser struct {
	*strings.Reader
}

func (nopWriterCloser) Write([]byte) (int, error) { return 0, io.EOF }
func (nopWriterCloser) Close() error              { return nil }

func ExampleOpenHandler() {
	src := "echo $(</etc/hostname); echo bar >/dev/null"
	file, _ := syntax.NewParser().Parse(strings.NewReader(src), "")

	open := func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
		// Hard-code the contents of the hostname file for all platforms.
		if path == "/etc/hostname" {
			return nopWriterCloser{strings.NewReader("mymachine")}, nil
		}
		// DefaultOpenHandler already redirects /dev/null to NUL on Windows.
		return interp.DefaultOpenHandler()(ctx, path, flag, perm)
	}
	runner, _ := interp.New(
		interp.StdIO(nil, os.Stdout, os.Stdout),
		interp.OpenHandler(open),
	)
	runner.Run(context.TODO(), file)
	// Output:
	// mymachine
}
07070100000033000081A4000000000000000000000001686AE5C000003098000000000000000000000000000000000000001C00000000sh-3.12.0/interp/handler.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp

import (
	"context"
	"fmt"
	"io"
	"io/fs"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/syntax"
)

// HandlerCtx returns HandlerContext value stored in ctx.
// It panics if ctx has no HandlerContext stored.
func HandlerCtx(ctx context.Context) HandlerContext {
	hc, ok := ctx.Value(handlerCtxKey{}).(HandlerContext)
	if !ok {
		panic("interp.HandlerCtx: no HandlerContext in ctx")
	}
	return hc
}

type handlerCtxKey struct{}

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.
	Env expand.Environ

	// 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
	// Stderr is the interpreter's current standard error writer.
	Stderr io.Writer
}

// CallHandlerFunc is a handler which runs on every [syntax.CallExpr].
// It is called once variable assignments and field expansion have occurred.
// The call's arguments are replaced by what the handler returns,
// and then the call is executed by the Runner as usual.
// At this time, returning an empty slice without an error is not supported.
//
// This handler is similar to [ExecHandlerFunc], but has two major differences:
//
// First, it runs for all simple commands, including function calls and builtins.
//
// Second, it is not expected to execute the simple command, but instead to
// 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] 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 [syntax.CallExpr] nodes
// where the first argument is neither a declared function nor a builtin.
//
// Returning a nil error means a zero exit status.
// Other exit statuses can be set 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.
// 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.
// A negative value means that a kill signal will be sent immediately.
//
// On Windows, the kill signal is always sent immediately,
// because Go doesn't currently support sending Interrupt on Windows.
// [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 ExitStatus(127)
		}
		cmd := exec.Cmd{
			Path:   path,
			Args:   args,
			Env:    execEnv(hc.Env),
			Dir:    hc.Dir,
			Stdin:  hc.Stdin,
			Stdout: hc.Stdout,
			Stderr: hc.Stderr,
		}

		err = cmd.Start()
		if err == nil {
			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 err := err.(type) {
		case *exec.ExitError:
			// 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 ExitStatus(128 + status.Signal())
			}
			return ExitStatus(err.ExitCode())
		case *exec.Error:
			// did not start
			fmt.Fprintf(hc.Stderr, "%v\n", err)
			return ExitStatus(127)
		default:
			return err
		}
	}
}

func checkStat(dir, file string, checkExec bool) (string, error) {
	if !filepath.IsAbs(file) {
		file = filepath.Join(dir, file)
	}
	info, err := os.Stat(file)
	if err != nil {
		return "", err
	}
	m := info.Mode()
	if m.IsDir() {
		return "", fmt.Errorf("is a directory")
	}
	if checkExec && runtime.GOOS != "windows" && m&0o111 == 0 {
		return "", fmt.Errorf("permission denied")
	}
	return file, nil
}

func winHasExt(file string) bool {
	i := strings.LastIndex(file, ".")
	if i < 0 {
		return false
	}
	return strings.LastIndexAny(file, `:\/`) < i
}

// findExecutable returns the path to an existing executable file.
func findExecutable(dir, file string, exts []string) (string, error) {
	if len(exts) == 0 {
		// non-windows
		return checkStat(dir, file, true)
	}
	if winHasExt(file) {
		if file, err := checkStat(dir, file, true); err == nil {
			return file, nil
		}
	}
	for _, e := range exts {
		f := file + e
		if f, err := checkStat(dir, f, true); err == nil {
			return f, nil
		}
	}
	return "", fmt.Errorf("not found")
}

// findFile returns the path to an existing file.
func findFile(dir, file string, _ []string) (string, error) {
	return checkStat(dir, file, false)
}

// 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
// provided environment. env is used to fetch relevant environment variables
// such as PWD and PATH.
//
// If no error is returned, the returned path must be valid.
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].
type findAny = func(dir string, file string, exts []string) (string, error)

func lookPathDir(cwd string, env expand.Environ, file string, find findAny) (string, error) {
	if find == nil {
		panic("no find function found")
	}

	pathList := filepath.SplitList(env.Get("PATH").String())
	if len(pathList) == 0 {
		pathList = []string{""}
	}
	chars := `/`
	if runtime.GOOS == "windows" {
		chars = `:\/`
	}
	exts := pathExts(env)
	if strings.ContainsAny(file, chars) {
		return find(cwd, file, exts)
	}
	for _, elem := range pathList {
		var path string
		switch elem {
		case "", ".":
			// otherwise "foo" won't be "./foo"
			path = "." + string(filepath.Separator) + file
		default:
			path = filepath.Join(elem, file)
		}
		if f, err := find(cwd, path, exts); err == nil {
			return f, nil
		}
	}
	return "", fmt.Errorf("%q: executable file not found in $PATH", file)
}

// 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)
}

func pathExts(env expand.Environ) []string {
	if runtime.GOOS != "windows" {
		return nil
	}
	pathext := env.Get("PATHEXT").String()
	if pathext == "" {
		return []string{".com", ".exe", ".bat", ".cmd"}
	}
	var exts []string
	for _, e := range strings.Split(strings.ToLower(pathext), `;`) {
		if e == "" {
			continue
		}
		if e[0] != '.' {
			e = "." + e
		}
		exts = append(exts, e)
	}
	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, except for named pipes created by process substitutions.
// Files opened by executed programs are not included.
//
// The path parameter may be relative to the current directory,
// which can be fetched via [HandlerCtx].
//
// Use a return error of type [*os.PathError] to have the error printed to
// stderr and the exit status set to 1.
// Any other error will halt the [Runner] and will be returned via the API.
//
// Note that implementations which do not return [os.File] will cause
// extra files and goroutines for input redirections; see [StdIO].
type OpenHandlerFunc func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)

// 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 runtime.GOOS == "windows" && path == "/dev/null" {
			path = "NUL"
			// Work around https://go.dev/issue/71752, where Go 1.24 started giving
			// "Invalid handle" errors when opening "NUL" with O_TRUNC.
			// TODO: hopefully remove this in the future once the bug is fixed.
			flag &^= os.O_TRUNC
		} else if path != "" && !filepath.IsAbs(path) {
			path = filepath.Join(mc.Dir, path)
		}
		return os.OpenFile(path, flag, perm)
	}
}

// TODO(v4): if this is kept in v4, it most likely needs to use [io/fs.DirEntry] for efficiency

// ReadDirHandlerFunc is a handler which reads directories. It is called during
// shell globbing, if enabled.
//
// Deprecated: use [ReadDirHandlerFunc2], which uses [fs.DirEntry].
type ReadDirHandlerFunc func(ctx context.Context, path string) ([]fs.FileInfo, error)

// ReadDirHandlerFunc2 is a handler which reads directories. It is called during
// shell globbing, if enabled.
type ReadDirHandlerFunc2 func(ctx context.Context, path string) ([]fs.DirEntry, error)

// DefaultReadDirHandler returns the [ReadDirHandlerFunc] used by default.
// It makes use of [ioutil.ReadDir].
func DefaultReadDirHandler() ReadDirHandlerFunc {
	return func(ctx context.Context, path string) ([]fs.FileInfo, error) {
		return ioutil.ReadDir(path)
	}
}

// DefaultReadDirHandler2 returns the [ReadDirHandlerFunc2] used by default.
// It uses [os.ReadDir].
func DefaultReadDirHandler2() ReadDirHandlerFunc2 {
	return func(ctx context.Context, path string) ([]fs.DirEntry, error) {
		return os.ReadDir(path)
	}
}

// StatHandlerFunc is a handler which gets a file's information.
type StatHandlerFunc func(ctx context.Context, name string, followSymlinks bool) (fs.FileInfo, error)

// DefaultStatHandler returns 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) (fs.FileInfo, error) {
		if !followSymlinks {
			return os.Lstat(path)
		} else {
			return os.Stat(path)
		}
	}
}
07070100000034000081A4000000000000000000000001686AE5C000003BF3000000000000000000000000000000000000002100000000sh-3.12.0/interp/handler_test.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp_test

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"testing"
	"time"

	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/syntax"
)

func blocklistOneExec(name string) func(interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
		return func(ctx context.Context, args []string) error {
			if args[0] == name {
				return fmt.Errorf("%s: blocklisted program", name)
			}
			return next(ctx, args)
		}
	}
}

func blocklistAllExec(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		return fmt.Errorf("blocklisted: %s", args[0])
	}
}

func blocklistNondevOpen(ctx context.Context, path string, flags int, mode os.FileMode) (io.ReadWriteCloser, error) {
	if path != "/dev/null" {
		return nil, fmt.Errorf("non-dev: %s", path)
	}

	return interp.DefaultOpenHandler()(ctx, path, flags, mode)
}

func mockFileOpen(ctx context.Context, path string, flags int, mode os.FileMode) (io.ReadWriteCloser, error) {
	return nopWriterCloser{strings.NewReader(fmt.Sprintf("body of %s", path))}, nil
}

func blocklistGlob(ctx context.Context, path string) ([]fs.FileInfo, error) {
	return nil, fmt.Errorf("blocklisted: glob")
}

func execPrint(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		hc := interp.HandlerCtx(ctx)
		fmt.Fprintf(hc.Stdout, "would run: %s\n", args)
		return nil
	}
}

func execExitStatus5(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		return interp.ExitStatus(5)
	}
}

func execCustomError(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		return fmt.Errorf("custom error")
	}
}

func execCustomExitStatus5(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		return fmt.Errorf("custom error: %w", interp.ExitStatus(5))
	}
}

func execDotRunnerBuiltin(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		if name, ok := strings.CutPrefix(args[0], "."); ok {
			hc := interp.HandlerCtx(ctx)
			args[0] = name
			err := hc.Builtin(ctx, args)
			fmt.Fprintf(hc.Stdout, "ran builtin: %s\n", args)
			return err
		}
		return next(ctx, args)
	}
}

// runnerCtx allows us to give handler functions access to the Runner, if needed.
var runnerCtx = new(int)

func execPrintWouldExec(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		runner, ok := ctx.Value(runnerCtx).(*interp.Runner)
		if ok && runner.Exited() {
			return fmt.Errorf("would exec via builtin: %s", args)
		}
		return nil
	}
}

// TODO: join with TestRunnerOpts?
var modCases = []struct {
	name string
	opts []interp.RunnerOption
	src  string
	want string
}{
	{
		name: "ExecBlocklistOne",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(blocklistOneExec("sleep")),
		},
		src:  "echo foo; sleep 1",
		want: "foo\nRunner.Run error: sleep: blocklisted program",
	},
	{
		name: "ExecBlocklistOneSubshell",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(
				blocklistOneExec("faa"),
				testExecHandler, // sed is used below
			),
		},
		src:  "a=$(echo foo | sed 's/o/a/g'); echo $a; $a args",
		want: "faa\nRunner.Run error: faa: blocklisted program",
	},
	{
		name: "ExecBlocklistAllSubshell",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(blocklistAllExec),
		},
		src:  "(malicious)",
		want: "Runner.Run error: blocklisted: malicious",
	},
	{
		name: "ExecPipe",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(blocklistAllExec),
		},
		src:  "malicious | echo foo",
		want: "foo\nRunner.Run error: blocklisted: malicious",
	},
	{
		name: "ExecPipeFail",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(blocklistAllExec),
		},
		src:  "set -o pipefail; malicious | echo foo",
		want: "foo\nRunner.Run error: blocklisted: malicious",
	},
	{
		name: "ExecCmdSubst",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(blocklistAllExec),
		},
		src:  "a=$(malicious)",
		want: "blocklisted: malicious\nRunner.Run error: blocklisted: malicious",
	},
	{
		name: "ExecBackground",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(blocklistAllExec),
		},
		src: "{ malicious; true; } & { malicious; true; } & wait",
		// Note that "wait" with no arguments always succeeds.
		want: "",
	},
	{
		name: "ExecPrintWouldExec",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execPrintWouldExec),
		},
		src:  "exec /bin/sh",
		want: "Runner.Run error: would exec via builtin: [/bin/sh]",
	},
	{
		name: "ExecPrintAndBlocklist",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(
				execPrint,
				blocklistOneExec("foo"),
			),
		},
		src:  "foo",
		want: "would run: [foo]\n",
	},
	{
		name: "ExecPrintAndBlocklistSeparate",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execPrint),
			interp.ExecHandlers(blocklistOneExec("foo")),
		},
		src:  "foo",
		want: "would run: [foo]\n",
	},
	{
		name: "ExecBlocklistAndPrint",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(
				blocklistOneExec("foo"),
				execPrint,
			),
		},
		src:  "foo",
		want: "Runner.Run error: foo: blocklisted program",
	},
	{
		name: "ExecExitStatus5",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execExitStatus5),
		},
		src:  "foo",
		want: "Runner.Run error: exit status 5",
	},
	{
		name: "ExecCustomError",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomError),
		},
		src:  "foo",
		want: "Runner.Run error: custom error",
	},
	{
		name: "ExecCustomErrorContinuation",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomError),
		},
		src:  "foo; echo next",
		want: "Runner.Run error: custom error",
	},
	{
		name: "ExecCustomExitStatus5",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "foo",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecCustomExitStatus5Continuation",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "foo; echo next",
		want: "next\n",
	},
	{
		name: "ExecCustomExitStatus5Pipefail",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "set -o pipefail; foo | true",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecCustomExitStatus5ErrExit",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "set -o errexit; foo",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecCustomExitStatus5Exec",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "exec foo; echo never-run",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecCustomExitStatus5Negated",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "! foo; echo custom-error-wiped; exit 1",
		want: "custom-error-wiped\nRunner.Run error: exit status 1",
	},
	{
		name: "ExecCustomExitStatus5CmdSubst",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "x=$(foo)",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecCustomExitStatus5Subshell",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "(foo)",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecCustomExitStatus5Wait",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "foo & bg=$!; wait $bg",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecCustomExitStatus5Exit",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "foo; exit",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecCustomExitStatus5Source",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execCustomExitStatus5),
		},
		src:  "echo 'foo' >a; source a",
		want: "Runner.Run error: custom error: exit status 5",
	},
	{
		name: "ExecDotRunnerBuiltin",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execDotRunnerBuiltin, execExitStatus5),
		},
		src:  ".true; foo; echo $?; .false",
		want: "ran builtin: [true]\n5\nran builtin: [false]\nRunner.Run error: exit status 1",
	},
	{
		name: "ExecDotRunnerBuiltinExiting",
		opts: []interp.RunnerOption{
			interp.ExecHandlers(execDotRunnerBuiltin, execExitStatus5),
		},
		src:  "echo before; .exit 0; echo after",
		want: "before\nran builtin: [exit 0]\n",
	},
	{
		name: "NonExecBuiltin",
		opts: []interp.RunnerOption{
			interp.CallHandler(func(ctx context.Context, args []string) ([]string, error) {
				hc := interp.HandlerCtx(ctx)
				err := hc.Builtin(ctx, append([]string{"echo"}, args...))
				return nil, err
			}),
		},
		src:  "foo; bar",
		want: "Runner.Run error: HandlerContext.Builtin can only be called via an ExecHandlerFunc",
	},
	{
		name: "OpenForbidNonDev",
		opts: []interp.RunnerOption{
			interp.OpenHandler(blocklistNondevOpen),
		},
		src:  "echo foo >/dev/null; echo bar >/tmp/x",
		want: "Runner.Run error: non-dev: /tmp/x",
	},
	{
		name: "OpenMockFile",
		opts: []interp.RunnerOption{
			interp.OpenHandler(mockFileOpen),
		},
		src:  "echo $(<foo); echo $(< <(echo bar))",
		want: "body of foo\nbar\n",
	},
	{
		name: "CallReplaceWithBlank",
		opts: []interp.RunnerOption{
			interp.OpenHandler(blocklistNondevOpen),
			interp.CallHandler(func(ctx context.Context, args []string) ([]string, error) {
				return []string{"echo", "blank"}, nil
			}),
		},
		src:  "echo foo >/dev/null; { bar; } && baz",
		want: "blank\nblank\n",
	},
	{
		name: "CallDryRun",
		opts: []interp.RunnerOption{
			interp.CallHandler(func(ctx context.Context, args []string) ([]string, error) {
				return append([]string{"echo", "run:"}, args...), nil
			}),
		},
		src:  "cd some-dir; cat foo; exit 1",
		want: "run: cd some-dir\nrun: cat foo\nrun: exit 1\n",
	},
	{
		name: "CallError",
		opts: []interp.RunnerOption{
			interp.CallHandler(func(ctx context.Context, args []string) ([]string, error) {
				if args[0] == "echo" && len(args) > 2 {
					return nil, fmt.Errorf("refusing to run echo builtin with multiple args")
				}
				return args, nil
			}),
		},
		src:  "echo foo; echo foo bar",
		want: "foo\nRunner.Run error: refusing to run echo builtin with multiple args",
	},
	{
		name: "GlobForbid",
		opts: []interp.RunnerOption{
			interp.ReadDirHandler(blocklistGlob),
		},
		src:  "echo *",
		want: "blocklisted: glob\n",
	},
}

func TestRunnerHandlers(t *testing.T) {
	t.Parallel()

	p := syntax.NewParser()
	for _, tc := range modCases {
		t.Run(tc.name, func(t *testing.T) {
			skipIfUnsupported(t, tc.src)
			file := parse(t, p, tc.src)
			tdir := t.TempDir()
			var cb concBuffer
			r, err := interp.New(interp.Dir(tdir), interp.StdIO(nil, &cb, &cb))
			if err != nil {
				t.Fatal(err)
			}
			for _, opt := range tc.opts {
				opt(r)
			}
			ctx := context.WithValue(context.Background(), runnerCtx, r)
			if err := r.Run(ctx, file); err != nil {
				fmt.Fprintf(&cb, "Runner.Run error: %v", err)
			}
			got := cb.String()
			if got != tc.want {
				t.Fatalf("want:\n%q\ngot:\n%q", tc.want, got)
			}
		})
	}
}

type readyBuffer struct {
	buf       bytes.Buffer
	seenReady sync.WaitGroup
}

func (b *readyBuffer) Write(p []byte) (n int, err error) {
	if string(p) == "ready\n" {
		b.seenReady.Done()
		return len(p), nil
	}
	return b.buf.Write(p)
}

func TestKillTimeout(t *testing.T) {
	if testing.Short() {
		t.Skip("sleeps and timeouts are slow")
	}
	if runtime.GOOS == "windows" {
		t.Skip("skipping trap tests on windows")
	}
	t.Parallel()

	tests := []struct {
		src         string
		want        string
		killTimeout time.Duration
		forcedKill  bool
	}{
		// killed immediately
		{
			`sh -c "trap 'echo trapped; exit 0' INT; echo ready; for i in \$(seq 1 100); do sleep 0.01; done"`,
			"",
			-1,
			true,
		},
		// interrupted first, and stops itself in time
		{
			`sh -c "trap 'echo trapped; exit 0' INT; echo ready; for i in \$(seq 1 100); do sleep 0.01; done"`,
			"trapped\n",
			time.Second,
			false,
		},
		// interrupted first, but does not stop itself in time
		{
			`sh -c "trap 'echo trapped; for i in \$(seq 1 100); do sleep 0.01; done' INT; echo ready; for i in \$(seq 1 100); do sleep 0.01; done"`,
			"trapped\n",
			20 * time.Millisecond,
			true,
		},
	}

	for _, test := range tests {
		t.Run("", func(t *testing.T) {
			t.Parallel()
			file := parse(t, nil, test.src)
			attempt := 0
			for {
				var rbuf readyBuffer
				rbuf.seenReady.Add(1)
				ctx, cancel := context.WithCancel(context.Background())
				r, err := interp.New(
					interp.StdIO(nil, &rbuf, &rbuf),
					interp.ExecHandler(interp.DefaultExecHandler(test.killTimeout)),
				)
				if err != nil {
					t.Fatal(err)
				}
				go func() {
					rbuf.seenReady.Wait()
					cancel()
				}()
				err = r.Run(ctx, file)
				if test.forcedKill {
					if errors.As(err, new(interp.ExitStatus)) || err == nil {
						t.Error("command was not force-killed")
					}
				} else {
					if err != nil && err != context.Canceled && err != context.DeadlineExceeded {
						t.Errorf("execution errored: %v", err)
					}
				}
				got := rbuf.buf.String()
				if got != test.want {
					if attempt < 3 && got == "" && test.killTimeout > 0 {
						attempt++
						test.killTimeout *= 2
						continue
					}
					t.Fatalf("want:\n%s\ngot:\n%s", test.want, got)
				}
				break
			}
		})
	}
}

func TestKillSignal(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping signal tests on windows")
	}
	tests := []struct {
		signal os.Signal
		want   error
	}{
		{syscall.SIGINT, interp.ExitStatus(130)},  // 128 + 2
		{syscall.SIGKILL, interp.ExitStatus(137)}, // 128 + 9
		{syscall.SIGTERM, interp.ExitStatus(143)}, // 128 + 15
	}

	// pid_and_hang is implemented in TestMain; we use it to have the
	// interpreter spawn a process, and easily grab its PID to send it a
	// signal directly. The program prints its PID and hangs forever.
	file := parse(t, nil, "GOSH_CMD=pid_and_hang $GOSH_PROG")
	for _, test := range tests {
		t.Run(fmt.Sprintf("signal-%d", test.signal), func(t *testing.T) {
			t.Parallel()

			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
			defer cancel()

			outReader, outWriter := io.Pipe()
			stderr := new(bytes.Buffer)
			r, _ := interp.New(interp.StdIO(nil, outWriter, stderr))
			errch := make(chan error, 1)
			go func() {
				errch <- r.Run(ctx, file)
				outWriter.Close()
			}()

			br := bufio.NewReader(outReader)
			line, err := br.ReadString('\n')
			if err != nil {
				t.Fatal(err)
			}
			pid, err := strconv.Atoi(strings.TrimSpace(line))
			if err != nil {
				t.Fatal(err)
			}

			proc, err := os.FindProcess(pid)
			if err != nil {
				t.Fatal(err)
			}
			if err := proc.Signal(test.signal); err != nil {
				t.Fatal(err)
			}
			if got := <-errch; got != test.want {
				t.Fatalf("want error %v, got %v. stderr: %s", test.want, got, stderr)
			}
		})
	}
}
07070100000035000081A4000000000000000000000001686AE5C00001D1C4000000000000000000000000000000000000002000000000sh-3.12.0/interp/interp_test.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp_test

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"math/bits"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"slices"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/go-quicktest/qt"
	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/syntax"
)

// runnerRunTimeout is the context timeout used by any tests calling [Runner.Run].
// The timeout saves us from hangs or burning too much CPU if there are bugs.
// All the test cases are designed to be inexpensive and stop in a very short
// amount of time, so 5s should be plenty even for busy machines.
const runnerRunTimeout = 5 * time.Second

// Some program which should be in $PATH. Needs to run before runTests is
// initialized (so an init function wouldn't work), because runTest uses it.
var pathProg = func() string {
	if runtime.GOOS == "windows" {
		return "cmd"
	}
	return "sh"
}()

func parse(tb testing.TB, parser *syntax.Parser, src string) *syntax.File {
	if parser == nil {
		parser = syntax.NewParser()
	}
	file, err := parser.Parse(strings.NewReader(src), "")
	if err != nil {
		tb.Fatal(err)
	}
	return file
}

func BenchmarkRun(b *testing.B) {
	b.ReportAllocs()
	b.StopTimer()
	src := `
echo a b c d
echo ./$foo_interp_missing/etc $(echo foo_interp_missing bar_interp_missing)
foo_interp_missing="bar_interp_missing"
x=y :
fn() {
	local a=b
	for i in 1 2 3; do
		echo $i | cat
	done
}
[[ $foo_interp_missing == bar_interp_missing ]] && fn
echo a{b,c}d *.go
let i=(2 + 3)
`
	file := parse(b, nil, src)
	r, _ := interp.New()
	ctx := context.Background()
	b.StartTimer()
	for i := 0; i < b.N; i++ {
		r.Reset()
		if err := r.Run(ctx, file); err != nil {
			b.Fatal(err)
		}
	}
}

var hasBash52 bool

func TestMain(m *testing.M) {
	if os.Getenv("GOSH_PROG") != "" {
		switch os.Getenv("GOSH_CMD") {
		case "exit_0":
			os.Exit(0)
		case "exit_5":
			os.Exit(5)
		case "print_ok":
			fmt.Printf("exec ok\n")
			os.Exit(0)
		case "print_fail":
			fmt.Printf("exec fail\n")
			os.Exit(1)
		case "pid_and_hang":
			fmt.Println(os.Getpid())
			time.Sleep(time.Hour)
			os.Exit(0)
		case "foo_interp_missing_null_bar_interp_missing":
			fmt.Println("foo_interp_missing\x00bar_interp_missing")
			os.Exit(0)
		case "lookpath":
			_, err := exec.LookPath(pathProg)
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}
			fmt.Printf("%s found\n", pathProg)
			os.Exit(0)
		}
		r := strings.NewReader(os.Args[1])
		file, err := syntax.NewParser().Parse(r, "")
		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			os.Exit(1)
		}
		runner, _ := interp.New(
			interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
			interp.ExecHandlers(testExecHandler),
		)
		ctx := context.Background()
		if err := runner.Run(ctx, file); err != nil {
			var es interp.ExitStatus
			if errors.As(err, &es) {
				os.Exit(int(es))
			}

			fmt.Fprintln(os.Stderr, err)
			os.Exit(1)
		}

		os.Exit(0)
	}
	prog, err := os.Executable()
	if err != nil {
		panic(err)
	}
	os.Setenv("GOSH_PROG", prog)

	// Mimic syntax/parser_test.go's TestMain.
	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")
	}

	os.Unsetenv("CDPATH")
	hasBash52 = checkBash()

	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	os.Setenv("GO_TEST_DIR", wd)

	os.Setenv("INTERP_GLOBAL", "value")
	os.Setenv("MULTILINE_INTERP_GLOBAL", "\nwith\nnewlines\n\n")

	// Double check that env vars on Windows are case insensitive.
	if runtime.GOOS == "windows" {
		os.Setenv("mixedCase_INTERP_GLOBAL", "value")
	} else {
		os.Setenv("MIXEDCASE_INTERP_GLOBAL", "value")
	}

	os.Setenv("PATH_PROG", pathProg)

	// To print env vars. Only a builtin on Windows.
	if runtime.GOOS == "windows" {
		os.Setenv("ENV_PROG", "cmd /c set")
	} else {
		os.Setenv("ENV_PROG", "env")
	}

	for _, s := range []string{"a", "b", "c", "d", "foo_interp_missing", "bar_interp_missing"} {
		os.Unsetenv(s)
	}
	exit := m.Run()
	os.Exit(exit)
}

func checkBash() bool {
	out, err := exec.Command("bash", "-c", "echo -n $BASH_VERSION").Output()
	if err != nil {
		return false
	}
	return strings.HasPrefix(string(out), "5.2")
}

// concBuffer wraps a [bytes.Buffer] in a mutex so that concurrent writes
// to it don't upset the race detector.
type concBuffer struct {
	buf bytes.Buffer
	sync.Mutex
}

func (c *concBuffer) Write(p []byte) (int, error) {
	c.Lock()
	n, err := c.buf.Write(p)
	c.Unlock()
	return n, err
}

func (c *concBuffer) WriteString(s string) (int, error) {
	c.Lock()
	n, err := c.buf.WriteString(s)
	c.Unlock()
	return n, err
}

func (c *concBuffer) String() string {
	c.Lock()
	s := c.buf.String()
	c.Unlock()
	return s
}

func (c *concBuffer) Reset() {
	c.Lock()
	c.buf.Reset()
	c.Unlock()
}

type runTest struct {
	in, want string
}

var runTests = []runTest{
	// no-op programs
	{"", ""},
	{"true", ""},
	{":", ""},
	{"exit", ""},
	{"exit 0", ""},
	{"{ :; }", ""},
	{"(:)", ""},

	// exit status codes
	{"exit 1", "exit status 1"},
	{"exit -1", "exit status 255"},
	{"exit 300", "exit status 44"},
	{"false", "exit status 1"},
	{"false foo_interp_missing", "exit status 1"},
	{"! false", ""},
	{"true foo_interp_missing", ""},
	{": foo_interp_missing", ""},
	{"! true", "exit status 1"},
	{"false; true", ""},
	{"false; exit", "exit status 1"},
	{"exit; echo foo_interp_missing", ""},
	{"exit 0; echo foo_interp_missing", ""},
	{"printf", "usage: printf format [arguments]\nexit status 2 #JUSTERR"},
	{"break", "break is only useful in a loop\n #JUSTERR"},
	{"continue", "continue is only useful in a loop\n #JUSTERR"},
	{"cd a b", "usage: cd [dir]\nexit status 2 #JUSTERR"},
	{"shift a", "usage: shift [n]\nexit status 2 #JUSTERR"},
	{
		"shouldnotexist",
		"\"shouldnotexist\": executable file not found in $PATH\nexit status 127 #JUSTERR",
	},
	{
		"for i in 1; do continue a; done",
		"usage: continue [n]\nexit status 2 #JUSTERR",
	},
	{
		"for i in 1; do break a; done",
		"usage: break [n]\nexit status 2 #JUSTERR",
	},
	{"false; a=b", ""},
	{"false; false &", ""},
	{
		"GOSH_CMD=exit_0 $GOSH_PROG; echo next",
		"next\n",
	},
	{
		"GOSH_CMD=exit_5 $GOSH_PROG; echo next",
		"next\n",
	},

	// we don't need to follow bash error strings
	{"exit a", "invalid exit status code: \"a\"\nexit status 2 #JUSTERR"},
	{"exit 1 2", "exit cannot take multiple arguments\nexit status 1 #JUSTERR"},
	{"f() { return a; }; f", "invalid return status code: \"a\"\nexit status 2 #JUSTERR"},

	// echo
	{"echo", "\n"},
	{"echo a b c", "a b c\n"},
	{"echo -n foo_interp_missing", "foo_interp_missing"},
	{`echo -e '\t'`, "\t\n"},
	{`echo -E '\t'`, "\\t\n"},
	{"echo -x foo_interp_missing", "-x foo_interp_missing\n"},
	{"echo -e -x -e foo_interp_missing", "-x -e foo_interp_missing\n"},

	// printf
	{"printf foo_interp_missing", "foo_interp_missing"},
	{"printf %%", "%"},
	{"printf %", "missing format char\nexit status 1 #JUSTERR"},
	{"printf %; echo foo_interp_missing", "missing format char\nfoo_interp_missing\n #IGNORE"},
	{"printf %1", "missing format char\nexit status 1 #JUSTERR"},
	{"printf %+", "missing format char\nexit status 1 #JUSTERR"},
	{"printf %B foo_interp_missing", "invalid format char: B\nexit status 1 #JUSTERR"},
	{"printf %12-s foo_interp_missing", "invalid format char: -\nexit status 1 #JUSTERR"},
	{"printf ' %s \n' bar_interp_missing", " bar_interp_missing \n"},
	{"printf '\\A'", "\\A"},
	{"printf %s foo_interp_missing", "foo_interp_missing"},
	{"printf %s", ""},
	{"printf %d,%i 3 4", "3,4"},
	{"printf %d", "0"},
	{"printf %d,%d 010 0x10", "8,16"},
	{"printf %c,%c,%c foo_interp_missing àa", "f,\xc3,\x00"}, // TODO: use a rune?
	{"printf %3s a", "  a"},
	{"printf %3i 1", "  1"},
	{"printf %+i%+d 1 -3", "+1-3"},
	{"printf %-5x 10", "a    "},
	{"printf %02x 1", "01"},
	{"printf 'a% 5s' a", "a    a"},
	{"printf 'nofmt' 1 2 3", "nofmt"},
	{"printf '%d_' 1 2 3", "1_2_3_"},
	{"printf '%02d %02d\n' 1 2 3", "01 02\n03 00\n"},
	{`printf '0%s1' 'a\bc'`, `0a\bc1`},
	{`printf '0%b1' 'a\bc'`, "0a\bc1"},
	{"printf 'a%bc'", "ac"},

	// words and quotes
	{"echo  foo_interp_missing ", "foo_interp_missing\n"},
	{"echo ' foo_interp_missing '", " foo_interp_missing \n"},
	{`echo " foo_interp_missing "`, " foo_interp_missing \n"},
	{`echo a'b'c"d"e`, "abcde\n"},
	{`a=" b c "; echo $a`, "b c\n"},
	{`a=" b c "; echo "$a"`, " b c \n"},
	{`a=" b c "; echo foo${a}bar`, "foo b c bar\n"},
	{`a="b    c"; echo foo${a}bar`, "foob cbar\n"},
	{`echo "$(echo ' b c ')"`, " b c \n"},
	{"echo ''", "\n"},
	{`$(echo)`, ""},
	{`echo -n '\\'`, `\\`},
	{`echo -n "\\"`, `\`},
	{`set -- a b c; x="$@"; echo "$x"`, "a b c\n"},
	{`set -- b c; echo a"$@"d`, "ab cd\n"},
	{`count() { echo $#; }; set --; count "$@"`, "0\n"},
	{`count() { echo $#; }; set -- ""; count "$@"`, "1\n"},
	{`count() { echo $#; }; set -- ""; shift; count "$@"`, "0\n"},
	{`count() { echo $#; }; a=(); count "${a[@]}"`, "0\n"},
	{`count() { echo $#; }; a=(""); count "${a[@]}"`, "1\n"},
	{`echo $1 $3; set -- a b c; echo $1 $3`, "\na c\n"},
	{`[[ $0 == "bash" || $0 == "gosh" ]]`, ""},

	// dollar quotes
	{`echo $'foo_interp_missing\nbar_interp_missing'`, "foo_interp_missing\nbar_interp_missing\n"},
	{`echo $'\r\t\\'`, "\r\t\\\n"},
	{`echo $"foo_interp_missing\nbar_interp_missing"`, "foo_interp_missing\\nbar_interp_missing\n"},
	{`echo $'%s'`, "%s\n"},
	{`a=$'\r\t\\'; echo "$a"`, "\r\t\\\n"},
	{`a=$"foo_interp_missing\nbar_interp_missing"; echo "$a"`, "foo_interp_missing\\nbar_interp_missing\n"},
	{`echo $'\a\b\e\E\f\v'`, "\a\b\x1b\x1b\f\v\n"},
	{`echo $'\\\'\"\?'`, "\\'\"?\n"},
	{`echo $'\1\45\12345\777\9'`, "\x01%S45\xff\\9\n"},
	{`echo $'\x\xf\x09\xAB'`, "\\x\x0f\x09\xab\n"},
	{`echo $'\u\uf\u09\uABCD\u00051234'`, "\\u\u000f\u0009\uabcd\u00051234\n"},
	{`echo $'\U\Uf\U09\UABCD\U00051234'`, "\\U\u000f\u0009\uabcd\U00051234\n"},
	{
		"echo 'foo_interp_missing\x00bar_interp_missing'",
		"foo_interp_missingbar_interp_missing\n",
	},
	{
		"echo \"foo_interp_missing\x00bar_interp_missing\"",
		"foo_interp_missingbar_interp_missing\n",
	},
	{
		"echo $'foo_interp_missing\x00bar_interp_missing'",
		"foo_interp_missingbar_interp_missing\n",
	},
	{
		"echo $'foo_interp_missing\\x00bar_interp_missing'",
		"foo_interp_missing\n",
	},
	{
		"echo $'foo_interp_missing\\xbar_interp_missing'",
		"foo_interp_missing\xbar_interp_missing\n",
	},
	{
		"a='foo_interp_missing\x00bar_interp_missing'; eval \"echo -n ${a} ${a@Q}\";",
		"foo_interp_missingbar_interp_missing foo_interp_missingbar_interp_missing",
	},
	{
		"a=$'foo_interp_missing\\x00bar_interp_missing'; eval \"echo -n ${a} ${a@Q}\";",
		"foo_interp_missing foo_interp_missing",
	},
	{
		"i\x00f true; then echo foo_interp_missing\x00; \x00fi",
		"foo_interp_missing\n",
	},
	{
		"echo $(GOSH_CMD=foo_interp_missing_null_bar_interp_missing $GOSH_PROG)",
		"foo_interp_missingbar_interp_missing\n #IGNORE",
	},
	// See the TODO where FOO_INTERP_MISSING_NULL_BAR_INTERP_MISSING is set.
	// {
	// 	"echo $FOO_INTERP_MISSING_NULL_BAR_INTERP_MISSING \"${FOO_INTERP_MISSING_NULL_BAR_INTERP_MISSING}\"",
	// 	"foo_interp_missing\n",
	// },

	// escaped chars
	{"echo a\\b", "ab\n"},
	{"echo a\\ b", "a b\n"},
	{"echo \\$a", "$a\n"},
	{"echo \"a\\b\"", "a\\b\n"},
	{"echo 'a\\b'", "a\\b\n"},
	{"echo \"a\\\nb\"", "ab\n"},
	{"echo 'a\\\nb'", "a\\\nb\n"},
	{`echo "\""`, "\"\n"},
	{`echo \\`, "\\\n"},
	{`echo \\\\`, "\\\\\n"},
	{`echo \`, "\n"},

	// escape characters in double quote literal
	{`echo "\\"`, "\\\n"},     // special character is preserved
	{`echo "\b"`, "\\b\n"},    // non-special character has both characters preserved
	{`echo "\\\\"`, "\\\\\n"}, // sequential backslashes (escape characters repeated sequentially)

	// vars
	{"foo_interp_missing=bar_interp_missing; echo $foo_interp_missing", "bar_interp_missing\n"},
	{"foo_interp_missing=bar_interp_missing foo_interp_missing=etc; echo $foo_interp_missing", "etc\n"},
	{"foo_interp_missing=bar_interp_missing; foo_interp_missing=etc; echo $foo_interp_missing", "etc\n"},
	{"foo_interp_missing=bar_interp_missing; foo_interp_missing=; echo $foo_interp_missing", "\n"},
	{"unset foo_interp_missing; echo $foo_interp_missing", "\n"},
	{"foo_interp_missing=bar_interp_missing; unset foo_interp_missing; echo $foo_interp_missing", "\n"},
	{"echo $INTERP_GLOBAL", "value\n"},
	{"INTERP_GLOBAL=; echo $INTERP_GLOBAL", "\n"},
	{"unset INTERP_GLOBAL; echo $INTERP_GLOBAL", "\n"},
	{"echo $MIXEDCASE_INTERP_GLOBAL", "value\n"},
	{"foo_interp_missing=bar_interp_missing; foo_interp_missing=x true; echo $foo_interp_missing", "bar_interp_missing\n"},
	{"foo_interp_missing=bar_interp_missing; foo_interp_missing=x true; echo $foo_interp_missing", "bar_interp_missing\n"},
	{"foo_interp_missing=bar_interp_missing; $ENV_PROG | grep '^foo_interp_missing='", "exit status 1"},
	{"foo_interp_missing=bar_interp_missing $ENV_PROG | grep '^foo_interp_missing='", "foo_interp_missing=bar_interp_missing\n"},
	{"foo_interp_missing=a foo_interp_missing=b $ENV_PROG | grep '^foo_interp_missing='", "foo_interp_missing=b\n"},
	{"$ENV_PROG | grep '^INTERP_GLOBAL='", "INTERP_GLOBAL=value\n"},
	{"INTERP_GLOBAL=new; $ENV_PROG | grep '^INTERP_GLOBAL='", "INTERP_GLOBAL=new\n"},
	{"INTERP_GLOBAL=; $ENV_PROG | grep '^INTERP_GLOBAL='", "INTERP_GLOBAL=\n"},
	{"unset INTERP_GLOBAL; $ENV_PROG | grep '^INTERP_GLOBAL='", "exit status 1"},
	{"a=b; a+=c x+=y; echo $a $x", "bc y\n"},
	{`a=" x  y"; b=$a c="$a"; echo $b; echo $c`, "x y\nx y\n"},
	{`a=" x  y"; b=$a c="$a"; echo "$b"; echo "$c"`, " x  y\n x  y\n"},
	{`arr=("foo_interp_missing" "bar_interp_missing" "lala" "foo_interp_missingbar_interp_missing"); echo ${arr[@]:2}; echo ${arr[*]:2}`, "lala foo_interp_missingbar_interp_missing\nlala foo_interp_missingbar_interp_missing\n"},
	{`arr=("foo_interp_missing" "bar_interp_missing" "lala" "foo_interp_missingbar_interp_missing"); echo ${arr[@]:2:4}; echo ${arr[*]:1:4}`, "lala foo_interp_missingbar_interp_missing\nbar_interp_missing lala foo_interp_missingbar_interp_missing\n"},
	{`arr=("foo_interp_missing" "bar_interp_missing"); echo ${arr[@]}; echo ${arr[*]}`, "foo_interp_missing bar_interp_missing\nfoo_interp_missing bar_interp_missing\n"},
	{`arr=("foo_interp_missing"); echo ${arr[@]:99}`, "\n"},
	{`echo ${arr[@]:1:99}; echo ${arr[*]:1:99}`, "\n\n"},
	{`arr=(0 1 2 3 4 5 6 7 8 9 0 a b c d e f g h); echo ${arr[@]:3:4}`, "3 4 5 6\n"},
	{`echo ${foo_interp_missing[@]}; echo ${foo_interp_missing[*]}`, "\n\n"},
	// TODO: reenable once we figure out the broken pipe error
	//{`$ENV_PROG | while read line; do if test -z "$line"; then echo empty; fi; break; done`, ""}, // never begin with an empty element

	// inline variables have special scoping
	{
		"f() { echo $inline; inline=bar_interp_missing true; echo $inline; }; inline=foo_interp_missing f",
		"foo_interp_missing\nfoo_interp_missing\n",
	},
	{"v=x; read v <<< 'y'; echo $v", "y\n"},
	{"v=x; v=inline read v <<< 'y'; echo $v", "x\n"},
	{"v=x; v=inline unset v; echo $v", "x\n"},
	{"v=x; echo 'v=y' >f; v=inline source f; echo $v", "x\n"},
	{"declare -n v=v2; v=inline true; echo $v $v2", "\n"},
	{"f() { echo $v; }; v=x; v=y f; f", "y\nx\n"},
	{"f() { echo $v; }; v=x; v+=y f; f", "xy\nx\n"},
	{"f() { echo $v; }; declare -n v=v2; v2=x; v=y f; f", "y\nx\n"},
	{"f() { echo ${v[@]}; }; v=(e1 e2); v=y f; f", "y\ne1 e2\n"},

	// special vars
	{"echo $?; false; echo $?", "0\n1\n"},
	{"for i in 1 2; do\necho $LINENO\necho $LINENO\ndone", "2\n3\n2\n3\n"},
	{"[[ -n $$ && $$ -gt 0 ]]", ""},
	{"[[ $$ -eq $PPID ]]", "exit status 1"},
	{"[[ $RANDOM -eq $RANDOM ]]", "exit status 1"},   // 1 in 32k chance of a collision, 0.003%
	{"[[ $SRANDOM -eq $SRANDOM ]]", "exit status 1"}, // 1 in 2**32 chance of a collision,

	// Ensure that we consistently use 64 bits even on 32-bit platforms.
	// Bash doesn't do this, but we do, for portability and consistency.
	{"[[ 1000000000123 -lt 100 ]]", "exit status 1"},
	{"[[ 1000000000123 -eq 1000000000456 ]]", "exit status 1"},
	{"[[ 1000000000123 < 100 ]]", "exit status 1"},
	{"((1000000000123 == 1000000000456))", "exit status 1"},

	// var manipulation
	{"echo ${#a} ${#a[@]}", "0 0\n"},
	{"a=bar_interp_missing; echo ${#a} ${#a[@]}", "18 1\n"},
	{"a=世界; echo ${#a}", "2\n"},
	{"a=(a bcd); echo ${#a} ${#a[@]} ${#a[*]} ${#a[1]}", "1 2 2 3\n"},
	{
		"a=($(echo a bcd)); echo ${#a} ${#a[@]} ${#a[*]} ${#a[1]}",
		"1 2 2 3\n",
	},
	{
		"a=([0]=$(echo a b) $(echo c d)); echo ${#a} ${#a[@]} ${#a[*]} ${#a[0]}",
		"3 3 3 3\n",
	},
	{"set -- a bc; echo ${#@} ${#*} $#", "2 2 2\n"},
	{
		"echo ${!a}; echo more",
		"invalid indirect expansion\nexit status 1 #JUSTERR",
	},
	{
		"a=b; echo ${!a}; b=c; echo ${!a}",
		"\nc\n",
	},
	{
		"a=foo_interp_missing; echo ${a:1}; echo ${a: -1}; echo ${a: -10}; echo ${a:5}",
		"oo_interp_missing\ng\nrp_missing\nnterp_missing\n",
	},
	{
		"a=foo_interp_missing; echo ${a::2}; echo ${a::-1}; echo ${a: -10}; echo ${a::5}",
		"fo\nfoo_interp_missin\nrp_missing\nfoo_i\n",
	},
	{
		"a=abc; echo ${a:1:1}",
		"b\n",
	},
	{
		"a=foo_interp_missing; echo ${a/no/x} ${a/o/i} ${a//o/i} ${a/fo/}",
		"foo_interp_missing fio_interp_missing fii_interp_missing o_interp_missing\n",
	},
	{
		"a=foo_interp_missing; echo ${a/*/xx} ${a//?/na} ${a/o*}",
		"xx nananananananananananananananananana f\n",
	},
	{
		"a=12345; echo ${a//[42]} ${a//[^42]} ${a//[!42]}",
		"135 24 24\n",
	},
	{"a=0123456789; echo ${a//[1-35-8]}", "049\n"},
	{"a=]abc]; echo ${a//[]b]}", "ac\n"},
	{"a=-abc-; echo ${a//[-b]}", "ac\n"},
	{`a='x\y'; echo ${a//\\}`, "xy\n"},
	{"a=']'; echo ${a//[}", "]\n"},
	{"a=']'; echo ${a//[]}", "]\n"},
	{"a=']'; echo ${a//[]]}", "\n"},
	{"a='['; echo ${a//[[]}", "\n"},
	{"a=']'; echo ${a//[xy}", "]\n"},
	{"a='abc123'; echo ${a//[[:digit:]]}", "abc\n"},
	{"a='[[:wrong:]]'; echo ${a//[[:wrong:]]}", "[[:wrong:]]\n"},
	{"a='[[:wrong:]]'; echo ${a//[[:}", "[[:wrong:]]\n"},
	{"a='abcx1y'; echo ${a//x[[:digit:]]y}", "abc\n"},
	{`a=xyz; echo "${a/y/a  b}"`, "xa  bz\n"},
	{"a='foo_interp_missing/bar_interp_missing'; echo ${a//o*a/}", "fr_interp_missing\n"},
	{"a=foobar; echo ${a//a/} ${a///b} ${a///}", "foobr foobar foobar\n"},
	{
		"echo ${a:-b}; echo $a; a=; echo ${a:-b}; a=c; echo ${a:-b}",
		"b\n\nb\nc\n",
	},
	{
		"echo ${#:-never} ${?:-never} ${LINENO:-never}",
		"0 0 1\n",
	},
	{
		"echo ${1-one} ${2-two} ${3-three}",
		"one two three\n",
	},
	{
		"set -u; echo ${1}",
		"1: unbound variable\nexit status 1 #JUSTERR",
	},
	{
		"echo ${a-b}; echo $a; a=; echo ${a-b}; a=c; echo ${a-b}",
		"b\n\n\nc\n",
	},
	{
		"echo ${a:=b}; echo $a; a=; echo ${a:=b}; a=c; echo ${a:=b}",
		"b\nb\nb\nc\n",
	},
	{
		"echo ${a=b}; echo $a; a=; echo ${a=b}; a=c; echo ${a=b}",
		"b\nb\n\nc\n",
	},
	{
		"echo ${a:+b}; echo $a; a=; echo ${a:+b}; a=c; echo ${a:+b}",
		"\n\n\nb\n",
	},
	{
		"echo ${a+b}; echo $a; a=; echo ${a+b}; a=c; echo ${a+b}",
		"\n\nb\nb\n",
	},
	{
		"a=b; echo ${a:?err1}; a=; echo ${a:?err2}; unset a; echo ${a:?err3}",
		"b\na: err2\nexit status 1 #JUSTERR",
	},
	{
		"a=b; echo ${a?err1}; a=; echo ${a?err2}; unset a; echo ${a?err3}",
		"b\n\na: err3\nexit status 1 #JUSTERR",
	},
	{
		"echo ${a:?%s}",
		"a: %s\nexit status 1 #JUSTERR",
	},
	{
		"x=aaabccc; echo ${x#*a}; echo ${x##*a}",
		"aabccc\nbccc\n",
	},
	{
		"x=(__a _b c_); echo ${x[@]#_}",
		"_a b c_\n",
	},
	{
		"x=(a__ b_ _c); echo ${x[@]%%_}",
		"a_ b _c\n",
	},
	{
		"x=aaabccc; echo ${x%c*}; echo ${x%%c*}",
		"aaabcc\naaab\n",
	},
	{
		"x=aaabccc; echo ${x%%[bc}",
		"aaabccc\n",
	},
	{
		"a='àÉñ bAr_interp_missing'; echo ${a^}; echo ${a^^}",
		"ÀÉñ bAr_interp_missing\nÀÉÑ BAR_INTERP_MISSING\n",
	},
	{
		"a='àÉñ bAr_interp_missing'; echo ${a,}; echo ${a,,}",
		"àÉñ bAr_interp_missing\nàéñ bar_interp_missing\n",
	},
	{
		"a='àÉñ bAr_interp_missing'; echo ${a^?}; echo ${a^^[br]}",
		"ÀÉñ bAr_interp_missing\nàÉñ BAR_inteRp_missing\n",
	},
	{
		"a='àÉñ bAr_interp_missing'; echo ${a,?}; echo ${a,,[br]}",
		"àÉñ bAr_interp_missing\nàÉñ bAr_interp_missing\n",
	},
	{
		"a=(àÉñ bAr_interp_missing); echo ${a[@]^}; echo ${a[*],,}",
		"ÀÉñ BAr_interp_missing\nàéñ bar_interp_missing\n",
	},
	{
		"INTERP_X_1=a INTERP_X_2=b; echo ${!INTERP_X_*}",
		"INTERP_X_1 INTERP_X_2\n",
	},
	{
		"INTERP_X_2=b INTERP_X_1=a; echo ${!INTERP_*}",
		"INTERP_GLOBAL INTERP_X_1 INTERP_X_2\n",
	},
	{
		`INTERP_X_2=b INTERP_X_1=a; set -- ${!INTERP_*}; echo $#`,
		"3\n",
	},
	{
		`INTERP_X_2=b INTERP_X_1=a; set -- "${!INTERP_*}"; echo $#`,
		"1\n",
	},
	{
		`INTERP_X_2=b INTERP_X_1=a; set -- ${!INTERP_@}; echo $#`,
		"3\n",
	},
	{
		`INTERP_X_2=b INTERP_X_1=a; set -- "${!INTERP_@}"; echo $#`,
		"3\n",
	},
	{
		`a='b  c'; eval "echo -n ${a} ${a@Q}"`,
		`b c b  c`,
	},
	{
		`a='"\n'; printf "%s %s" "${a}" "${a@E}"`,
		"\"\\n \"\n",
	},
	{
		"declare a; a+=(b); echo ${a[@]} ${#a[@]}",
		"b 1\n",
	},
	{
		`a=""; a+=(b); echo ${a[@]} ${#a[@]}`,
		"b 2\n",
	},
	{
		"f() { local a; a=bad; a=good; echo $a; }; f",
		"good\n",
	},
	{
		`declare x; [[ -v x ]] && echo set || echo unset`,
		"unset\n",
	},
	{
		`declare x=; [[ -v x ]] && echo set || echo unset`,
		"set\n",
	},
	{
		`declare -a x; [[ -v x ]] && echo set || echo unset`,
		"unset\n",
	},
	{
		`declare -A x; [[ -v x ]] && echo set || echo unset`,
		"unset\n",
	},
	{
		`declare -r -x x; [[ -v x ]] && echo set || echo unset`,
		"unset\n",
	},
	{
		`declare -n x; [[ -v x ]] && echo set || echo unset`,
		"unset\n",
	},

	// if
	{
		"if true; then echo foo_interp_missing; fi",
		"foo_interp_missing\n",
	},
	{
		"if false; then echo foo_interp_missing; fi",
		"",
	},
	{
		"if false; then echo foo_interp_missing; fi",
		"",
	},
	{
		"if true; then echo foo_interp_missing; else echo bar_interp_missing; fi",
		"foo_interp_missing\n",
	},
	{
		"if false; then echo foo_interp_missing; else echo bar_interp_missing; fi",
		"bar_interp_missing\n",
	},
	{
		"if true; then false; fi",
		"exit status 1",
	},
	{
		"if false; then :; else false; fi",
		"exit status 1",
	},
	{
		"if false; then :; elif true; then echo foo_interp_missing; fi",
		"foo_interp_missing\n",
	},
	{
		"if false; then :; elif false; then :; elif true; then echo foo_interp_missing; fi",
		"foo_interp_missing\n",
	},
	{
		"if false; then :; elif false; then :; else echo foo_interp_missing; fi",
		"foo_interp_missing\n",
	},

	// while
	{
		"while false; do echo foo_interp_missing; done",
		"",
	},
	{
		"while true; do exit 1; done",
		"exit status 1",
	},
	{
		"while true; do break; done",
		"",
	},
	{
		"while true; do while true; do break 2; done; done",
		"",
	},

	// until
	{
		"until true; do echo foo_interp_missing; done",
		"",
	},
	{
		"until false; do exit 1; done",
		"exit status 1",
	},
	{
		"until false; do break; done",
		"",
	},

	// for
	{
		"for i in 1 2 3; do echo $i; done",
		"1\n2\n3\n",
	},
	{
		"for i in 1 2 3; do echo $i; exit; done",
		"1\n",
	},
	{
		"for i in 1 2 3; do echo $i; false; done",
		"1\n2\n3\nexit status 1",
	},
	{
		"for i in 1 2 3; do echo $i; break; done",
		"1\n",
	},
	{
		"for i in 1 2 3; do echo $i; continue; echo foo_interp_missing; done",
		"1\n2\n3\n",
	},
	{
		"for i in 1 2; do for j in a b; do echo $i $j; continue 2; done; done",
		"1 a\n2 a\n",
	},
	{
		"for ((i=0; i<3; i++)); do echo $i; done",
		"0\n1\n2\n",
	},
	// for, with missing Init, Cond, Post
	{
		"i=0; for ((; i<3; i++)); do echo $i; done",
		"0\n1\n2\n",
	},
	{
		"for ((i=0;; i++)); do if [ $i -ge 3 ]; then break; fi; echo $i; done",
		"0\n1\n2\n",
	},
	{
		"for ((i=0; i<3;)); do echo $i; i=$((i+1)); done",
		"0\n1\n2\n",
	},
	{
		"i=0; for ((;;)); do if [ $i -ge 3 ]; then break; fi; echo $i; i=$((i+1)); done",
		"0\n1\n2\n",
	},
	// TODO: uncomment once expandEnv.Set starts returning errors
	// {
	// 	"readonly i; for ((i=0; i<3; i++)); do echo $i; done",
	// 	"0\n1\n2\n",
	// },
	{
		"for ((i=5; i>0; i--)); do echo $i; break; done",
		"5\n",
	},
	{
		"for i in 1 2; do for j in a b; do echo $i $j; done; break; done",
		"1 a\n1 b\n",
	},
	{
		"for i in 1 2 3; do :; done; echo $i",
		"3\n",
	},
	{
		"for ((i=0; i<3; i++)); do :; done; echo $i",
		"3\n",
	},
	{
		"set -- a 'b c'; for i in; do echo $i; done",
		"",
	},
	{
		"set -- a 'b c'; for i; do echo $i; done",
		"a\nb c\n",
	},

	// block
	{
		"{ echo foo_interp_missing; }",
		"foo_interp_missing\n",
	},
	{
		"{ false; }",
		"exit status 1",
	},

	// subshell
	{
		"(echo foo_interp_missing)",
		"foo_interp_missing\n",
	},
	{
		"(false)",
		"exit status 1",
	},
	{
		"(exit 1)",
		"exit status 1",
	},
	{
		"(false); echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"(exit 0); echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"(exit 1); echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"(foo_interp_missing=bar_interp_missing; echo $foo_interp_missing); echo $foo_interp_missing",
		"bar_interp_missing\n\n",
	},
	{
		"(echo() { printf 'bar_interp_missing\n'; }; echo); echo",
		"bar_interp_missing\n\n",
	},
	{
		"unset INTERP_GLOBAL & echo $INTERP_GLOBAL",
		"value\n",
	},
	{
		"(fn() { :; }) & pwd >/dev/null",
		"",
	},
	{
		"x[0]=x; (echo ${x[0]}; x[0]=y; echo ${x[0]}); echo ${x[0]}",
		"x\ny\nx\n",
	},
	{
		`x[3]=x; (x[3]=y); echo ${x[3]}`,
		"x\n",
	},
	{
		"shopt -s expand_aliases; alias f='echo x'\nf\n(f\nalias f='echo y'\neval f\n)\nf\n",
		"x\nx\ny\nx\n",
	},
	{
		"set -- a; echo $1; (echo $1; set -- b; echo $1); echo $1",
		"a\na\nb\na\n",
	},
	{"false; ( echo $? )", "1\n"},

	// cd/pwd
	{"[[ fo~ == 'fo~' ]]", ""},
	{`[[ 'ab\c' == *\\* ]]`, ""},
	{`[[ foo_interp_missing/bar_interp_missing == foo_interp_missing* ]]`, ""},
	{"[[ a == [ab ]]", "exit status 1"},
	{`HOME='/*'; echo ~; echo "$HOME"`, "/*\n/*\n"},
	{`test -d ~`, ""},
	{
		`for flag in b c d e f g h k L p r s S u w x; do test -$flag ""; echo -n "$flag$? "; done`,
		`b1 c1 d1 e1 f1 g1 h1 k1 L1 p1 r1 s1 S1 u1 w1 x1 `,
	},
	{`foo_interp_missing=~; test -d $foo_interp_missing`, ""},
	{`foo_interp_missing=~; test -d "$foo_interp_missing"`, ""},
	{`foo_interp_missing='~'; test -d $foo_interp_missing`, "exit status 1"},
	{`foo_interp_missing='~'; [ $foo_interp_missing == '~' ]`, ""},
	{
		`[[ ~ == "$HOME" ]] && [[ ~/foo_interp_missing == "$HOME/foo_interp_missing" ]]`,
		"",
	},
	{
		`HOME=$PWD/home; mkdir home; touch home/f; [[ -e ~/f ]]`,
		"",
	},
	{
		`HOME=$PWD/home; mkdir home; touch home/f; [[ ~/f -ef $HOME/f ]]`,
		"",
	},
	{
		"[[ ~noexist == '~noexist' ]]",
		"",
	},
	{
		`w="$HOME"; cd; [[ $PWD == "$w" ]]`,
		"",
	},
	{
		`mkdir test.cd; cd test.cd; cd ''; [[ "$PWD" == "$OLDPWD" ]]`,
		"",
	},
	{
		`HOME=/foo_interp_missing; echo $HOME`,
		"/foo_interp_missing\n",
	},
	{
		"cd noexist",
		"exit status 1 #JUSTERR",
	},
	{
		"mkdir -p a/b && cd a && cd b && cd ../..",
		"",
	},
	{
		">a && cd a",
		"exit status 1 #JUSTERR",
	},
	{
		`[[ $PWD == "$(pwd)" ]]`,
		"",
	},
	{
		"PWD=changed; [[ $PWD == changed ]]",
		"",
	},
	{
		"PWD=changed; mkdir a; cd a; [[ $PWD == changed ]]",
		"exit status 1",
	},
	{
		`mkdir %s; old="$PWD"; cd %s; [[ $old == "$PWD" ]]`,
		"exit status 1",
	},
	{
		`old="$PWD"; mkdir a; cd a; cd ..; [[ $old == "$PWD" ]]`,
		"",
	},
	{
		`[[ $PWD == "$OLDPWD" ]]`,
		"exit status 1",
	},
	{
		`old="$PWD"; mkdir a; cd a; [[ $old == "$OLDPWD" ]]`,
		"",
	},
	{
		`mkdir a; ln -s a b; [[ $(cd a && pwd) == "$(cd b && pwd)" ]]; echo $?`,
		"1\n",
	},
	{
		`pwd -a`,
		"invalid option: \"-a\"\nexit status 2 #JUSTERR",
	},
	{
		`pwd -L -P -a`,
		"invalid option: \"-a\"\nexit status 2 #JUSTERR",
	},
	{
		`mkdir a; ln -s a b; [[ "$(cd a && pwd -P)" == "$(cd b && pwd -P)" ]]`,
		"",
	},
	{
		`mkdir a; ln -s a b; [[ "$(cd a && pwd -P)" == "$(cd b && pwd -L)" ]]; echo $?`,
		"1\n",
	},
	{
		`orig="$PWD"; mkdir a; cd a; cd - >/dev/null; [[ "$PWD" == "$orig" ]]`,
		"",
	},
	{
		`orig="$PWD"; mkdir a; cd a; [[ $(cd -) == "$orig" ]]`,
		"",
	},

	// dirs/pushd/popd
	{"set -- $(dirs); echo $# ${#DIRSTACK[@]}", "1 1\n"},
	{"pushd", "pushd: no other directory\nexit status 1 #JUSTERR"},
	{"pushd -n", ""},
	{"pushd foo_interp_missing bar_interp_missing", "pushd: too many arguments\nexit status 2 #JUSTERR"},
	{"pushd does-not-exist; set -- $(dirs); echo $#", "1\n #IGNORE"},
	{"mkdir a; pushd a >/dev/null; set -- $(dirs); echo $#", "2\n"},
	{"mkdir a; set -- $(pushd a); echo $#", "2\n"},
	{
		`mkdir a; pushd a >/dev/null; set -- $(dirs); [[ $1 == "$HOME" ]]`,
		"exit status 1",
	},
	{
		`mkdir a; pushd a >/dev/null; [[ ${DIRSTACK[0]} == "$HOME" ]]`,
		"exit status 1",
	},
	{
		`old=$(dirs); mkdir a; pushd a >/dev/null; pushd >/dev/null; set -- $(dirs); [[ $1 == "$old" ]]`,
		"",
	},
	{
		`old=$(dirs); mkdir a; pushd a >/dev/null; pushd -n >/dev/null; set -- $(dirs); [[ $1 == "$old" ]]`,
		"exit status 1",
	},
	{
		"mkdir a; pushd a >/dev/null; pushd >/dev/null; rm -r a; pushd",
		"exit status 1 #JUSTERR",
	},
	{
		`old=$(dirs); mkdir a; pushd -n a >/dev/null; set -- $(dirs); [[ $1 == "$old" ]]`,
		"",
	},
	{
		`old=$(dirs); mkdir a; pushd -n a >/dev/null; pushd >/dev/null; set -- $(dirs); [[ $1 == "$old" ]]`,
		"exit status 1",
	},
	{"popd", "popd: directory stack empty\nexit status 1 #JUSTERR"},
	{"popd -n", "popd: directory stack empty\nexit status 1 #JUSTERR"},
	{"popd foo_interp_missing", "popd: invalid argument\nexit status 2 #JUSTERR"},
	{"old=$(dirs); mkdir a; pushd a >/dev/null; set -- $(popd); echo $#", "1\n"},
	{
		`old=$(dirs); mkdir a; pushd a >/dev/null; popd >/dev/null; [[ $(dirs) == "$old" ]]`,
		"",
	},
	{"old=$(dirs); mkdir a; pushd a >/dev/null; set -- $(popd -n); echo $#", "1\n"},
	{
		`old=$(dirs); mkdir a; pushd a >/dev/null; popd -n >/dev/null; [[ $(dirs) == "$old" ]]`,
		"exit status 1",
	},
	{
		"mkdir a; pushd a >/dev/null; pushd >/dev/null; rm -r a; popd",
		"exit status 1 #JUSTERR",
	},

	// binary cmd
	{
		"true && echo foo_interp_missing || echo bar_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"false && echo foo_interp_missing || echo bar_interp_missing",
		"bar_interp_missing\n",
	},

	// func
	{
		"foo_interp_missing() { echo bar_interp_missing; }; foo_interp_missing",
		"bar_interp_missing\n",
	},
	{
		"foo_interp_missing() { echo $1; }; foo_interp_missing",
		"\n",
	},
	{
		"foo_interp_missing() { echo $1; }; foo_interp_missing a b",
		"a\n",
	},
	{
		"foo_interp_missing() { echo $1; bar_interp_missing c d; echo $2; }; bar_interp_missing() { echo $2; }; foo_interp_missing a b",
		"a\nd\nb\n",
	},
	{
		`foo_interp_missing() { echo $#; }; foo_interp_missing; foo_interp_missing 1 2 3; foo_interp_missing "a b"; echo $#`,
		"0\n3\n1\n0\n",
	},
	{
		`foo_interp_missing() { for a in $*; do echo "$a"; done }; foo_interp_missing 'a  1' 'b  2'`,
		"a\n1\nb\n2\n",
	},
	{
		`foo_interp_missing() { for a in "$*"; do echo "$a"; done }; foo_interp_missing 'a  1' 'b  2'`,
		"a  1 b  2\n",
	},
	{
		`foo_interp_missing() { for a in "foo_interp_missing$*"; do echo "$a"; done }; foo_interp_missing 'a  1' 'b  2'`,
		"foo_interp_missinga  1 b  2\n",
	},
	{
		`foo_interp_missing() { for a in $@; do echo "$a"; done }; foo_interp_missing 'a  1' 'b  2'`,
		"a\n1\nb\n2\n",
	},
	{
		`foo_interp_missing() { for a in "$@"; do echo "$a"; done }; foo_interp_missing 'a  1' 'b  2'`,
		"a  1\nb  2\n",
	},

	// alias (note the input newlines)
	{
		"alias foo_interp_missing; alias foo_interp_missing=echo; alias foo_interp_missing; alias foo_interp_missing=; alias foo_interp_missing",
		"alias: \"foo_interp_missing\" not found\nalias foo_interp_missing='echo'\nalias foo_interp_missing=''\n #IGNORE",
	},
	{
		"shopt -s expand_aliases; alias foo_interp_missing=echo\nfoo_interp_missing foo_interp_missing; foo_interp_missing bar_interp_missing",
		"foo_interp_missing\nbar_interp_missing\n",
	},
	{
		"shopt -s expand_aliases; alias true=echo\ntrue foo_interp_missing; unalias true\ntrue bar_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"shopt -s expand_aliases; alias echo='echo a'\necho b c",
		"a b c\n",
	},
	{
		"shopt -s expand_aliases; alias foo_interp_missing='echo '\nfoo_interp_missing foo_interp_missing; foo_interp_missing bar_interp_missing",
		"echo\nbar_interp_missing\n",
	},

	// case
	{
		"case b in x) echo foo_interp_missing ;; a|b) echo bar_interp_missing ;; esac",
		"bar_interp_missing\n",
	},
	{
		"case b in x) echo foo_interp_missing ;; y|z) echo bar_interp_missing ;; esac",
		"",
	},
	{
		"case foo_interp_missing in bar_interp_missing) echo foo_interp_missing ;; *) echo bar_interp_missing ;; esac",
		"bar_interp_missing\n",
	},
	{
		"case foo_interp_missing in *o*) echo bar_interp_missing ;; esac",
		"bar_interp_missing\n",
	},
	{
		"case foo_interp_missing in '*') echo x ;; f*) echo y ;; esac",
		"y\n",
	},

	// exec
	{
		"$GOSH_PROG 'echo foo_interp_missing'",
		"foo_interp_missing\n",
	},
	{
		"$GOSH_PROG 'echo foo_interp_missing >&2' >/dev/null",
		"foo_interp_missing\n",
	},
	{
		"echo foo_interp_missing | $GOSH_PROG 'cat >&2' >/dev/null",
		"foo_interp_missing\n",
	},
	{
		"$GOSH_PROG 'exit 1'",
		"exit status 1",
	},
	{
		"exec >/dev/null; echo foo_interp_missing",
		"",
	},

	// return
	{"return", "return: can only be done from a func or sourced script\nexit status 1 #JUSTERR"},
	{"f() { return; }; f", ""},
	{"f() { return 2; }; f", "exit status 2"},
	{"f() { echo foo_interp_missing; return; echo bar_interp_missing; }; f", "foo_interp_missing\n"},
	{"f1() { :; }; f2() { f1; return; }; f2", ""},
	{"echo 'return' >a; source a", ""},
	{"echo 'return' >a; source a; return", "return: can only be done from a func or sourced script\nexit status 1 #JUSTERR"},
	{"echo 'return 2' >a; source a", "exit status 2"},
	{"echo 'echo foo_interp_missing; return; echo bar_interp_missing' >a; source a", "foo_interp_missing\n"},

	// command
	{"command", ""},
	{"command -o echo", "command: invalid option \"-o\"\nexit status 2 #JUSTERR"},
	{"command -vo echo", "command: invalid option \"-o\"\nexit status 2 #JUSTERR"},
	{"echo() { :; }; echo foo_interp_missing", ""},
	{"echo() { :; }; command echo foo_interp_missing", "foo_interp_missing\n"},
	{"command -v does-not-exist", "exit status 1"},
	{"foo_interp_missing() { :; }; command -v foo_interp_missing", "foo_interp_missing\n"},
	{"foo_interp_missing() { :; }; command -v does-not-exist foo_interp_missing", "foo_interp_missing\n"},
	{"command -v echo", "echo\n"},
	{"[[ $(command -v $PATH_PROG) == $PATH_PROG ]]", "exit status 1"},

	// cmd substitution
	{
		"echo foo_interp_missing $(printf bar_interp_missing)",
		"foo_interp_missing bar_interp_missing\n",
	},
	{
		"echo foo_interp_missing $(echo bar_interp_missing)",
		"foo_interp_missing bar_interp_missing\n",
	},
	{
		"$(echo echo foo_interp_missing bar_interp_missing)",
		"foo_interp_missing bar_interp_missing\n",
	},
	{
		"for i in 1 $(echo 2 3) 4; do echo $i; done",
		"1\n2\n3\n4\n",
	},
	{
		"echo 1$(echo 2 3)4",
		"12 34\n",
	},
	{
		`mkdir d; [[ $(cd d && pwd) == "$(pwd)" ]]`,
		"exit status 1",
	},
	{
		"a=sub true & { a=main $ENV_PROG | grep '^a='; }",
		"a=main\n",
	},
	{
		"echo foo_interp_missing >f; echo $(cat f); echo $(<f)",
		"foo_interp_missing\nfoo_interp_missing\n",
	},
	{
		"echo foo_interp_missing >f; echo $(<f; echo bar_interp_missing)",
		"bar_interp_missing\n",
	},
	{
		"$(false); echo $?; $(exit 3); echo $?; $(true); echo $?",
		"1\n3\n0\n",
	},
	{
		"foo=$(false); echo $?; echo foo $(false); echo $?",
		"1\nfoo\n0\n",
	},
	{
		"$(false) $(true); echo $?; $(true) $(false); echo $?",
		"0\n1\n",
	},
	{
		"foo=$(false) $(true); echo $?; foo=$(true) $(false); echo $?",
		"1\n0\n",
	},

	// pipes
	{
		"echo foo_interp_missing | sed 's/o/a/g'",
		"faa_interp_missing\n",
	},
	{
		"echo foo_interp_missing | false | true",
		"",
	},
	{
		"true $(true) | true", // used to panic
		"",
	},
	{
		// The first command in the block used to consume stdin, even
		// though it shouldn't be. We just want to run any arbitrary
		// non-builtin program that doesn't consume stdin.
		"echo foo_interp_missing | { $ENV_PROG >/dev/null; cat; }",
		"foo_interp_missing\n",
	},

	// redirects
	{
		"echo foo_interp_missing >&1 | sed 's/o/a/g'",
		"faa_interp_missing\n",
	},
	{
		"echo foo_interp_missing >&2 | sed 's/o/a/g'",
		"foo_interp_missing\n",
	},
	{
		// TODO: why does bash need a block here?
		"{ echo foo_interp_missing >&2; } |& sed 's/o/a/g'",
		"faa_interp_missing\n",
	},
	{
		"echo foo_interp_missing >/dev/null; echo bar_interp_missing",
		"bar_interp_missing\n",
	},
	{
		">a; echo foo_interp_missing >>b; wc -c <a >>b; cat b | tr -d ' '",
		"foo_interp_missing\n0\n",
	},
	{
		"echo foo_interp_missing >a; <a",
		"",
	},
	{
		"echo foo_interp_missing >a; mkdir b; cd b; cat <../a",
		"foo_interp_missing\n",
	},
	{
		"echo foo_interp_missing >a; wc -c <a | tr -d ' '",
		"19\n",
	},
	{
		"echo foo_interp_missing >>a; echo bar_interp_missing &>>a; wc -c <a | tr -d ' '",
		"38\n",
	},
	{
		"{ echo a; echo b >&2; } &>/dev/null",
		"",
	},
	{
		"sed 's/o/a/g' <<EOF\nfoo_interp_missing$foo_interp_missing\nEOF",
		"faa_interp_missing\n",
	},
	{
		"sed 's/o/a/g' <<'EOF'\nfoo_interp_missing$foo_interp_missing\nEOF",
		"faa_interp_missing$faa_interp_missing\n",
	},
	{
		"sed 's/o/a/g' <<EOF\n\tfoo_interp_missing\nEOF",
		"\tfaa_interp_missing\n",
	},
	{
		"sed 's/o/a/g' <<EOF\nfoo_interp_missing\nEOF",
		"faa_interp_missing\n",
	},
	{
		"cat <<EOF\n~/foo_interp_missing\nEOF",
		"~/foo_interp_missing\n",
	},
	{
		"sed 's/o/a/g' <<<foo_interp_missing$foo_interp_missing",
		"faa_interp_missing\n",
	},
	{
		"cat <<-EOF\n\tfoo_interp_missing\nEOF",
		"foo_interp_missing\n",
	},
	{
		"cat <<-EOF\n\tfoo_interp_missing\n\nEOF",
		"foo_interp_missing\n\n",
	},
	{
		"cat <<EOF\nfoo_interp_missing\\\nbar_interp_missing\nEOF",
		"foo_interp_missingbar_interp_missing\n",
	},
	{
		"cat <<'EOF'\nfoo_interp_missing\\\nbar_interp_missing\nEOF",
		"foo_interp_missing\\\nbar_interp_missing\n",
	},
	{
		"cat <<EOF\nfoo\\\"bar\\baz\nEOF",
		"foo\\\"bar\\baz\n",
	},
	{
		"cat <<EOF\n \\\\ \\$ \\` \nEOF",
		" \\ $ ` \n",
	},
	{
		"mkdir a; echo foo_interp_missing >a |& grep -q 'is a directory'",
		" #IGNORE bash prints a warning",
	},
	{
		"echo foo_interp_missing 1>&1 | sed 's/o/a/g'",
		"faa_interp_missing\n",
	},
	{
		"echo foo_interp_missing 2>&2 |& sed 's/o/a/g'",
		"faa_interp_missing\n",
	},
	{
		"printf 2>&1 | sed 's/.*usage.*/foo_interp_missing/'",
		"foo_interp_missing\n",
	},
	{
		"mkdir a && cd a && echo foo_interp_missing >b && cd .. && cat a/b",
		"foo_interp_missing\n",
	},
	{
		"echo foo 2>&-; :",
		"foo\n",
	},
	{
		// `>&-` closes stdout or stderr. Note that any writes result in errors.
		"echo foo >&- 2>&-; :",
		"",
	},
	{
		"echo foo | sed $(read line 2>/dev/null; echo 's/o/a/g')",
		"",
	},
	{
		// `<&-` closes stdin, to e.g. ensure that a subshell does not consume
		// the standard input shared with the parent shell.
		// Note that any reads result in errors.
		"echo foo | sed $(exec <&-; read line 2>/dev/null; echo 's/o/a/g')",
		"faa\n",
	},
	{
		// Concurrent pipe commands used to cause races when modifying the environment.
		"a=1 b=2 c=3 d=4 e=5 : | a=1 b=2 c=3 d=4 e=5 : | a=1 b=2 c=3 d=4 e=5 : | a=1 b=2 c=3 d=4 e=5 :",
		"",
	},

	// background/wait
	{"wait", ""},
	{"wait foo_interp_missing", "wait: pid foo_interp_missing is not a child of this shell\nexit status 1 #JUSTERR"},
	{"{ true; } & wait", ""},
	{"{ false; } & wait", ""},
	{"{ sleep 0.01; true; } & wait", ""},
	{"{ sleep 0.01; false; } & wait", ""},
	{
		"{ echo foo_interp_missing; } & wait; echo bar_interp_missing",
		"foo_interp_missing\nbar_interp_missing\n",
	},
	{
		"{ echo foo_interp_missing & wait; } & wait; echo bar_interp_missing",
		"foo_interp_missing\nbar_interp_missing\n",
	},
	{`mkdir d; old=$PWD; cd d & wait; [[ $old == "$PWD" ]]`, ""},
	{
		"f() { echo 1; }; { sleep 0.01; f; } & f() { echo 2; }; wait",
		"1\n",
	},
	{"[[ -n $! ]]", "exit status 1"},
	{"true & [[ -n $! ]]", ""},
	{"true & true;  [[ -n $! ]]", ""},
	{"true & pid=$!; wait $pid", ""},
	{"false & pid=$!; wait $pid", "exit status 1"},
	{"{ sleep 0.01; true; } & pid=$!; wait $pid", ""},
	{"{ sleep 0.01; false; } & pid=$!; wait $pid", "exit status 1"},
	{"(true) & ok=$!; (false) & fail=$!; wait $ok $fail", "exit status 1"},
	{"(true) & ok=$!; (false) & ignore=$!; wait $ok", ""},
	{"echo foo | true | false & wait $!", "exit status 1"},
	{"echo foo | false | true & wait $!", ""},
	{"f() { false & true; }; f; wait $!", "exit status 1"},
	// The parent and child shells should not cause data races when setting env vars.
	// Note that we can't use `echo $var`, as it seems to write newlines separately,
	// which can cause them to get mixed up between concurrent subshells.
	{
		"{ for n in {0..9}; do { echo -n $n$'\n'; } & done; wait; } | sort",
		"0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n",
	},
	{
		"outer=val; for n in {0..9}; do { echo -n $outer$'\n'; } & outer=val; done; wait",
		"val\nval\nval\nval\nval\nval\nval\nval\nval\nval\n",
	},
	{
		"for n in {0..9}; do { inner=val; } & echo $inner; done",
		"\n\n\n\n\n\n\n\n\n\n",
	},
	{
		"exit 2 & bg1=$!; exit 0 & bg2=$!; wait $bg1 $bg2; echo $?",
		"0\n",
	},
	{
		"exit 2 & bg1=$!; exit 4 & bg2=$!; wait $bg1 $bg2; echo $?",
		"4\n",
	},

	// bash test
	{
		"[[ a ]]",
		"",
	},
	{
		"[[ '' ]]",
		"exit status 1",
	},
	{
		"[[ '' ]]; [[ a ]]",
		"",
	},
	{
		"[[ ! (a == b) ]]",
		"",
	},
	{
		"[[ a != b ]]",
		"",
	},
	{
		"[[ a && '' ]]",
		"exit status 1",
	},
	{
		"[[ a || '' ]]",
		"",
	},
	{
		"[[ a > 3 ]]",
		"",
	},
	{
		"[[ a < 3 ]]",
		"exit status 1",
	},
	{
		"[[ 3 == 03 ]]",
		"exit status 1",
	},
	{
		"[[ a -eq b ]]",
		"",
	},
	{
		"[[ 3 -eq 03 ]]",
		"",
	},
	{
		"[[ 3 -ne 4 ]]",
		"",
	},
	{
		"[[ 3 -le 4 ]]",
		"",
	},
	{
		"[[ 3 -ge 4 ]]",
		"exit status 1",
	},
	{
		"[[ 3 -ge 3 ]]",
		"",
	},
	{
		"[[ 3 -lt 4 ]]",
		"",
	},
	{
		"[[ ' 3' -lt '4 ' ]]",
		"",
	},
	{
		"[[ 3 -gt 4 ]]",
		"exit status 1",
	},
	{
		"[[ 3 -gt 3 ]]",
		"exit status 1",
	},
	{
		"[[ a -nt a || a -ot a ]]",
		"exit status 1",
	},
	{
		"touch -t 202111050000.30 a b; [[ a -nt b || a -ot b ]]",
		"exit status 1",
	},
	{
		"touch -t 202111050200.00 a; touch -t 202111060100.00 b; [[ a -nt b ]]",
		"exit status 1",
	},
	{
		"touch -t 202111050000.00 a; touch -t 202111060000.00 b; [[ a -ot b ]]",
		"",
	},
	{
		"[[ a -ef b ]]",
		"exit status 1",
	},
	{
		">a >b; [[ a -ef b ]]",
		"exit status 1",
	},
	{
		">a; [[ a -ef a ]]",
		"",
	},
	{
		">a; ln a b; [[ a -ef b ]]",
		"",
	},
	{
		">a; ln -s a b; [[ a -ef b ]]",
		"",
	},
	{
		"[[ -z 'foo_interp_missing' || -n '' ]]",
		"exit status 1",
	},
	{
		"[[ -z '' && -n 'foo_interp_missing' ]]",
		"",
	},
	{
		"a=x b=''; [[ -v a && -v b && ! -v c ]]",
		"",
	},
	{
		"[[ abc == *b* ]]",
		"",
	},
	{
		"[[ abc != *b* ]]",
		"exit status 1",
	},
	{
		"[[ *b = '*b' ]]",
		"",
	},
	{
		"[[ ab == a. ]]",
		"exit status 1",
	},
	{
		`x='*b*'; [[ abc == $x ]]`,
		"",
	},
	{
		`x='*b*'; [[ abc == "$x" ]]`,
		"exit status 1",
	},
	{
		`[[ abc == \a\bc ]]`,
		"",
	},
	{
		"[[ abc != *b'*' ]]",
		"",
	},
	{
		"[[ a =~ b ]]",
		"exit status 1",
	},
	{
		"[[ foo_interp_missing =~ foo_interp_missing && foo_interp_missing =~ .* && foo_interp_missing =~ f.o ]]",
		"",
	},
	{
		"[[ foo_interp_missing =~ oo ]] && echo foo_interp_missing; [[ foo_interp_missing =~ ^oo$ ]] && echo bar_interp_missing || true",
		"foo_interp_missing\n",
	},
	{
		"[[ a =~ [ ]]",
		"exit status 2",
	},
	{
		"[[ a__b__c =~ _*(b_*) ]]; echo ${BASH_REMATCH[0]}; echo ${BASH_REMATCH[1]}",
		"__b__\nb__\n",
	},
	{
		"[[ -e a ]] && echo x; >a; [[ -e a ]] && echo y",
		"y\n",
	},
	{
		"ln -s b a; [[ -e a ]] && echo x; >b; [[ -e a ]] && echo y",
		"y\n",
	},
	{
		"[[ -f a ]] && echo x; >a; [[ -f a ]] && echo y",
		"y\n",
	},
	{
		"[[ -e a ]] && echo x; mkdir a; [[ -e a ]] && echo y",
		"y\n",
	},
	{
		"[[ -d a ]] && echo x; mkdir a; [[ -d a ]] && echo y",
		"y\n",
	},
	{
		"[[ -r a ]] && echo x; >a; [[ -r a ]] && echo y",
		"y\n",
	},
	{
		"[[ -w a ]] && echo x; >a; [[ -w a ]] && echo y",
		"y\n",
	},
	{
		"[[ -s a ]] && echo x; echo body >a; [[ -s a ]] && echo y",
		"y\n",
	},
	{
		"[[ -L a ]] && echo x; ln -s b a; [[ -L a ]] && echo y;",
		"y\n",
	},
	{
		"[[ \"multiline\ntext\" == *text* ]] && echo x; [[ \"multiline\ntext\" == *multiline* ]] && echo y",
		"x\ny\n",
	},
	// * should match a newline
	{
		"[[ \"multiline\ntext\" == multiline*text ]] && echo x",
		"x\n",
	},
	{
		"[[ \"multiline\ntext\" == text ]]",
		"exit status 1",
	},
	{
		`case $'a\nb' in a*b) echo match ;; esac`,
		"match\n",
	},
	{
		`a=$'a\nb'; echo "${a/a*b/sub}"`,
		"sub\n",
	},
	{
		"mkdir a; cd a; test -f b && echo x; >b; test -f b && echo y",
		"y\n",
	},
	{
		">a; [[ -b a ]] && echo block; [[ -c a ]] && echo char; true",
		"",
	},
	{
		"[[ -e /dev/sda ]] || { echo block; exit; }; [[ -b /dev/sda ]] && echo block; [[ -c /dev/sda ]] && echo char; true",
		"block\n",
	},
	{
		"[[ -e /dev/nvme0n1 ]] || { echo block; exit; }; [[ -b /dev/nvme0n1 ]] && echo block; [[ -c /dev/nvme0n1 ]] && echo char; true",
		"block\n",
	},
	{
		"[[ -e /dev/tty ]] || { echo char; exit; }; [[ -b /dev/tty ]] && echo block; [[ -c /dev/tty ]] && echo char; true",
		"char\n",
	},
	{"[[ -t 1 ]]", "exit status 1"},
	{"[[ -t 1234 ]]", "exit status 1"},
	{"[[ -o wrong ]]", "exit status 1"},
	{"[[ -o errexit ]]", "exit status 1"},
	{"set -e; [[ -o errexit ]]", ""},
	{"[[ -o noglob ]]", "exit status 1"},
	{"set -f; [[ -o noglob ]]", ""},
	{"[[ -o allexport ]]", "exit status 1"},
	{"set -a; [[ -o allexport ]]", ""},
	{"[[ -o nounset ]]", "exit status 1"},
	{"set -u; [[ -o nounset ]]", ""},
	{"[[ -o noexec ]]", "exit status 1"},
	{"set -n; [[ -o noexec ]]", ""}, // actually does nothing, but oh well
	{"[[ -o pipefail ]]", "exit status 1"},
	{"set -o pipefail; [[ -o pipefail ]]", ""},
	// TODO: we don't implement precedence of && over ||.
	// {"[[ a == x && b == x || c == c ]]", ""},
	{"[[ (a == x && b == x) || c == c ]]", ""},
	{"[[ a == x && (b == x || c == c) ]]", "exit status 1"},

	// classic test
	{
		"[",
		"1:1: [: missing matching ]\nexit status 2 #JUSTERR",
	},
	{
		"[ a",
		"1:1: [: missing matching ]\nexit status 2 #JUSTERR",
	},
	{
		"[ a b c ]",
		"1:1: not a valid test operator: b\nexit status 2 #JUSTERR",
	},
	{
		"[ a -a ]",
		"1:1: -a must be followed by an expression\nexit status 2 #JUSTERR",
	},
	{"[ a ]", ""},
	{"[ -n ]", ""},
	{"[ '-n' ]", ""},
	{"[ -z ]", ""},
	{"[ ! ]", ""},
	{"[ a != b ]", ""},
	{"[ ! a '==' a ]", "exit status 1"},
	{"[ a -a 0 -gt 1 ]", "exit status 1"},
	{"[ 0 -gt 1 -o 1 -gt 0 ]", ""},
	{"[ 3 -gt 4 ]", "exit status 1"},
	{"[ 3 -lt 4 ]", ""},
	{"[ ' 3' -lt '4 ' ]", ""},
	{
		"[ -e a ] && echo x; >a; [ -e a ] && echo y",
		"y\n",
	},
	{
		"test 3 -gt 4",
		"exit status 1",
	},
	{
		"test 3 -lt 4",
		"",
	},
	{
		"test 3 -lt",
		"1:1: -lt must be followed by a word\nexit status 2 #JUSTERR",
	},
	{
		"touch -t 202111050000.00 a; touch -t 202111060000.00 b; [ a -nt b ]",
		"exit status 1",
	},
	{
		"touch -t 202111050000.00 a; touch -t 202111060000.00 b; [ a -ot b ]",
		"",
	},
	{
		">a; [ a -ef a ]",
		"",
	},
	{"[ 3 -eq 04 ]", "exit status 1"},
	{"[ 3 -eq 03 ]", ""},
	{"[ 3 -ne 03 ]", "exit status 1"},
	{"[ 3 -le 4 ]", ""},
	{"[ 3 -ge 4 ]", "exit status 1"},
	{
		"[ -d a ] && echo x; mkdir a; [ -d a ] && echo y",
		"y\n",
	},
	{
		"[ -r a ] && echo x; >a; [ -r a ] && echo y",
		"y\n",
	},
	{
		"[ -w a ] && echo x; >a; [ -w a ] && echo y",
		"y\n",
	},
	{
		// A directory is readable, writable, and executable.
		"mkdir d; [ -r d ] && echo r; [ -w d ] && echo w; [ -x d ] && echo x",
		"r\nw\nx\n",
	},
	{
		"[ -s a ] && echo x; echo body >a; [ -s a ] && echo y",
		"y\n",
	},
	{
		"[ -L a ] && echo x; ln -s b a; [ -L a ] && echo y;",
		"y\n",
	},
	{
		">a; [ -b a ] && echo block; [ -c a ] && echo char; true",
		"",
	},
	{"[ -t 1 ]", "exit status 1"},
	{"[ -t 1234 ]", "exit status 1"},
	{"[ -o wrong ]", "exit status 1"},
	{"[ -o errexit ]", "exit status 1"},
	{"set -e; [ -o errexit ]", ""},
	{"a=x b=''; [ -v a -a -v b -a ! -v c ]", ""},
	{"[ a = a ]", ""},
	{"[ a != a ]", "exit status 1"},
	{"[ abc = ab* ]", "exit status 1"},
	{"[ abc != ab* ]", ""},
	// TODO: we don't implement precedence of -a over -o.
	// {"[ a = x -a b = x -o c = c ]", ""},
	{`[ \( a = x -a b = x \) -o c = c ]`, ""},
	{`[ a = x -a \( b = x -o c = c \) ]`, "exit status 1"},

	// arithm
	{
		"echo $((1 == +1))",
		"1\n",
	},
	{
		"echo $((!0))",
		"1\n",
	},
	{
		"echo $((!3))",
		"0\n",
	},
	{
		"echo $((~0))",
		"-1\n",
	},
	{
		"echo $((~3))",
		"-4\n",
	},
	{
		"echo $((1 + 2 - 3))",
		"0\n",
	},
	{
		"echo $((-1 * 6 / 2))",
		"-3\n",
	},
	{
		"a=2; echo $(( a + $a + c ))",
		"4\n",
	},
	{
		"a=b; b=c; c=5; echo $((a % 3))",
		"2\n",
	},
	{
		"echo $((2 > 2 || 2 < 2))",
		"0\n",
	},
	{
		"echo $((2 >= 2 && 2 <= 2))",
		"1\n",
	},
	{
		"echo $(((1 & 2) != (1 | 2)))",
		"1\n",
	},
	{
		"echo $a; echo $((a = 3 ^ 2)); echo $a",
		"\n1\n1\n",
	},
	{
		"echo $((a += 1, a *= 2, a <<= 2, a >> 1))",
		"4\n",
	},
	{
		"echo $((a -= 10, a /= 2, a >>= 1, a << 1))",
		"-6\n",
	},
	{
		"echo $((a |= 3, a &= 1, a ^= 8, a %= 5, a))",
		"4\n",
	},
	{
		"echo $((a = 3, ++a, a--))",
		"4\n",
	},
	{
		"echo $((2 ** 3)) $((1234 ** 4567))",
		"8 0\n",
	},
	{
		"echo $((1 ? 2 : 3)) $((0 ? 2 : 3))",
		"2 3\n",
	},
	{
		"((1))",
		"",
	},
	{
		"((3 == 4))",
		"exit status 1",
	},
	{
		"let i=(3+4); let i++; echo $i; let i--; echo $i",
		"8\n7\n",
	},
	{
		"let 3==4",
		"exit status 1",
	},
	{
		"a=1; let a++; echo $a",
		"2\n",
	},
	{
		"a=$((1 + 2)); echo $a",
		"3\n",
	},
	{
		"x=3; echo $(($x)) $((x))",
		"3 3\n",
	},
	{
		"set -- 1; echo $(($@))",
		"1\n",
	},
	{
		"a=b b=a; echo $(($a))",
		"0\n #IGNORE bash prints a warning",
	},
	{
		"let x=3; let 3/0; ((3/0)); echo $((x/y)); let x/=0",
		"division by zero\ndivision by zero\ndivision by zero\ndivision by zero\nexit status 1 #JUSTERR",
	},
	{
		"let x=3; let 3%0; ((3%0)); echo $((x%y)); let x%=0",
		"division by zero\ndivision by zero\ndivision by zero\ndivision by zero\nexit status 1 #JUSTERR",
	},
	{
		"let x=' 3'; echo $x",
		"3\n",
	},
	{
		"x=' 3'; let x++; echo \"$x\"",
		"4\n",
	},

	// set/shift
	{
		"echo $#; set foo_interp_missing bar_interp_missing; echo $#",
		"0\n2\n",
	},
	{
		"shift; set a b c; shift; echo $@",
		"b c\n",
	},
	{
		"shift 2; set a b c; shift 2; echo $@",
		"c\n",
	},
	{
		`echo $#; set '' ""; echo $#`,
		"0\n2\n",
	},
	{
		"set -- a b; echo $#",
		"2\n",
	},
	{
		"set -U",
		"set: invalid option: \"-U\"\nexit status 2 #JUSTERR",
	},
	{
		"set -e; false; echo foo_interp_missing",
		"exit status 1",
	},
	{
		"set -e; shouldnotexist; echo foo_interp_missing",
		"\"shouldnotexist\": executable file not found in $PATH\nexit status 127 #JUSTERR",
	},
	{
		"set -e; set +e; false; echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"set -e; ! false; echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"set -e; ! true; echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"set -e; if false; then echo never; fi; echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"set -e; while false; do echo never; done; echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"set -e; false || true; echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"set -e; false && true; echo foo_interp_missing",
		"foo_interp_missing\n",
	},
	{
		"set -e; true && false; echo foo_interp_missing",
		"exit status 1",
	},
	{
		"false | :",
		"",
	},
	{
		// Important that we don't print in these, as otherwise we get "broken pipe" errors.
		"GOSH_CMD=exit_5 $GOSH_PROG | GOSH_CMD=exit_0 $GOSH_PROG",
		"",
	},
	{
		"set -o pipefail; false | :",
		"exit status 1",
	},
	{
		"set -o pipefail; GOSH_CMD=exit_5 $GOSH_PROG | GOSH_CMD=exit_0 $GOSH_PROG",
		"exit status 5",
	},
	{
		"set -o pipefail; true | false | true | :",
		"exit status 1",
	},
	{
		"set -o pipefail; set -M 2>/dev/null | false",
		"exit status 1",
	},
	{
		"set -o pipefail; false | :; echo next",
		"next\n",
	},
	{
		"set -o pipefail; exit 0 | :; echo next",
		"next\n",
	},
	{
		"set -o pipefail; exit 1 | :; echo next",
		"next\n",
	},
	{
		"set -e -o pipefail; false | :; echo next",
		"exit status 1",
	},
	{
		"exit 0 && true; echo foo_interp_missing",
		"",
	},
	{
		"exit 1 && true; echo foo_interp_missing",
		"exit status 1",
	},
	{
		"set -f; >a.x; echo *.x;",
		"*.x\n",
	},
	{
		"set -f; set +f; >a.x; echo *.x;",
		"a.x\n",
	},
	{
		"set -a; foo_interp_missing=bar_interp_missing; $ENV_PROG | grep ^foo_interp_missing=",
		"foo_interp_missing=bar_interp_missing\n",
	},
	{
		"set -a; foo_interp_missing=(b a r); $ENV_PROG | grep ^foo_interp_missing=",
		"exit status 1",
	},
	{
		"foo_interp_missing=bar_interp_missing; set -a; $ENV_PROG | grep ^foo_interp_missing=",
		"exit status 1",
	},
	{
		"a=b; echo $a; set -u; echo $a",
		"b\nb\n",
	},
	{
		"echo $a; set -u; echo $a; echo extra",
		"\na: unbound variable\nexit status 1 #JUSTERR",
	},
	{
		"foo_interp_missing=bar_interp_missing; set -u; echo ${foo_interp_missing/bar_interp_missing/}",
		"\n",
	},
	{
		"foo_interp_missing=bar_interp_missing; set -u; echo ${foo_interp_missing#bar_interp_missing}",
		"\n",
	},
	{
		"set -u; echo ${foo_interp_missing/bar_interp_missing/}",
		"foo_interp_missing: unbound variable\nexit status 1 #JUSTERR",
	},
	{
		"set -u; echo ${foo_interp_missing#bar_interp_missing}",
		"foo_interp_missing: unbound variable\nexit status 1 #JUSTERR",
	},
	// TODO: detect this case as unset
	// {
	// 	"set -u; foo_interp_missing=(bar_interp_missing); echo $foo_interp_missing; echo ${foo_interp_missing[3]}",
	// 	"bar_interp_missing\nfoo_interp_missing: unbound variable\nexit status 1 #JUSTERR",
	// },
	{
		"set -u; foo_interp_missing=(''); echo ${foo_interp_missing[0]}",
		"\n",
	},
	{
		"set -u; echo ${#foo_interp_missing}",
		"foo_interp_missing: unbound variable\nexit status 1 #JUSTERR",
	},
	{
		"set -u; echo ${foo_interp_missing+bar_interp_missing}",
		"\n",
	},
	{
		"set -u; echo ${foo_interp_missing:+bar_interp_missing}",
		"\n",
	},
	{
		"set -u; echo ${foo_interp_missing-bar_interp_missing}",
		"bar_interp_missing\n",
	},
	{
		"set -u; echo ${foo_interp_missing:-bar_interp_missing}",
		"bar_interp_missing\n",
	},
	{
		"set -u; echo ${foo_interp_missing=bar_interp_missing}",
		"bar_interp_missing\n",
	},
	{
		"set -u; echo ${foo_interp_missing:=bar_interp_missing}",
		"bar_interp_missing\n",
	},
	{
		"set -u; echo ${foo_interp_missing?bar_interp_missing}",
		"foo_interp_missing: bar_interp_missing\nexit status 1 #JUSTERR",
	},
	{
		"set -u; echo ${foo_interp_missing:?bar_interp_missing}",
		"foo_interp_missing: bar_interp_missing\nexit status 1 #JUSTERR",
	},
	{
		"set -ue; set -ueo pipefail",
		"",
	},
	{"set -n; echo foo_interp_missing", ""},
	{"set -n; [ wrong", ""},
	{"set -n; set +n; echo foo_interp_missing", ""},
	{
		"set -o foo_interp_missingbar_interp_missing",
		"set: invalid option: \"foo_interp_missingbar_interp_missing\"\nexit status 2 #JUSTERR",
	},
	{"set -o noexec; echo foo_interp_missing", ""},
	{"set +o noexec; echo foo_interp_missing", "foo_interp_missing\n"},
	{"set -e; set -o | grep -E 'errexit|noexec' | wc -l | tr -d ' '", "2\n"},
	{"set -e; set -o | grep -E 'errexit|noexec' | grep 'on$' | wc -l | tr -d ' '", "1\n"},
	{
		"set -a; set +o",
		`set -o allexport
set +o errexit
set +o noexec
set +o noglob
set +o nounset
set +o xtrace
set +o pipefail
 #IGNORE`,
	},
	{`set - foobar; echo $@; set -; echo $@`, "foobar\nfoobar\n"},

	// unset
	{
		"a=1; echo $a; unset a; echo $a",
		"1\n\n",
	},
	{
		"notinpath() { echo func; }; notinpath; unset -f notinpath; notinpath",
		"func\n\"notinpath\": executable file not found in $PATH\nexit status 127 #JUSTERR",
	},
	{
		"a=1; a() { echo func; }; unset -f a; echo $a",
		"1\n",
	},
	{
		"a=1; a() { echo func; }; unset -v a; a; echo $a",
		"func\n\n",
	},
	{
		"notinpath=1; notinpath() { echo func; }; notinpath; echo $notinpath; unset notinpath; notinpath; echo $notinpath; unset notinpath; notinpath",
		"func\n1\nfunc\n\n\"notinpath\": executable file not found in $PATH\nexit status 127 #JUSTERR",
	},
	{
		"unset PATH; [[ $PATH == '' ]]",
		"",
	},
	{
		"readonly a=1; echo $a; unset a; echo $a",
		"1\na: readonly variable\n1\n #IGNORE bash prints a warning",
	},
	{
		"f() { local a=1; echo $a; unset a; echo $a; }; f",
		"1\n\n",
	},
	{
		`a=b eval 'echo $a; unset a; echo $a'`,
		"b\n\n",
	},
	{
		`$(unset INTERP_GLOBAL); echo $INTERP_GLOBAL; unset INTERP_GLOBAL; echo $INTERP_GLOBAL`,
		"value\n\n",
	},
	{
		`x=orig; f() { local x=local; unset x; x=still_local; }; f; echo $x`,
		"orig\n",
	},
	{
		`x=orig; f() { local x=local; unset x; [[ -v x ]] && echo set || echo unset; }; f`,
		"unset\n",
	},
	{
		`PS3="pick one: "; select opt in foo bar baz; do echo "Selected $opt"; break; done <<< 3`,
		"1) foo\n2) bar\n3) baz\npick one: Selected baz\n",
	},
	{
		`opts=(foo bar baz); select opt in ${opts[@]}; do echo "Selected $opt"; break; done <<< 99`,
		"1) foo\n2) bar\n3) baz\n#? Selected \n",
	},
	{
		`select opt in foo; do
	case $opt in
	foo) echo "option 1"; break;;
	*) echo "invalid option $REPLY"; break;;
	esac
done <<< 2`,
		"1) foo\n#? invalid option 2\n",
	},

	// shopt
	{"set -e; shopt -o | grep -E 'errexit|noexec' | wc -l | tr -d ' '", "2\n"},
	{"set -e; shopt -o | grep -E 'errexit|noexec' | grep 'on$' | wc -l | tr -d ' '", "1\n"},
	{"shopt -s -o noexec; echo foo_interp_missing", ""},
	{"shopt -so noexec; echo foo_interp_missing", ""},
	{"shopt -u -o noexec; echo foo_interp_missing", "foo_interp_missing\n"},
	{"shopt -u globstar; shopt globstar | grep 'off$' | wc -l | tr -d ' '", "1\n"},
	{"shopt -s globstar; shopt globstar | grep 'off$' | wc -l | tr -d ' '", "0\n"},
	{"shopt extglob | grep 'off' | wc -l | tr -d ' '", "1\n"},
	{
		"shopt inherit_errexit",
		"inherit_errexit\ton\t(\"off\" not supported)\n #JUSTERR",
	},
	{
		"shopt -s extglob",
		"shopt: invalid option name \"extglob\" \"off\" (\"on\" not supported)\nexit status 1 #IGNORE",
	},
	{
		"shopt -s interactive_comments",
		"shopt: invalid option name \"interactive_comments\" \"on\" (\"off\" not supported)\nexit status 1 #IGNORE",
	},
	{
		"shopt -s foo",
		"shopt: invalid option name \"foo\"\nexit status 1 #JUSTERR",
	},
	{
		// Beware that macOS file systems are by default case-preserving but
		// case-insensitive, so e.g. "touch x X" creates only one file.
		"touch a ab Ac Ad; shopt -u nocaseglob; echo a*",
		"a ab\n",
	},
	{
		"touch a ab Ac Ad; shopt -s nocaseglob; echo a*",
		"Ac Ad a ab\n",
	},
	{
		"touch a ab abB Ac Ad; shopt -u nocaseglob; echo *b",
		"ab\n",
	},
	{
		"touch a ab abB Ac Ad; shopt -s nocaseglob; echo *b",
		"ab abB\n",
	},

	// IFS
	{`echo -n "$IFS"`, " \t\n"},
	{`a="x:y:z"; IFS=:; echo $a`, "x y z\n"},
	{`a=(x y z); IFS=-; echo ${a[*]}`, "x y z\n"},
	{`a=(x y z); IFS=-; echo ${a[@]}`, "x y z\n"},
	{`a=(x y z); IFS=-; echo "${a[*]}"`, "x-y-z\n"},
	{`a=(x y z); IFS=-; echo "${a[@]}"`, "x y z\n"},
	{`a="  x y z"; IFS=; echo $a`, "  x y z\n"},
	{`a=(x y z); IFS=; echo "${a[*]}"`, "xyz\n"},
	{`a=(x y z); IFS=-; echo "${!a[@]}"`, "0 1 2\n"},
	{`set -- x y z; IFS=-; echo $*`, "x y z\n"},
	{`set -- x y z; IFS=-; echo "$*"`, "x-y-z\n"},
	{`set -- x y z; IFS=; echo $*`, "x y z\n"},
	{`set -- x y z; IFS=; echo "$*"`, "xyz\n"},

	// builtin
	{"builtin", ""},
	{"builtin noexist", "exit status 1 #JUSTERR"},
	{"builtin echo foo_interp_missing", "foo_interp_missing\n"},
	{
		"echo() { printf 'bar_interp_missing\n'; }; echo foo_interp_missing; builtin echo foo_interp_missing",
		"bar_interp_missing\nfoo_interp_missing\n",
	},

	// type
	{"type", ""},
	{"type for", "for is a shell keyword\n"},
	{"type echo", "echo is a shell builtin\n"},
	{"echo() { :; }; type echo | grep 'is a function'", "echo is a function\n"},
	{"type $PATH_PROG | grep -q -E ' is (/|[A-Z]:)'", ""},
	{"type noexist", "type: noexist: not found\nexit status 1 #JUSTERR"},
	{"PATH=/; type $PATH_PROG", "type: " + pathProg + ": not found\nexit status 1 #JUSTERR"},
	{"shopt -s expand_aliases; alias foo_interp_missing='bar_interp_missing baz'\ntype foo_interp_missing", "foo_interp_missing is aliased to `bar_interp_missing baz'\n"},
	{"alias foo_interp_missing='bar_interp_missing baz'\ntype foo_interp_missing", "type: foo_interp_missing: not found\nexit status 1 #JUSTERR"},
	{"type -p $PATH_PROG | grep -q -E '^(/|[A-Z]:)'", ""},
	{"PATH=/; type -p $PATH_PROG", "exit status 1"},
	{"shopt -s expand_aliases; alias foo_interp_missing='bar_interp_missing'; type -t foo_interp_missing", "alias\n"},
	{"type -t case", "keyword\n"},
	{"foo_interp_missing(){ :; }; type -t foo_interp_missing", "function\n"},
	{"type -t type", "builtin\n"},
	{"type -t $PATH_PROG", "file\n"},
	{"type -t inexisting_dfgsdgfds", "exit status 1"},

	// trap
	{"trap 'echo at_exit' EXIT; true", "at_exit\n"},
	{"trap 'echo on_err' ERR; false; echo FAIL", "on_err\nFAIL\n"},
	{"trap 'echo on_err' ERR; false || true; echo OK", "OK\n"},
	{"trap 'echo at_exit' EXIT; trap - EXIT; echo OK", "OK\n"},
	{"set -e; trap 'echo A' ERR EXIT; false; echo FAIL", "A\nA\nexit status 1"},
	{"trap 'foo_interp_missingbar_interp_missing' UNKNOWN", "trap: UNKNOWN: invalid signal specification\nexit status 2 #JUSTERR"},
	// TODO: our builtin appears to not receive the piped bytes?
	// {"trap 'echo on_err' ERR; trap | grep -q '.*echo on_err.*'", "trap -- \"echo on_err\" ERR\n"},
	{"trap 'false' ERR EXIT; false", "exit status 1"},

	// eval
	{"eval", ""},
	{"eval ''", ""},
	{"eval echo foo_interp_missing", "foo_interp_missing\n"},
	{"eval 'echo foo_interp_missing'", "foo_interp_missing\n"},
	{"eval 'exit 1'", "exit status 1"},
	{"eval '('", "eval: 1:1: reached EOF without matching ( with )\nexit status 1 #JUSTERR"},
	{"set a b; eval 'echo $@'", "a b\n"},
	{"eval 'a=foo_interp_missing'; echo $a", "foo_interp_missing\n"},
	{`a=b eval "echo $a"`, "\n"},
	{`a=b eval 'echo $a'`, "b\n"},
	{`eval 'echo "\$a"'`, "$a\n"},
	{`a=b eval 'x=y eval "echo \$a \$x"'`, "b y\n"},
	{`a=b eval 'a=y eval "echo $a \$a"'`, "b y\n"},
	{"a=b eval '(echo $a)'", "b\n"},

	// source
	{
		"source",
		"1:1: source: need filename\nexit status 2 #JUSTERR",
	},
	{
		"echo 'echo foo_interp_missing' >a; source a; . a",
		"foo_interp_missing\nfoo_interp_missing\n",
	},
	{
		"echo 'echo $@' >a; source a; source a b c; echo $@",
		"\nb c\n\n",
	},
	{
		"echo 'foo_interp_missing=bar_interp_missing' >a; source a; echo $foo_interp_missing",
		"bar_interp_missing\n",
	},

	// source from PATH
	{
		"mkdir test; echo 'echo foo_interp_missing' >test/a; PATH=$PWD/test source a; . test/a",
		"foo_interp_missing\nfoo_interp_missing\n",
	},

	// source with set and shift
	{
		"echo 'set -- d e f' >a; source a; echo $@",
		"d e f\n",
	},
	{
		"echo 'echo $@' >a; set -- b c; source a; echo $@",
		"b c\nb c\n",
	},
	{
		"echo 'echo $@' >a; set -- b c; source a d e; echo $@",
		"d e\nb c\n",
	},
	{
		"echo 'shift; echo $@' >a; set -- b c; source a d e; echo $@",
		"e\nb c\n",
	},
	{
		"echo 'shift' >a; set -- b c; source a; echo $@",
		"c\n",
	},
	{
		"echo 'shift; set -- $@' >a; set -- b c; source a d e; echo $@",
		"e\n",
	},
	{
		"echo 'set -- g f'>b; echo 'set -- d e f; echo $@; source b;' >a; source a; echo $@",
		"d e f\ng f\n",
	},
	{
		"echo 'set -- g f'>b; echo 'echo $@; set -- d e f; source b;' >a; source a b c; echo $@",
		"b c\ng f\n",
	},
	{
		"echo 'shift; echo $@' >b; echo 'shift; echo $@; source b' >a; source a b c d; echo $@",
		"c d\nd\n\n",
	},
	{
		"echo 'set -- b c d' >b; echo 'source b' >a; set -- a; source a; echo $@",
		"b c d\n",
	},
	{
		"echo 'echo $@' >b; echo 'set -- b c d; source b' >a; set -- a; source a; echo $@",
		"b c d\nb c d\n",
	},
	{
		"echo 'shift; echo $@' >b; echo 'shift; echo $@; source b c d' >a; set -- a b; source a; echo $@",
		"b\nd\nb\n",
	},
	{
		"echo 'set -- a b c' >b; echo 'echo $@; source b; echo $@' >a; source a; echo $@",
		"\na b c\na b c\n",
	},

	// indexed arrays
	{
		"a=foo_interp_missing; echo ${a[0]} ${a[@]} ${a[x]}; echo ${a[1]}",
		"foo_interp_missing foo_interp_missing foo_interp_missing\n\n",
	},
	{
		"a=(); echo ${a[0]} ${a[@]} ${a[x]} ${a[1]}",
		"\n",
	},
	{
		"a=(b c); echo $a; echo ${a[0]}; echo ${a[1]}; echo ${a[x]}",
		"b\nb\nc\nb\n",
	},
	{
		"a=(b c); echo ${a[@]}; echo ${a[*]}",
		"b c\nb c\n",
	},
	{
		"a=(1 2 3); echo ${a[2-1]}; echo $((a[1+1]))",
		"2\n3\n",
	},
	{
		"a=(1 2) x=(); a+=b x+=c; echo ${a[@]}; echo ${x[@]}",
		"1b 2\nc\n",
	},
	{
		"a=(1 2) x=(); a+=(b c) x+=(d e); echo ${a[@]}; echo ${x[@]}",
		"1 2 b c\nd e\n",
	},
	{
		"a=bbb; a+=(c d); echo ${a[@]}",
		"bbb c d\n",
	},
	{
		`a=('a  1' 'b  2'); for e in ${a[@]}; do echo "$e"; done`,
		"a\n1\nb\n2\n",
	},
	{
		`a=('a  1' 'b  2'); for e in "${a[*]}"; do echo "$e"; done`,
		"a  1 b  2\n",
	},
	{
		`a=('a  1' 'b  2'); for e in "${a[@]}"; do echo "$e"; done`,
		"a  1\nb  2\n",
	},
	{
		`declare -a a; a[0]='a  1'; a[1]='b  2'; for e in "${a[@]}"; do echo "$e"; done`,
		"a  1\nb  2\n",
	},
	{
		`a=([1]=y [0]=x); echo ${a[0]}`,
		"x\n",
	},
	{
		`a=(y); a[2]=x; echo ${a[2]}`,
		"x\n",
	},
	{
		`a="y"; a[2]=x; echo ${a[2]}`,
		"x\n",
	},
	{
		`declare -a a=(x y); echo ${a[1]}`,
		"y\n",
	},
	{
		`a=b; echo "${a[@]}"`,
		"b\n",
	},
	{
		`a=(b); echo ${a[3]}`,
		"\n",
	},
	{
		`a=(b); echo ${a[-2]}`,
		"negative array index\n #JUSTERR",
	},
	// TODO: also test with gaps in arrays.
	{
		`a=([0]=' x ' [1]=' y '); for v in "${a[@]}"; do echo "$v"; done`,
		" x \n y \n",
	},
	{
		`a=([0]=' x ' [1]=' y '); for v in "${a[*]}"; do echo "$v"; done`,
		" x   y \n",
	},
	{
		`a=([0]=' x ' [1]=' y '); for v in "${!a[@]}"; do echo "$v"; done`,
		"0\n1\n",
	},
	{
		`a=([0]=' x ' [1]=' y '); for v in "${!a[*]}"; do echo "$v"; done`,
		"0 1\n",
	},

	// associative arrays
	{
		`a=foo_interp_missing; echo ${a[""]} ${a["x"]}`,
		"foo_interp_missing foo_interp_missing\n",
	},
	{
		`declare -A a=(); echo ${a[0]} ${a[@]} ${a[1]} ${a["x"]}`,
		"\n",
	},
	{
		`declare -A a=([x]=b [y]=c); echo $a; echo ${a[0]}; echo ${a["x"]}; echo ${a["_"]}`,
		"\n\nb\n\n",
	},
	{
		`declare -Ag a=([x]=y); echo ${a["x"]}`,
		"y\n",
	},
	{
		`declare -A a=([x]=b [y]=c); for e in ${a[@]}; do echo $e; done | sort`,
		"b\nc\n",
	},
	{
		`declare -A a=([y]=b [x]=c); for e in ${a[*]}; do echo $e; done | sort`,
		"b\nc\n",
	},
	{
		`declare -A a=([x]=a); a["y"]=d; a["x"]=c; for e in ${a[@]}; do echo $e; done | sort`,
		"c\nd\n",
	},
	{
		`declare -A a=([x]=a); a[y]=d; a[x]=c; for e in ${a[@]}; do echo $e; done | sort`,
		"c\nd\n",
	},
	{
		// cheating a little; bash just did a=c
		`a=(["x"]=b ["y"]=c); echo ${a["y"]}`,
		"c\n",
	},
	{
		`declare -A a=(['x']=b); echo ${a['x']} ${a[$'x']} ${a[$"x"]}`,
		"b b b\n",
	},
	{
		`a=(['x']=b); echo ${a['y']}`,
		"\n #IGNORE bash requires -A",
	},
	{
		`declare -A a=(['a  1']=' x ' ['b  2']=' y '); for v in "${a[@]}"; do echo "$v"; done | sort`,
		" x \n y \n",
	},
	{
		`declare -A a=(['a  1']=' x ' ['b  2']=' y '); for v in "${a[*]}"; do echo "$v"; done`,
		" x   y \n",
	},
	{
		`declare -A a=(['a  1']=' x ' ['b  2']=' y '); for v in "${!a[@]}"; do echo "$v"; done | sort`,
		"a  1\nb  2\n",
	},
	{
		`declare -A a=(['a  1']=' x ' ['b  2']=' y '); for v in "${!a[*]}"; do echo "$v"; done`,
		"a  1 b  2\n",
	},
	{
		`declare -A a; a[a]=x; a[b]=y; for v in "${!a[@]}"; do echo "$v"; done | sort`,
		"a\nb\n",
	},
	{
		`declare -A a; a[a]=x; a[b]=y; declare -A a; for v in "${!a[@]}"; do echo "$v"; done | sort`,
		"a\nb\n",
	},
	// weird assignments
	{"a=b; a=(c d); echo ${a[@]}", "c d\n"},
	{"a=(b c); a=d; echo ${a[@]}", "d c\n"},
	{"declare -A a=([x]=b [y]=c); a=d; for e in ${a[@]}; do echo $e; done | sort", "b\nc\nd\n"},
	{"i=3; a=b; a[i]=x; echo ${a[@]}", "b x\n"},
	{"i=3; declare a=(b); a[i]=x; echo ${!a[@]}", "0 3\n"},
	{"i=3; declare -A a=(['x']=b); a[i]=x; for e in ${!a[@]}; do echo $e; done | sort", "i\nx\n"},

	// declare
	{"declare -B foo_interp_missing", "declare: invalid option \"-B\"\nexit status 2 #JUSTERR"},
	{"a=b; declare a; echo $a; declare a=; echo $a", "b\n\n"},
	{"a=b; declare a; echo $a", "b\n"},
	{
		"declare a=b c=(1 2); echo $a; echo ${c[@]}",
		"b\n1 2\n",
	},
	{"a=x; declare $a; echo $a $x", "x\n"},
	{"a=x=y; declare $a; echo $a $x", "x=y y\n"},
	{"a='x=(y)'; declare $a; echo $a $x", "x=(y) (y)\n"},
	{"a='x=b y=c'; declare $a; echo $x $y", "b c\n"},
	{"declare =bar_interp_missing", "declare: invalid name \"\"\nexit status 1 #JUSTERR"},
	{"declare $unset=$unset", "declare: invalid name \"\"\nexit status 1 #JUSTERR"},

	// export
	{"declare foo_interp_missing=bar_interp_missing; $ENV_PROG | grep '^foo_interp_missing='", "exit status 1"},
	{"declare -x foo_interp_missing=bar_interp_missing; $ENV_PROG | grep '^foo_interp_missing='", "foo_interp_missing=bar_interp_missing\n"},
	{"export foo_interp_missing=bar_interp_missing; $ENV_PROG | grep '^foo_interp_missing='", "foo_interp_missing=bar_interp_missing\n"},
	{"foo_interp_missing=bar_interp_missing; export foo_interp_missing; $ENV_PROG | grep '^foo_interp_missing='", "foo_interp_missing=bar_interp_missing\n"},
	{"export foo_interp_missing=bar_interp_missing; foo_interp_missing=baz; $ENV_PROG | grep '^foo_interp_missing='", "foo_interp_missing=baz\n"},
	{"export foo_interp_missing=bar_interp_missing; readonly foo_interp_missing=baz; $ENV_PROG | grep '^foo_interp_missing='", "foo_interp_missing=baz\n"},
	{"export foo_interp_missing=(1 2); $ENV_PROG | grep '^foo_interp_missing='", "exit status 1"},
	{"declare -A foo_interp_missing=([a]=b); export foo_interp_missing; $ENV_PROG | grep '^foo_interp_missing='", "exit status 1"},
	{"export foo_interp_missing=(b c); foo_interp_missing=x; $ENV_PROG | grep '^foo_interp_missing='", "exit status 1"},
	{"foo_interp_missing() { bar_interp_missing=foo_interp_missing; export bar_interp_missing; }; foo_interp_missing; $ENV_PROG | grep ^bar_interp_missing=", "bar_interp_missing=foo_interp_missing\n"},
	{"foo_interp_missing() { export bar_interp_missing; }; bar_interp_missing=foo_interp_missing; foo_interp_missing; $ENV_PROG | grep ^bar_interp_missing=", "bar_interp_missing=foo_interp_missing\n"},
	{"foo_interp_missing() { export bar_interp_missing; }; foo_interp_missing; bar_interp_missing=foo_interp_missing; $ENV_PROG | grep ^bar_interp_missing=", "bar_interp_missing=foo_interp_missing\n"},
	{"foo_interp_missing() { export bar_interp_missing=foo_interp_missing; }; foo_interp_missing; readonly bar_interp_missing; $ENV_PROG | grep ^bar_interp_missing=", "bar_interp_missing=foo_interp_missing\n"},

	// local
	{
		"local a=b",
		"local: can only be used in a function\nexit status 1 #JUSTERR",
	},
	{
		"local a=b 2>/dev/null; echo $a",
		"\n",
	},
	{
		"{ local a=b; }",
		"local: can only be used in a function\nexit status 1 #JUSTERR",
	},
	{
		"echo 'local a=b' >a; source a",
		"local: can only be used in a function\nexit status 1 #JUSTERR",
	},
	{
		"echo 'local a=b' >a; f() { source a; }; f; echo $a",
		"\n",
	},
	{
		"f() { local a=b; }; f; echo $a",
		"\n",
	},
	{
		"a=x; f() { local a=b; }; f; echo $a",
		"x\n",
	},
	{
		"a=x; f() { echo $a; local a=b; echo $a; }; f",
		"x\nb\n",
	},
	{
		"f1() { local a=b; }; f2() { f1; echo $a; }; f2",
		"\n",
	},
	{
		"f() { a=1; declare b=2; export c=3; readonly d=4; declare -g e=5; }; f; echo $a $b $c $d $e",
		"1 3 4 5\n",
	},
	{
		`f() { local x; [[ -v x ]] && echo set || echo unset; }; f`,
		"unset\n",
	},
	{
		`f() { local x=; [[ -v x ]] && echo set || echo unset; }; f`,
		"set\n",
	},
	{
		`export x=before; f() { local x; export x=after; $ENV_PROG | grep '^x='; }; f; echo $x`,
		"x=after\nbefore\n",
	},
	{
		"getx() { echo $X; }; f() { local X=Y; getx; echo $X; }; f",
		"Y\nY\n",
	},
	{
		"setx() { X=Y; }; f() { local X; setx; echo $X; }; f",
		"Y\n",
	},
	{
		"setx() { local X=Y; }; f() { local X; setx; echo $X; }; f",
		"\n",
	},
	{
		"setx() { declare X=Y; }; f() { local X; setx; echo $X; }; f",
		"\n",
	},
	{
		"setx() { X=Y :; }; f() { local X; setx; echo $X; }; f",
		"\n",
	},

	// unset global from inside function
	{"f() { unset foo_interp_missing; echo $foo_interp_missing; }; foo_interp_missing=bar_interp_missing; f", "\n"},
	{"f() { unset foo_interp_missing; }; foo_interp_missing=bar_interp_missing; f; echo $foo_interp_missing", "\n"},

	// name references
	{"declare -n foo_interp_missing=bar_interp_missing; bar_interp_missing=etc; [[ -R foo_interp_missing ]]", ""},
	{"declare -n foo_interp_missing=bar_interp_missing; bar_interp_missing=etc; [ -R foo_interp_missing ]", ""},
	{"nameref foo_interp_missing=bar_interp_missing; bar_interp_missing=etc; [[ -R foo_interp_missing ]]", " #IGNORE"},
	{"declare foo_interp_missing=bar_interp_missing; bar_interp_missing=etc; [[ -R foo_interp_missing ]]", "exit status 1"},
	{
		"declare -n foo_interp_missing=bar_interp_missing; bar_interp_missing=etc; echo $foo_interp_missing; bar_interp_missing=zzz; echo $foo_interp_missing",
		"etc\nzzz\n",
	},
	{
		"declare -n foo_interp_missing=bar_interp_missing; bar_interp_missing=(x y); echo ${foo_interp_missing[1]}; bar_interp_missing=(a b); echo ${foo_interp_missing[1]}",
		"y\nb\n",
	},
	{
		"declare -n foo_interp_missing=bar_interp_missing; bar_interp_missing=etc; echo $foo_interp_missing; unset bar_interp_missing; echo $foo_interp_missing",
		"etc\n\n",
	},
	{
		"declare -n a1=a2 a2=a3 a3=a4; a4=x; echo $a1 $a3",
		"x x\n",
	},
	{
		"declare -n foo_interp_missing=bar_interp_missing bar_interp_missing=foo_interp_missing; echo $foo_interp_missing",
		"\n #IGNORE",
	},
	{
		"declare -n foo_interp_missing=bar_interp_missing; echo $foo_interp_missing",
		"\n",
	},
	{
		"declare -n foo_interp_missing=bar_interp_missing; echo ${!foo_interp_missing}",
		"bar_interp_missing\n",
	},
	{
		"declare -n foo_interp_missing=bar_interp_missing; bar_interp_missing=etc; echo $foo_interp_missing; echo ${!foo_interp_missing}",
		"etc\nbar_interp_missing\n",
	},
	{
		"declare -n foo_interp_missing=bar_interp_missing; bar_interp_missing=etc; foo_interp_missing=xxx; echo $foo_interp_missing $bar_interp_missing",
		"xxx xxx\n",
	},
	{
		"declare -n foo_interp_missing=bar_interp_missing; foo_interp_missing=xxx; echo $foo_interp_missing $bar_interp_missing",
		"xxx xxx\n",
	},
	// TODO: figure this one out
	//{
	//        "declare -n foo_interp_missing=bar_interp_missing bar_interp_missing=baz; foo_interp_missing=xxx; echo $foo_interp_missing $bar_interp_missing; echo $baz",
	//        "xxx xxx\nxxx\n",
	//},
	{
		"echo ${!@}-${!*}; set -- foo_interp_missing; echo ${!@}-${!*}-${!1}; foo_interp_missing=value; echo ${!@}-${!*}-${!1}",
		"-\n--\nvalue-value-value\n",
	},

	// read-only vars
	{"declare -r foo_interp_missing=bar_interp_missing; echo $foo_interp_missing", "bar_interp_missing\n"},
	{"readonly foo_interp_missing=bar_interp_missing; echo $foo_interp_missing", "bar_interp_missing\n"},
	{"readonly foo_interp_missing=bar_interp_missing; export foo_interp_missing; echo $foo_interp_missing", "bar_interp_missing\n"},
	{"readonly foo_interp_missing=bar_interp_missing; readonly bar_interp_missing=foo_interp_missing; export foo_interp_missing bar_interp_missing; echo $bar_interp_missing", "foo_interp_missing\n"},
	{
		"a=b; a=c; echo $a; readonly a; a=d",
		"c\na: readonly variable\nexit status 1 #JUSTERR",
	},
	{
		"declare -r foo_interp_missing=bar_interp_missing; foo_interp_missing=etc",
		"foo_interp_missing: readonly variable\nexit status 1 #JUSTERR",
	},
	{
		"declare -r foo_interp_missing=bar_interp_missing; export foo_interp_missing=",
		"foo_interp_missing: readonly variable\nexit status 1 #JUSTERR",
	},
	{
		"readonly foo_interp_missing=bar_interp_missing; foo_interp_missing=etc",
		"foo_interp_missing: readonly variable\nexit status 1 #JUSTERR",
	},
	{
		"foo_interp_missing() { bar_interp_missing=foo_interp_missing; readonly bar_interp_missing; }; foo_interp_missing; bar_interp_missing=bar_interp_missing",
		"bar_interp_missing: readonly variable\nexit status 1 #JUSTERR",
	},
	{
		"foo_interp_missing() { readonly bar_interp_missing; }; foo_interp_missing; bar_interp_missing=foo_interp_missing",
		"bar_interp_missing: readonly variable\nexit status 1 #JUSTERR",
	},
	{
		"foo_interp_missing() { readonly bar_interp_missing=foo_interp_missing; }; foo_interp_missing; export bar_interp_missing; $ENV_PROG | grep '^bar_interp_missing='",
		"bar_interp_missing=foo_interp_missing\n",
	},

	// multiple var modes at once
	{
		"declare -r -x foo_interp_missing=bar_interp_missing; $ENV_PROG | grep '^foo_interp_missing='",
		"foo_interp_missing=bar_interp_missing\n",
	},
	{
		"declare -r -x foo_interp_missing=bar_interp_missing; foo_interp_missing=x",
		"foo_interp_missing: readonly variable\nexit status 1 #JUSTERR",
	},

	// globbing
	{"echo .", ".\n"},
	{"echo ..", "..\n"},
	{"echo ./.", "./.\n"},
	{
		">a.x >b.x >c.x; echo *.x; rm a.x b.x c.x",
		"a.x b.x c.x\n",
	},
	{
		`>a.x; echo '*.x' "*.x"; rm a.x`,
		"*.x *.x\n",
	},
	{
		`>a.x >b.y; echo *'.'x; rm a.x`,
		"a.x\n",
	},
	{
		`>a.x; echo *'.x' "a."* '*'.x; rm a.x`,
		"a.x a.x *.x\n",
	},
	{
		"echo *.x; echo foo_interp_missing *.y bar_interp_missing",
		"*.x\nfoo_interp_missing *.y bar_interp_missing\n",
	},
	{
		"mkdir a; >a/b.x; echo */*.x | sed 's@\\\\@/@g'; cd a; echo *.x",
		"a/b.x\nb.x\n",
	},
	{
		"mkdir -p a/b/c; echo a/* | sed 's@\\\\@/@g'",
		"a/b\n",
	},
	{
		">.hidden >a; echo *; echo .h*; rm .hidden a",
		"a\n.hidden\n",
	},
	{
		`mkdir d; >d/.hidden >d/a; set -- "$(echo d/*)" "$(echo d/.h*)"; echo ${#1} ${#2}; rm -r d`,
		"3 9\n",
	},
	{
		"mkdir -p a/b/c; echo a/** | sed 's@\\\\@/@g'",
		"a/b\n",
	},
	{
		"shopt -s globstar; mkdir -p a/b/c; echo a/** | sed 's@\\\\@/@g'",
		"a/ a/b a/b/c\n",
	},
	{
		"shopt -s globstar; mkdir -p a/b/c; echo **/c | sed 's@\\\\@/@g'",
		"a/b/c\n",
	},
	{
		"shopt -s globstar; mkdir -p a/b; touch c; echo ** | sed 's@\\\\@/@g'",
		"a a/b c\n",
	},
	{
		"shopt -s globstar; mkdir -p a/b; touch c; echo **/ | sed 's@\\\\@/@g'",
		"a/ a/b/\n",
	},
	{
		"shopt -s globstar; mkdir -p a/b/c a/d; echo ** | sed 's@\\\\@/@g'",
		"a a/b a/b/c a/d\n",
	},
	{
		"shopt -s globstar; mkdir -p a.x a/b.x a/b/c.x; echo **.x ./**.x | sed 's@\\\\@/@g'",
		"a.x ./a.x\n",
	},
	{
		"mkdir foo; touch foo/bar; echo */bar */bar/ | sed 's@\\\\@/@g'",
		"foo/bar */bar/\n",
	},
	{
		"shopt -s nullglob; touch existing-1; echo missing-* existing-*",
		"existing-1\n",
	},
	// Extended globbing is not supported
	{"ls ab+(2|3).txt", "extended globbing is not supported\nexit status 1 #JUSTERR"},
	{"echo *(/)", "extended globbing is not supported\nexit status 1 #JUSTERR"},
	{`if [[ "foo" == @(foo|bar) ]]; then exit 1; else exit 1; fi`, "extended globbing is not supported\n #JUSTERR"},
	// Ensure that setting nullglob does not return invalid globs as null
	// strings.
	{
		"shopt -s nullglob; [ -n butter ] && echo bubbles",
		"bubbles\n",
	},
	{
		"cat <<EOF\n{foo_interp_missing,bar_interp_missing}\nEOF",
		"{foo_interp_missing,bar_interp_missing}\n",
	},
	{
		"cat <<EOF\n*.go\nEOF",
		"*.go\n",
	},
	{
		"mkdir -p a/b a/c; echo ./a/* | sed 's@\\\\@/@g'",
		"./a/b ./a/c\n",
	},
	{
		"mkdir -p a/b a/c d; cd d; echo ../a/* | sed 's@\\\\@/@g'",
		"../a/b ../a/c\n",
	},
	{
		"mkdir x-d1 x-d2; >x-f; echo x-*/ | sed 's@\\\\@/@g'",
		"x-d1/ x-d2/\n",
	},
	{
		"mkdir x-d1 x-d2; >x-f; echo ././x-*/// | sed 's@\\\\@/@g'",
		"././x-d1/ ././x-d2/\n",
	},
	{
		"mkdir -p x-d1/a x-d2/b; >x-f; echo x-*/* | sed 's@\\\\@/@g'",
		"x-d1/a x-d2/b\n",
	},
	{
		"mkdir -p foo_interp_missing/bar_interp_missing; ln -s foo_interp_missing sym; echo sy*/; echo sym/b*",
		"sym/\nsym/bar_interp_missing\n",
	},
	{
		">foo_interp_missing; ln -s foo_interp_missing sym; echo sy*; echo sy*/",
		"sym\nsy*/\n",
	},
	{
		"mkdir x-d; >x-f; test -d $PWD/x-*/",
		"",
	},
	{
		"mkdir dir; >dir/x-f; ln -s dir sym; cd sym; test -f $PWD/x-*",
		"",
	},

	// brace expansion; more exhaustive tests in the syntax package
	{"echo a}b", "a}b\n"},
	{"echo {a,b{c,d}", "{a,bc {a,bd\n"},
	{"echo a{b}", "a{b}\n"},
	{"echo a{à,世界}", "aà a世界\n"},
	{"echo a{b,c}d{e,f}g", "abdeg abdfg acdeg acdfg\n"},
	{"echo a{b{x,y},c}d", "abxd abyd acd\n"},
	{"echo a{1..", "a{1..\n"},
	{"echo a{1..2}b{4..5}c", "a1b4c a1b5c a2b4c a2b5c\n"},
	{"echo a{c..f}", "ac ad ae af\n"},
	{"echo a{4..1..1}", "a4 a3 a2 a1\n"},

	// tilde expansion
	{
		"[[ '~/foo_interp_missing' == ~/foo_interp_missing ]] || [[ ~/foo_interp_missing == '~/foo_interp_missing' ]]",
		"exit status 1",
	},
	{
		"case '~/foo_interp_missing' in ~/foo_interp_missing) echo match ;; esac",
		"",
	},
	{
		"a=~/foo_interp_missing; [[ $a == '~/foo_interp_missing' ]]",
		"exit status 1",
	},
	{
		`a=$(echo "~/foo_interp_missing"); [[ $a == '~/foo_interp_missing' ]]`,
		"",
	},
	{
		`HOME=/foo; rel=/bar; echo ~/bar ~/'bar' ~/"bar" ~/$rel ~/"$rel"`,
		"/foo/bar /foo/bar /foo/bar /foo//bar /foo//bar\n",
	},
	{
		`HOME=/foo; rel=/bar; echo ~'/bar' ~"/bar" ~$rel ~"/$rel"`,
		"~/bar ~/bar ~/bar ~//bar\n",
	},
	{
		`HOME=/foo; echo ~ ~/ ~/'' ~'' ~""`,
		"/foo /foo/ /foo/ ~ ~\n",
	},

	// /dev/null
	{"echo foo_interp_missing >/dev/null", ""},
	{"cat </dev/null", ""},

	// time - real would be slow and flaky; see TestElapsedString
	{"{ time; } |& wc | tr -s ' '", " 4 6 42\n"},
	{"{ time echo -n; } |& wc | tr -s ' '", " 4 6 42\n"},
	{"{ time -p; } |& wc | tr -s ' '", " 3 6 29\n"},
	{"{ time -p echo -n; } |& wc | tr -s ' '", " 3 6 29\n"},

	// exec
	{"exec", ""},
	{
		"exec builtin echo foo_interp_missing",
		"\"builtin\": executable file not found in $PATH\nexit status 127 #JUSTERR",
	},
	{
		"exec $GOSH_PROG 'echo foo_interp_missing'; echo bar_interp_missing",
		"foo_interp_missing\n",
	},

	// read
	{
		"read </dev/null",
		"exit status 1",
	},
	{
		"read 1</dev/null",
		"exit status 1",
	},
	{
		"read -X",
		"read: invalid option \"-X\"\nexit status 2 #JUSTERR",
	},
	{
		"read -rX",
		"read: invalid option \"-X\"\nexit status 2 #JUSTERR",
	},
	{
		"read 0ab",
		"read: invalid identifier \"0ab\"\nexit status 2 #JUSTERR",
	},
	{
		"read <<< foo_interp_missing; echo $REPLY",
		"foo_interp_missing\n",
	},
	{
		"read <<<'  a  b  c  '; echo \"$REPLY\"",
		"  a  b  c  \n",
	},
	{
		"read <<< 'y\nn\n'; echo $REPLY",
		"y\n",
	},
	{
		"read a_0 <<< foo_interp_missing; echo $a_0",
		"foo_interp_missing\n",
	},
	{
		"read a b <<< 'foo_interp_missing  bar_interp_missing  baz  '; echo \"$a\"; echo \"$b\"",
		"foo_interp_missing\nbar_interp_missing  baz\n",
	},
	{
		"while read a; do echo $a; done <<< 'a\nb\nc'",
		"a\nb\nc\n",
	},
	{
		"while read a b; do echo -e \"$a\n$b\"; done <<< '1 2\n3'",
		"1\n2\n3\n\n",
	},
	{
		`read a <<< '\\'; echo "$a"`,
		"\\\n",
	},
	{
		`read a <<< '\a\b\c'; echo "$a"`,
		"abc\n",
	},
	{
		"read -r a b <<< '1\\\t2'; echo $a; echo $b;",
		"1\\\n2\n",
	},
	{
		"echo line\\\ncontinuation | while read a; do echo $a; done",
		"linecontinuation\n",
	},
	{
		"while read a; do echo $a; GOSH_CMD=print_ok $GOSH_PROG; done <<< 'a\nb\nc'",
		"a\nexec ok\nb\nexec ok\nc\nexec ok\n",
	},
	{
		"while read a; do echo $a; GOSH_CMD=print_ok $GOSH_PROG; done <<EOF\na\nb\nc\nEOF",
		"a\nexec ok\nb\nexec ok\nc\nexec ok\n",
	},
	{
		"echo file1 >f; echo file2 >>f; while read a; do echo $a; done <f",
		"file1\nfile2\n",
	},
	// TODO: our final exit status here isn't right.
	// {
	// 	"while read a; do echo $a; GOSH_CMD=print_fail $GOSH_PROG; done <<< 'a\nb\nc'",
	// 	"a\nexec fail\nb\nexec fail\nc\nexec fail\nexit status 1",
	// },
	{
		`read -r a <<< '\\'; echo "$a"`,
		"\\\\\n",
	},
	{
		"read -r a <<< '\\a\\b\\c'; echo $a",
		"\\a\\b\\c\n",
	},
	{
		"IFS=: read a b c <<< '1:2:3'; echo $a; echo $b; echo $c",
		"1\n2\n3\n",
	},
	{
		"IFS=: read a b c <<< '1\\:2:3'; echo \"$a\"; echo $b; echo $c",
		"1:2\n3\n\n",
	},
	{
		"read -p",
		"read: -p: option requires an argument\nexit status 2 #JUSTERR",
	},
	{
		"read -X -p",
		"read: invalid option \"-X\"\nexit status 2 #JUSTERR",
	},
	{
		"read -p 'Display me as a prompt. Continue? (y/n) ' choice <<< 'y'; echo $choice",
		"Display me as a prompt. Continue? (y/n) y\n #IGNORE bash requires a terminal",
	},
	{
		"read -r -p 'Prompt and raw flag together: ' a <<< '\\a\\b\\c'; echo $a",
		"Prompt and raw flag together: \\a\\b\\c\n #IGNORE bash requires a terminal",
	},
	{
		`a=a; echo | (read a; echo -n "$a")`,
		"",
	},
	{
		`a=b; read a < /dev/null; echo -n "$a"`,
		"",
	},
	{
		"a=c; echo x | (read a; echo -n $a)",
		"x",
	},
	{
		"a=d; echo -n y | (read a; echo -n $a)",
		"y",
	},

	// getopts
	{
		"getopts",
		"getopts: usage: getopts optstring name [arg ...]\nexit status 2",
	},
	{
		"getopts a a:b",
		"getopts: invalid identifier: \"a:b\"\nexit status 2 #JUSTERR",
	},
	{
		"getopts abc opt -a; echo $opt; $optarg",
		"a\n",
	},
	{
		"getopts abc opt -z",
		"getopts: illegal option -- \"z\"\n #IGNORE",
	},
	{
		"getopts a: opt -a",
		"getopts: option requires an argument -- \"a\"\n #IGNORE",
	},
	{
		"getopts :abc opt -z; echo $opt; echo $OPTARG",
		"?\nz\n",
	},
	{
		"getopts :a: opt -a; echo $opt; echo $OPTARG",
		":\na\n",
	},
	{
		"getopts abc opt foo_interp_missing -a; echo $opt; echo $OPTIND",
		"?\n1\n",
	},
	{
		"getopts abc opt -a foo_interp_missing; echo $opt; echo $OPTIND",
		"a\n2\n",
	},
	{
		"OPTIND=3; getopts abc opt -a -b -c; echo $opt;",
		"c\n",
	},
	{
		"OPTIND=100; getopts abc opt -a -b -c; echo $opt;",
		"?\n",
	},
	{
		"OPTIND=foo_interp_missing; getopts abc opt -a -b -c; echo $opt;",
		"a\n",
	},
	{
		"while getopts ab:c opt -c -b arg -a foo_interp_missing; do echo $opt $OPTARG $OPTIND; done",
		"c 2\nb arg 4\na 5\n",
	},
	{
		"while getopts abc opt -ba -c foo_interp_missing; do echo $opt $OPTARG $OPTIND; done",
		"b 1\na 2\nc 3\n",
	},
	{
		"a() { while getopts abc: opt; do echo $opt $OPTARG; done }; a -a -b -c arg",
		"a\nb\nc arg\n",
	},
	// mapfile
	{
		"mapfile <<EOF\na\nb\nc\nEOF\n" + `for x in "${MAPFILE[@]}"; do echo "$x"; done`,
		"a\n\nb\n\nc\n\n",
	},
	{
		"mapfile -t <<EOF\na\nb\nc\nEOF\n" + `for x in "${MAPFILE[@]}"; do echo "$x"; done`,
		"a\nb\nc\n",
	},
	{
		"mapfile -t -d b <<EOF\nabc\nEOF\n" + `for x in "${MAPFILE[@]}"; do echo "$x"; done`,
		"a\nc\n\n",
	},
	{
		"mapfile -t butter <<EOF\na\nb\nc\nEOF\n" + `for x in "${butter[@]}"; do echo "$x"; done`,
		"a\nb\nc\n",
	},
}

var runTestsUnix = []runTest{
	{"[[ -n $PPID && $PPID -ge 0 ]]", ""}, // can be 0 if running as the init process
	{
		// no root user on windows
		"[[ ~root == '~root' ]]",
		"exit status 1",
	},

	// windows does not support paths with '*'
	{
		"mkdir -p '*/a.z' 'b/a.z'; cd '*'; set -- *.z; echo $#",
		"1\n",
	},
	{
		"mkdir -p 'a-*/d'; test -d $PWD/a-*/*",
		"",
	},

	// no fifos on windows
	{
		"[ -p a ] && echo x; mkfifo a; [ -p a ] && echo y",
		"y\n",
	},
	{
		"[[ -p a ]] && echo x; mkfifo a; [[ -p a ]] && echo y",
		"y\n",
	},

	{"sh() { :; }; sh -c 'echo foo_interp_missing'", ""},
	{"sh() { :; }; command sh -c 'echo foo_interp_missing'", "foo_interp_missing\n"},

	// chmod is practically useless on Windows
	{
		"[ -x a ] && echo x; >a; chmod 0755 a; [ -x a ] && echo y",
		"y\n",
	},
	{
		"[[ -x a ]] && echo x; >a; chmod 0755 a; [[ -x a ]] && echo y",
		"y\n",
	},
	{
		">a; [ -k a ] && echo x; chmod +t a; [ -k a ] && echo y",
		"y\n",
	},
	{
		">a; [ -u a ] && echo x; chmod u+s a; [ -u a ] && echo y",
		"y\n",
	},
	{
		">a; [ -g a ] && echo x; chmod g+s a; [ -g a ] && echo y",
		"y\n",
	},
	{
		">a; [[ -k a ]] && echo x; chmod +t a; [[ -k a ]] && echo y",
		"y\n",
	},
	{
		">a; [[ -u a ]] && echo x; chmod u+s a; [[ -u a ]] && echo y",
		"y\n",
	},
	{
		">a; [[ -g a ]] && echo x; chmod g+s a; [[ -g a ]] && echo y",
		"y\n",
	},
	{
		`mkdir a; chmod 0100 a; cd a`,
		"",
	},
	// Note that these will succeed if we're root.
	{
		`mkdir a; chmod 0000 a; cd a && test $UID -ne 0`,
		"exit status 1 #JUSTERR",
	},
	{
		`mkdir a; chmod 0222 a; cd a && test $UID -ne 0`,
		"exit status 1 #JUSTERR",
	},
	{
		`mkdir a; chmod 0444 a; cd a && test $UID -ne 0`,
		"exit status 1 #JUSTERR",
	},
	{
		`mkdir a; chmod 0010 a; cd a && test $UID -ne 0`,
		"exit status 1 #JUSTERR",
	},
	{
		`mkdir a; chmod 0001 a; cd a && test $UID -ne 0`,
		"exit status 1 #JUSTERR",
	},
	{
		`unset UID`,
		"UID: readonly variable\n #IGNORE",
	},
	{
		`test -n "$EUID" && echo OK`,
		"OK\n",
	},
	{
		`set EUID=newvalue; test EUID != newvalue && echo OK || echo EUID=$EUID`,
		"OK\n",
	},
	{
		`unset EUID`,
		"EUID: readonly variable\n #IGNORE",
	},
	// GID is not set in bash
	{
		`unset GID`,
		"GID: readonly variable\n #IGNORE",
	},
	{
		`[[ -z $GID ]] && echo "GID not set"`,
		"exit status 1 #JUSTERR #IGNORE",
	},

	// Unix-y PATH
	{
		"PATH=; bash -c 'echo foo_interp_missing'",
		"\"bash\": executable file not found in $PATH\nexit status 127 #JUSTERR",
	},
	{
		"cd /; sure/is/missing",
		"stat /sure/is/missing: no such file or directory\nexit status 127 #JUSTERR",
	},
	{
		"echo '#!/bin/sh\necho b' >a; chmod 0755 a; PATH=; a",
		"b\n",
	},
	{
		"mkdir c; cd c; echo '#!/bin/sh\necho b' >a; chmod 0755 a; PATH=; a",
		"b\n",
	},
	{
		"mkdir c; echo '#!/bin/sh\necho b' >c/a; chmod 0755 c/a; c/a",
		"b\n",
	},
	{
		"GOSH_CMD=lookpath $GOSH_PROG",
		"sh found\n",
	},

	// error strings which are too different on Windows
	{
		"echo foo_interp_missing >/shouldnotexist/file",
		"open /shouldnotexist/file: no such file or directory\nexit status 1 #JUSTERR",
	},
	{
		"set -e; echo foo_interp_missing >/shouldnotexist/file; echo foo_interp_missing",
		"open /shouldnotexist/file: no such file or directory\nexit status 1 #JUSTERR",
	},

	// process substitution; named pipes (fifos) are a TODO for windows
	{
		"sed 's/o/e/g' <(echo foo_interp_missing bar_interp_missing)",
		"fee_interp_missing bar_interp_missing\n",
	},
	{
		"cat <(echo foo_interp_missing) <(echo bar_interp_missing) <(echo baz)",
		"foo_interp_missing\nbar_interp_missing\nbaz\n",
	},
	{
		"cat <(cat <(echo nested))",
		"nested\n",
	},
	{
		// The tests here use "wait" because otherwise the parent may finish before
		// the subprocess has had time to process the input and print the result.
		"echo foo_interp_missing bar_interp_missing > >(sed 's/o/e/g'); wait",
		"fee_interp_missing bar_interp_missing\n",
	},
	{
		"echo foo_interp_missing bar_interp_missing | tee >(sed 's/o/e/g') >/dev/null; wait",
		"fee_interp_missing bar_interp_missing\n",
	},
	{
		"echo nested > >(cat > >(cat); wait); wait",
		"nested\n",
	},
	{
		"cat <(exit 0); wait $!; echo $?",
		"0\n",
	},
	{
		"cat <(exit 5); wait $!; echo $?",
		"5\n",
	},
	{
		// The reader here does not consume the named pipe.
		"test -e <(echo foo)",
		"",
	},
	// echo trace
	{
		`set -x; animals=("dog", "cat", "otter"); echo "hello ${animals[*]}"`,
		`+ animals=("dog", "cat", "otter")
+ echo 'hello dog, cat, otter'
hello dog, cat, otter
`,
	},
	{
		`set -x; s="always print a decimal point for %e, %E, %f, %F, %g and %G; do not remove trailing zeros for %g and %G"; echo "$s"`,
		`+ s='always print a decimal point for %e, %E, %f, %F, %g and %G; do not remove trailing zeros for %g and %G'
+ echo 'always print a decimal point for %e, %E, %f, %F, %g and %G; do not remove trailing zeros for %g and %G'
always print a decimal point for %e, %E, %f, %F, %g and %G; do not remove trailing zeros for %g and %G
`,
	},
	{
		`set -x
x=without; echo "$x"
x="double quote"; echo "$x"
x='single quote'; echo "$x"`,
		`+ x=without
+ echo without
without
+ x='double quote'
+ echo 'double quote'
double quote
+ x='single quote'
+ echo 'single quote'
single quote
`,
	},
	// for trace
	{
		`set -x
exec >/dev/null
echo "trace should go to stderr"`,
		`+ exec
+ echo 'trace should go to stderr'
`,
	},
	{
		`set -x
animals=(dog, cat, otter)
for i in ${animals[@]}
do
   echo "hello ${i}"
done
`,
		`+ animals=(dog, cat, otter)
+ for i in ${animals[@]}
+ echo 'hello dog,'
hello dog,
+ for i in ${animals[@]}
+ echo 'hello cat,'
hello cat,
+ for i in ${animals[@]}
+ echo 'hello otter'
hello otter
`,
	},
	{
		`set -x
loop() {
    for i do
        echo "something with $i"
    done
}
loop 1 2 3`,
		`+ loop 1 2 3
+ for i in "$@"
+ echo 'something with 1'
something with 1
+ for i in "$@"
+ echo 'something with 2'
something with 2
+ for i in "$@"
+ echo 'something with 3'
something with 3
`,
	},
	{
		`set -x; animals=(dog, cat, otter); for i in ${animals[@]}; do echo "hello ${i}"; done`,
		`+ animals=(dog, cat, otter)
+ for i in ${animals[@]}
+ echo 'hello dog,'
hello dog,
+ for i in ${animals[@]}
+ echo 'hello cat,'
hello cat,
+ for i in ${animals[@]}
+ echo 'hello otter'
hello otter
`,
	},
	{
		`set -x; a=x"y"$z b=(foo_interp_missing bar_interp_missing $none '')`,
		"+ a=xy\n+ b=(foo_interp_missing bar_interp_missing $none '')\n",
	},
	{
		`set -x; for i in a b; do echo $i; done`,
		`+ for i in a b
+ echo a
a
+ for i in a b
+ echo b
b
`,
	},
	{
		`set -x; for i in $none_a $none_b; do echo $i; done`,
		``,
	},
	// case trace
	{
		`set -x; pet=dog; case $pet in 'dog') echo "barks";; *) echo "unknown";; esac`,
		`+ pet=dog
+ case $pet in
+ echo barks
barks
`,
	},
	{
		`set -x
pet="dog"
case $pet in
  dog)
    echo "barks"
    ;;
  *)
    echo "unknown"
    ;;
esac`,
		`+ pet=dog
+ case $pet in
+ echo barks
barks
`,
	},
	// arithmetic
	{
		`set -x
a=$(( 4 + 5 )); echo $a
a=$((3+5)); echo $a`,
		`+ a=9
+ echo 9
9
+ a=8
+ echo 8
8
`,
	},
	{
		`set -x;
let a=5+4; echo $a
let "a = 5 + 4"; echo $a
let a++; echo $a`,
		`+ let a=5+4
+ echo 9
9
+ let 'a = 5 + 4'
+ echo 9
9
+ let a++
+ echo 10
10
`,
	},
	// functions
	{
		`set -x; function with_function () { echo 'hello, world'; }; with_function`,
		`+ with_function
+ echo 'hello, world'
hello, world
`,
	},
	{
		`set -x; without_function () { echo 'hello, world'; }; without_function`,
		`+ without_function
+ echo 'hello, world'
hello, world
`,
	},
	{
		// globbing wildcard as function name
		`@() { echo "$@"; }; @ lala; function +() { echo "$@"; }; + foo_interp_missing`,
		"lala\nfoo_interp_missing\n",
	},
	{
		`      @() { echo "$@"; }; @ lala;`,
		"lala\n",
	},
	{
		// globbing wildcard as function name but with space after the name
		`+ () { echo "$@"; }; + foo_interp_missing; @ () { echo "$@"; }; @ lala; ? () { echo "$@"; }; ? bar_interp_missing`,
		"foo_interp_missing\nlala\nbar_interp_missing\n",
	},
	// mapfile, no process substitution yet on Windows
	{
		`mapfile -t -d "" < <(printf "a\0b\n"); for x in "${MAPFILE[@]}"; do echo "$x"; done`,
		"a\nb\n\n",
	},
	// Windows does not support having a `\n` in a filename
	{
		`> $'bar\nbaz'; echo bar*baz`,
		"bar\nbaz\n",
	},
}

var runTestsWindows = []runTest{
	{"[[ -n $PPID || $PPID -gt 0 ]]", ""}, // os.Getppid can be 0 on windows
	{"cmd() { :; }; cmd /c 'echo foo_interp_missing'", ""},
	{"cmd() { :; }; command cmd /c 'echo foo_interp_missing'", "foo_interp_missing\r\n"},
	{
		"GOSH_CMD=lookpath $GOSH_PROG",
		"cmd found\n",
	},
}

// These tests are specific to 64-bit architectures, and that's fine. We don't
// need to add explicit versions for 32-bit.
var runTests64bit = []runTest{
	{"printf %i,%u -3 -3", "-3,18446744073709551613"},
	{"printf %o -3", "1777777777777777777775"},
	{"printf %x -3", "fffffffffffffffd"},
}

func init() {
	if runtime.GOOS == "windows" {
		runTests = append(runTests, runTestsWindows...)
	} else { // Unix-y
		runTests = append(runTests, runTestsUnix...)
	}
	if bits.UintSize == 64 {
		runTests = append(runTests, runTests64bit...)
	}
}

// ln -s: wine doesn't implement symlinks; see https://bugs.winehq.org/show_bug.cgi?id=44948
// process substitutions are not supported on Windows
var skipOnWindows = regexp.MustCompile(`ln -s|<\(`)

// process substitutions seemflaky on mac; see https://github.com/mvdan/sh/issues/576
var skipOnMac = regexp.MustCompile(`>\(|<\(`)

func skipIfUnsupported(tb testing.TB, src string) {
	switch {
	case runtime.GOOS == "windows" && skipOnWindows.MatchString(src):
		tb.Skipf("skipping non-portable test on windows")
	case runtime.GOOS == "darwin" && skipOnMac.MatchString(src):
		tb.Skipf("skipping non-portable test on mac")
	}
}

func TestRunnerRun(t *testing.T) {
	t.Parallel()

	p := syntax.NewParser()
	for _, c := range runTests {
		t.Run("", func(t *testing.T) {
			skipIfUnsupported(t, c.in)

			// Parse first, as we reuse a single parser.
			file := parse(t, p, c.in)

			t.Parallel()

			tdir := t.TempDir()
			var cb concBuffer
			r, err := interp.New(interp.Dir(tdir), interp.StdIO(nil, &cb, &cb),
				// TODO: why does this make some tests hang?
				// interp.Env(expand.ListEnviron(append(os.Environ(),
				// 	"FOO_INTERP_MISSING_NULL_BAR_INTERP_MISSING=foo_interp_missing\x00bar_interp_missing")...)),
				interp.ExecHandlers(testExecHandler),
			)
			if err != nil {
				t.Fatal(err)
			}
			ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
			defer cancel()
			if err := r.Run(ctx, file); err != nil {
				cb.WriteString(err.Error())
			}
			want := c.want
			if i := strings.Index(want, " #"); i >= 0 {
				want = want[:i]
			}
			if got := cb.String(); got != want {
				if len(got) > 200 {
					got = "…" + got[len(got)-200:]
				}
				t.Fatalf("wrong output in %q:\nwant: %q\ngot:  %q",
					c.in, want, got)
			}
		})
	}
}

func readLines(hc interp.HandlerContext) ([][]byte, error) {
	bs, err := io.ReadAll(hc.Stdin)
	if err != nil {
		return nil, err
	}
	if runtime.GOOS == "windows" {
		bs = bytes.ReplaceAll(bs, []byte("\r\n"), []byte("\n"))
	}
	bs = bytes.TrimSuffix(bs, []byte("\n"))
	return bytes.Split(bs, []byte("\n")), nil
}

func absPath(dir, path string) string {
	if path == "" {
		return ""
	}
	if !filepath.IsAbs(path) {
		path = filepath.Join(dir, path)
	}
	return filepath.Clean(path) // TODO: this clean is likely unnecessary
}

var testBuiltinsMap = map[string]func(interp.HandlerContext, []string) error{
	"cat": func(hc interp.HandlerContext, args []string) error {
		if len(args) == 0 {
			if hc.Stdin == nil || hc.Stdout == nil {
				return nil
			}
			_, err := io.Copy(hc.Stdout, hc.Stdin)
			return err
		}
		for _, arg := range args {
			path := absPath(hc.Dir, arg)
			f, err := os.Open(path)
			if err != nil {
				return err
			}
			_, err = io.Copy(hc.Stdout, f)
			f.Close()
			if err != nil {
				return err
			}
		}
		return nil
	},
	"wc": func(hc interp.HandlerContext, args []string) error {
		bs, err := io.ReadAll(hc.Stdin)
		if err != nil {
			return err
		}
		if len(args) == 0 {
			fmt.Fprintf(hc.Stdout, "%7d", bytes.Count(bs, []byte("\n")))
			fmt.Fprintf(hc.Stdout, "%8d", len(bytes.Fields(bs)))
			fmt.Fprintf(hc.Stdout, "%8d\n", len(bs))
		} else if args[0] == "-c" {
			fmt.Fprintln(hc.Stdout, len(bs))
		} else if args[0] == "-l" {
			fmt.Fprintln(hc.Stdout, bytes.Count(bs, []byte("\n")))
		}
		return nil
	},
	"tr": func(hc interp.HandlerContext, args []string) error {
		if len(args) != 2 || len(args[1]) != 1 {
			return fmt.Errorf("usage: tr [-s -d] [character]")
		}
		squeeze := args[0] == "-s"
		char := args[1][0]
		bs, err := io.ReadAll(hc.Stdin)
		if err != nil {
			return err
		}
		for {
			i := bytes.IndexByte(bs, char)
			if i < 0 {
				hc.Stdout.Write(bs) // remaining
				break
			}
			hc.Stdout.Write(bs[:i]) // up to char
			bs = bs[i+1:]

			bs = bytes.TrimLeft(bs, string(char)) // remove repeats
			if squeeze {
				hc.Stdout.Write([]byte{char})
			}
		}
		return nil
	},
	"sort": func(hc interp.HandlerContext, args []string) error {
		lines, err := readLines(hc)
		if err != nil {
			return err
		}
		slices.SortFunc(lines, bytes.Compare)
		for _, line := range lines {
			fmt.Fprintf(hc.Stdout, "%s\n", line)
		}
		return nil
	},
	"grep": func(hc interp.HandlerContext, args []string) error {
		var rx *regexp.Regexp
		quiet := false
		for _, arg := range args {
			if arg == "-q" {
				quiet = true
			} else if arg == "-E" {
			} else if rx == nil {
				rx = regexp.MustCompile(arg)
			} else {
				return fmt.Errorf("unexpected arg: %q", arg)
			}
		}
		lines, err := readLines(hc)
		if err != nil {
			return err
		}
		anyMatch := false
		for _, line := range lines {
			if rx.Match(line) {
				if quiet {
					return nil
				}
				anyMatch = true
				fmt.Fprintf(hc.Stdout, "%s\n", line)
			}
		}
		if !anyMatch {
			return interp.ExitStatus(1)
		}
		return nil
	},
	"sed": func(hc interp.HandlerContext, args []string) error {
		f := hc.Stdin
		switch len(args) {
		case 1:
		case 2:
			var err error
			f, err = os.Open(absPath(hc.Dir, args[1]))
			if err != nil {
				return err
			}
		default:
			return fmt.Errorf("usage: sed pattern [file]")
		}
		expr := args[0]
		if expr == "" || expr[0] != 's' {
			return fmt.Errorf("unimplemented")
		}
		sep := expr[1]
		expr = expr[2:]
		from := expr[:strings.IndexByte(expr, sep)]
		expr = expr[len(from)+1:]
		to := expr[:strings.IndexByte(expr, sep)]
		bs, err := io.ReadAll(f)
		if err != nil {
			return err
		}
		rx := regexp.MustCompile(from)
		bs = rx.ReplaceAllLiteral(bs, []byte(to))
		_, err = hc.Stdout.Write(bs)
		return err
	},
	"mkdir": func(hc interp.HandlerContext, args []string) error {
		for _, arg := range args {
			if arg == "-p" {
				continue
			}
			path := absPath(hc.Dir, arg)
			if err := os.MkdirAll(path, 0o777); err != nil {
				return err
			}
		}
		return nil
	},
	"rm": func(hc interp.HandlerContext, args []string) error {
		for _, arg := range args {
			if arg == "-r" {
				continue
			}
			path := absPath(hc.Dir, arg)
			if err := os.RemoveAll(path); err != nil {
				return err
			}
		}
		return nil
	},
	"ln": func(hc interp.HandlerContext, args []string) error {
		symbolic := args[0] == "-s"
		if symbolic {
			args = args[1:]
		}
		oldname := absPath(hc.Dir, args[0])
		newname := absPath(hc.Dir, args[1])
		if symbolic {
			return os.Symlink(oldname, newname)
		}
		return os.Link(oldname, newname)
	},
	"touch": func(hc interp.HandlerContext, args []string) error {
		filenames := args // create all arguments as filenames

		newTime := time.Now()
		if args[0] == "-t" {
			if len(args) < 3 {
				return fmt.Errorf("usage: touch [-t [[CC]YY]MMDDhhmm[.SS]] file")
			}
			filenames = args[2:] // treat the rest of the args as filenames

			arg := args[1]
			if len(arg) > 15 {
				return fmt.Errorf("usage: touch [-t [[CC]YY]MMDDhhmm[.SS]] file")
			}
			s, err := time.Parse("200601021504.05", arg)
			if err != nil {
				return err
			}
			newTime = s
		}

		for _, arg := range filenames {
			if strings.HasPrefix(arg, "-") {
				return fmt.Errorf("usage: touch [-t [[CC]YY]MMDDhhmm[.SS]] file")
			}
			path := absPath(hc.Dir, arg)
			// create the file if it does not exist
			f, err := os.OpenFile(path, os.O_CREATE, 0o666)
			if err != nil {
				return err
			}
			f.Close()
			// change the modification and access time
			if err := os.Chtimes(path, newTime, newTime); err != nil {
				return err
			}
		}
		return nil
	},
	"sleep": func(hc interp.HandlerContext, args []string) error {
		for _, arg := range args {
			// assume and default unit to be in seconds
			d, err := time.ParseDuration(fmt.Sprintf("%ss", arg))
			if err != nil {
				return err
			}
			time.Sleep(d)
		}
		return nil
	},
}

func testExecHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
	return func(ctx context.Context, args []string) error {
		if fn := testBuiltinsMap[args[0]]; fn != nil {
			return fn(interp.HandlerCtx(ctx), args[1:])
		}
		return next(ctx, args)
	}
}

func TestRunnerRunConfirm(t *testing.T) {
	if testing.Short() {
		t.Skip("calling bash is slow")
	}
	if !hasBash52 {
		t.Skip("bash 5.2 required to run")
	}
	t.Parallel()

	if runtime.GOOS == "windows" {
		// For example, it seems to treat environment variables as
		// case-sensitive, which isn't how Windows works.
		t.Skip("bash on Windows emulates Unix-y behavior")
	}
	for _, c := range runTests {
		t.Run("", func(t *testing.T) {
			if strings.Contains(c.want, " #IGNORE") {
				return
			}
			skipIfUnsupported(t, c.in)
			t.Parallel()
			tdir := t.TempDir()
			ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
			defer cancel()
			cmd := exec.CommandContext(ctx, "bash")
			cmd.Dir = tdir
			cmd.Stdin = strings.NewReader(c.in)
			out, err := cmd.CombinedOutput()
			if strings.Contains(c.want, " #JUSTERR") {
				// bash sometimes exits with status code 0 and
				// stderr "bash: ..." for an error
				fauxErr := bytes.HasPrefix(out, []byte("bash:"))
				if err == nil && !fauxErr {
					t.Fatalf("wanted bash to error in %q", c.in)
				}
				return
			}
			got := string(out)
			if err != nil {
				got += err.Error()
			}
			if got != c.want {
				t.Fatalf("wrong bash output in %q:\nwant: %q\ngot:  %q",
					c.in, c.want, got)
			}
		})
	}
}

func TestRunnerOpts(t *testing.T) {
	t.Parallel()

	withPath := func(strs ...string) func(*interp.Runner) error {
		prefix := []string{
			"PATH=" + os.Getenv("PATH"),
			"ENV_PROG=" + os.Getenv("ENV_PROG"),
		}
		return interp.Env(expand.ListEnviron(append(prefix, strs...)...))
	}
	opts := func(list ...interp.RunnerOption) []interp.RunnerOption {
		return list
	}
	cases := []struct {
		opts     []interp.RunnerOption
		in, want string
	}{
		{
			nil,
			"$ENV_PROG | grep '^INTERP_GLOBAL='",
			"INTERP_GLOBAL=value\n",
		},
		{
			opts(withPath()),
			"$ENV_PROG | grep '^INTERP_GLOBAL='",
			"exit status 1",
		},
		{
			opts(withPath("INTERP_GLOBAL=bar_interp_missing")),
			"$ENV_PROG | grep '^INTERP_GLOBAL='",
			"INTERP_GLOBAL=bar_interp_missing\n",
		},
		{
			opts(withPath("a=b")),
			"echo $a",
			"b\n",
		},
		{
			opts(withPath("A=b")),
			"$ENV_PROG | grep '^A='; echo $A",
			"A=b\nb\n",
		},
		{
			opts(withPath("A=b", "A=c")),
			"$ENV_PROG | grep '^A='; echo $A",
			"A=c\nc\n",
		},
		{
			opts(withPath("HOME=")),
			"echo $HOME",
			"\n",
		},
		{
			opts(withPath("PWD=foo_interp_missing")),
			"[[ $PWD == foo_interp_missing ]]",
			"exit status 1",
		},
		{
			opts(interp.Params("foo_interp_missing")),
			"echo $@",
			"foo_interp_missing\n",
		},
		{
			opts(interp.Params("-u", "--", "foo_interp_missing")),
			"echo $@; echo $unset",
			"foo_interp_missing\nunset: unbound variable\nexit status 1",
		},
		{
			opts(interp.Params("-u", "--", "foo_interp_missing")),
			"echo $@; echo ${unset:-default}",
			"foo_interp_missing\ndefault\n",
		},
		{
			opts(interp.Params("foo_interp_missing")),
			"set >/dev/null; echo $@",
			"foo_interp_missing\n",
		},
		{
			opts(interp.Params("foo_interp_missing")),
			"set -e; echo $@",
			"foo_interp_missing\n",
		},
		{
			opts(interp.Params("foo_interp_missing")),
			"set --; echo $@",
			"\n",
		},
		{
			opts(interp.Params("foo_interp_missing")),
			"set bar_interp_missing; echo $@",
			"bar_interp_missing\n",
		},
		{
			opts(interp.Env(expand.FuncEnviron(func(name string) string {
				if name == "foo" {
					return "bar"
				}
				return ""
			}))),
			"(echo $foo); echo x | echo $foo",
			"bar\nbar\n",
		},
	}
	p := syntax.NewParser()
	for _, c := range cases {
		t.Run("", func(t *testing.T) {
			skipIfUnsupported(t, c.in)
			file := parse(t, p, c.in)
			var cb concBuffer
			r, err := interp.New(append(c.opts,
				interp.StdIO(nil, &cb, &cb),
				interp.ExecHandlers(testExecHandler),
			)...)
			if err != nil {
				t.Fatal(err)
			}
			ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
			defer cancel()
			if err := r.Run(ctx, file); err != nil {
				cb.WriteString(err.Error())
			}
			if got := cb.String(); got != c.want {
				t.Fatalf("wrong output in %q:\nwant: %q\ngot:  %q",
					c.in, c.want, got)
			}
		})
	}
}

func TestRunnerContext(t *testing.T) {
	t.Parallel()

	cases := []string{
		"",
		"while true; do true; done",
		"until false; do true; done",
		"sleep 1000",
		"while true; do true; done & wait",
		"sleep 1000 & wait",
		"(while true; do true; done)",
		"$(while true; do true; done)",
		"while true; do true; done | while true; do true; done",
	}
	p := syntax.NewParser()
	for _, in := range cases {
		t.Run("", func(t *testing.T) {
			file := parse(t, p, in)
			ctx, cancel := context.WithCancel(context.Background())
			cancel()
			r, _ := interp.New()
			errChan := make(chan error)
			go func() {
				errChan <- r.Run(ctx, file)
			}()

			timeout := 500 * time.Millisecond
			select {
			case err := <-errChan:
				if err != nil && err != ctx.Err() {
					t.Fatal("Runner did not use ctx.Err()")
				}
			case <-time.After(timeout):
				t.Fatalf("program was not killed in %s", timeout)
			}
		})
	}
}

func TestCancelBlockedStdinRead(t *testing.T) {
	if runtime.GOOS == "windows" {
		// TODO: Why is this? The [os.File.SetReadDeadline] docs seem to imply that it should work
		// across all major platforms, and the file polling  implementation seems to be
		// for all posix platforms including Windows.
		// Our previous logic and tests with muesli/cancelreader did not test an os.Pipe
		// on Windows either, so skipping here is not any worse.
		t.Skip("os.Pipe on windows appears to not support cancellable reads")
	}
	t.Parallel()

	p := syntax.NewParser()
	file := parse(t, p, "read x")
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	// Make the linter happy, even though we deliberately wait for the timeout.
	defer cancel()

	stdinRead, stdinWrite, err := os.Pipe()
	if err != nil {
		t.Fatalf("Error calling os.Pipe: %v", err)
	}
	defer func() {
		stdinWrite.Close()
		stdinRead.Close()
	}()
	r, _ := interp.New(interp.StdIO(stdinRead, nil, nil))
	now := time.Now()
	errChan := make(chan error)
	go func() {
		errChan <- r.Run(ctx, file)
	}()

	timeout := 500 * time.Millisecond
	select {
	case err := <-errChan:
		if err == nil || err.Error() != "exit status 1" || ctx.Err() != context.DeadlineExceeded {
			t.Fatalf("'read x' did not timeout correctly; err: %v, ctx.Err(): %v; dur: %v",
				err, ctx.Err(), time.Since(now))
		}
	case <-time.After(timeout):
		t.Fatalf("program was not killed in %s", timeout)
	}
}

func TestRunnerAltNodes(t *testing.T) {
	t.Parallel()

	in := "echo foo_interp_missing"
	file := parse(t, nil, in)
	want := "foo_interp_missing\n"
	nodes := []syntax.Node{
		file,
		file.Stmts[0],
		file.Stmts[0].Cmd,
	}
	for _, node := range nodes {
		var cb concBuffer
		r, _ := interp.New(interp.StdIO(nil, &cb, &cb))
		ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
		defer cancel()
		if err := r.Run(ctx, node); err != nil {
			cb.WriteString(err.Error())
		}
		if got := cb.String(); got != want {
			t.Fatalf("wrong output in %q:\nwant: %q\ngot:  %q",
				in, want, got)
		}
	}
}

func TestRunnerDir(t *testing.T) {
	t.Parallel()

	wd, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	t.Run("Missing", func(t *testing.T) {
		_, err := interp.New(interp.Dir("missing"))
		if err == nil {
			t.Fatal("expected New to error when Dir is missing")
		}
	})
	t.Run("NotDir", func(t *testing.T) {
		_, err := interp.New(interp.Dir("interp_test.go"))
		if err == nil {
			t.Fatal("expected New to error when Dir is not a dir")
		}
	})
	t.Run("NotDirAbs", func(t *testing.T) {
		_, err := interp.New(interp.Dir(filepath.Join(wd, "interp_test.go")))
		if err == nil {
			t.Fatal("expected New to error when Dir is not a dir")
		}
	})
	t.Run("Relative", func(t *testing.T) {
		// On Windows, it's impossible to make a relative path from one
		// drive to another. Use the parent directory, as that's for
		// sure in the same drive as the current directory.
		rel := ".." + string(filepath.Separator)
		r, err := interp.New(interp.Dir(rel))
		if err != nil {
			t.Fatal(err)
		}
		if !filepath.IsAbs(r.Dir) {
			t.Errorf("Runner.Dir is not absolute")
		}
	})
	// Ensure that we treat symlinks and short paths properly, especially
	// with Dir and globbing.
	t.Run("SymlinkOrShortPath", func(t *testing.T) {
		tdir := t.TempDir()

		realDir := filepath.Join(tdir, "real-long-dir-name")
		realFile := filepath.Join(realDir, "realfile")

		if err := os.Mkdir(realDir, 0o777); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(realFile, []byte(""), 0o666); err != nil {
			t.Fatal(err)
		}

		var altDir string
		if runtime.GOOS == "windows" {
			short, err := shortPathName(realDir)
			if err != nil {
				t.Fatal(err)
			}
			altDir = short
			// We replace tdir later, and it might have been shortened.
			tdir = filepath.Dir(altDir)
		} else {
			altDir = filepath.Join(tdir, "symlink")
			if err := os.Symlink(realDir, altDir); err != nil {
				t.Fatal(err)
			}
		}

		var b bytes.Buffer
		r, err := interp.New(interp.Dir(altDir), interp.StdIO(nil, &b, &b))
		if err != nil {
			t.Fatal(err)
		}
		file := parse(t, nil, "echo $PWD $PWD/*")
		ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
		defer cancel()
		if err := r.Run(ctx, file); err != nil {
			t.Fatal(err)
		}
		got := b.String()
		got = strings.ReplaceAll(got, tdir, "")
		got = strings.TrimSpace(got)
		want := `/symlink /symlink/realfile`
		if runtime.GOOS == "windows" {
			want = `\\REAL.{4} \\REAL.{4}\\realfile`
		}
		if !regexp.MustCompile(want).MatchString(got) {
			t.Fatalf("\nwant regexp: %q\ngot: %q", want, got)
		}
	})
}

func TestRunnerIncremental(t *testing.T) {
	t.Parallel()

	file := parse(t, nil, "echo foo_interp_missing; false; echo bar_interp_missing; exit 0; echo baz")
	want := "foo_interp_missing\nbar_interp_missing\n"
	var b bytes.Buffer
	r, _ := interp.New(interp.StdIO(nil, &b, &b))
	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	for _, stmt := range file.Stmts {
		err := r.Run(ctx, stmt)
		if !errors.As(err, new(interp.ExitStatus)) && err != nil {
			// Keep track of unexpected errors.
			b.WriteString(err.Error())
		}
		if r.Exited() {
			break
		}
	}
	if got := b.String(); got != want {
		t.Fatalf("\nwant: %q\ngot:  %q", want, got)
	}
}

func TestRunnerResetFields(t *testing.T) {
	t.Parallel()

	tdir := t.TempDir()
	logPath := filepath.Join(tdir, "log")
	logFile, err := os.Create(logPath)
	if err != nil {
		t.Fatal(err)
	}
	defer logFile.Close()
	r, _ := interp.New(
		interp.Params("-f", "--", "first", tdir, logPath),
		interp.Dir(tdir),
		interp.ExecHandlers(testExecHandler),
	)
	// Check that using option funcs and Runner fields directly is still
	// kept by Reset.
	interp.StdIO(nil, logFile, os.Stderr)(r)
	r.Env = expand.ListEnviron(append(os.Environ(), "GLOBAL=foo_interp_missing")...)

	file := parse(t, nil, `
# Params set 3 arguments
[[ $# -eq 3 ]] || exit 10
[[ $1 == "first" ]] || exit 11

# Params set the -f option (noglob)
[[ -o noglob ]] || exit 12

# $PWD was set via Dir, and should be equal to $2
[[ "$PWD" == "$2" ]] || exit 13

# stdout should go into the log file, which is at $3
echo line1
echo line2
[[ "$(wc -l <$3)" == "2" ]] || exit 14

# $GLOBAL was set directly via the Env field
[[ "$GLOBAL" == "foo_interp_missing" ]] || exit 15

# Change all of the above within the script. Reset should undo this.
set +f -- newargs
cd
exec >/dev/null 2>/dev/null
GLOBAL=
export GLOBAL=
`)
	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	for i := range 3 {
		if err := r.Run(ctx, file); err != nil {
			t.Fatalf("run number %d: %v", i, err)
		}
		r.Reset()
		// empty the log file too
		logFile.Truncate(0)
		logFile.Seek(0, io.SeekStart)
	}
}

func TestRunnerManyResets(t *testing.T) {
	t.Parallel()
	r, _ := interp.New()
	for range 5 {
		r.Reset()
	}
}

func TestRunnerFilename(t *testing.T) {
	t.Parallel()

	want := "f.sh\n"
	file, _ := syntax.NewParser().Parse(strings.NewReader("echo $0"), "f.sh")
	var b bytes.Buffer
	r, _ := interp.New(interp.StdIO(nil, &b, &b))
	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	if err := r.Run(ctx, file); err != nil {
		t.Fatal(err)
	}
	if got := b.String(); got != want {
		t.Fatalf("\nwant: %q\ngot:  %q", want, got)
	}
}

func TestRunnerEnvNoModify(t *testing.T) {
	t.Parallel()

	env := expand.ListEnviron("one=1", "two=2")
	file := parse(t, nil, `echo -n "$one $two; "; one=x; unset two`)

	var b bytes.Buffer
	r, _ := interp.New(interp.Env(env), interp.StdIO(nil, &b, &b))
	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	for range 3 {
		r.Reset()
		err := r.Run(ctx, file)
		if err != nil {
			t.Fatal(err)
		}
	}

	want := "1 2; 1 2; 1 2; "
	if got := b.String(); got != want {
		t.Fatalf("\nwant: %q\ngot:  %q", want, got)
	}
}

func TestMalformedPathOnWindows(t *testing.T) {
	if runtime.GOOS != "windows" {
		t.Skip("Skipping windows test on non-windows GOOS")
	}
	tdir := t.TempDir()
	t.Parallel()

	path := filepath.Join(tdir, "test.cmd")
	script := []byte("@echo foo_interp_missing")
	if err := os.WriteFile(path, script, 0o777); err != nil {
		t.Fatal(err)
	}

	// set PATH to c:\tmp\dir instead of C:\tmp\dir
	volume := filepath.VolumeName(tdir)
	pathList := strings.ToLower(volume) + tdir[len(volume):]

	file := parse(t, nil, "test.cmd")
	var cb concBuffer
	r, _ := interp.New(interp.Env(expand.ListEnviron("PATH="+pathList)), interp.StdIO(nil, &cb, &cb))
	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	if err := r.Run(ctx, file); err != nil {
		t.Fatal(err)
	}
	want := "foo_interp_missing\r\n"
	if got := cb.String(); got != want {
		t.Fatalf("wrong output:\nwant: %q\ngot:  %q", want, got)
	}
}

func TestReadShouldNotPanicWithNilStdin(t *testing.T) {
	t.Parallel()

	r, err := interp.New()
	if err != nil {
		t.Fatal(err)
	}

	f := parse(t, nil, "read foo_interp_missingbar_interp_missing")
	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	if err := r.Run(ctx, f); err == nil {
		t.Fatal("it should have returned an error")
	}
}

func TestRunnerVars(t *testing.T) {
	t.Parallel()

	r, err := interp.New()
	if err != nil {
		t.Fatal(err)
	}

	f := parse(t, nil, "FOO_INTERP_MISSING=updated; BAR_INTERP_MISSING=new")
	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	if err := r.Run(ctx, f); err != nil {
		t.Fatal(err)
	}

	if want, got := "updated", r.Vars["FOO_INTERP_MISSING"].String(); got != want {
		t.Fatalf("wrong output:\nwant: %q\ngot:  %q", want, got)
	}
}

func TestRunnerSubshell(t *testing.T) {
	t.Parallel()

	r1, err := interp.New()
	if err != nil {
		t.Fatal(err)
	}

	r2 := r1.Subshell()
	f1 := parse(t, nil, "PARENT=foo_interp_missing")
	f2 := parse(t, nil, "CHILD=bar_interp_missing")

	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	if err := r1.Run(ctx, f1); err != nil {
		t.Fatal(err)
	}
	if err := r2.Run(ctx, f2); err != nil {
		t.Fatal(err)
	}

	if want, got := "foo_interp_missing", r1.Vars["PARENT"].String(); got != want {
		t.Fatalf("wrong output:\nwant: %q\ngot:  %q", want, got)
	}
	if want, got := "bar_interp_missing", r2.Vars["CHILD"].String(); got != want {
		t.Fatalf("wrong output:\nwant: %q\ngot:  %q", want, got)
	}

	r3 := r2.Subshell()
	f3 := parse(t, nil, "CHILD=modified")
	if err := r3.Run(ctx, f3); err != nil {
		t.Fatal(err)
	}
	if want, got := "bar_interp_missing", r2.Vars["CHILD"].String(); got != want {
		t.Fatalf("wrong output:\nwant: %q\ngot:  %q", want, got)
	}
	if want, got := "modified", r3.Vars["CHILD"].String(); got != want {
		t.Fatalf("wrong output:\nwant: %q\ngot:  %q", want, got)
	}
}

func TestRunnerNonFileStdin(t *testing.T) {
	t.Parallel()

	var cb concBuffer
	r, err := interp.New(interp.StdIO(strings.NewReader("a\nb\nc\n"), &cb, &cb))
	if err != nil {
		t.Fatal(err)
	}
	file := parse(t, nil, "while read a; do echo $a; GOSH_CMD=print_ok $GOSH_PROG; done")
	ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)
	defer cancel()
	if err := r.Run(ctx, file); err != nil {
		cb.WriteString(err.Error())
	}
	// TODO: just like with heredocs, the first print_ok call consumes all stdin.
	qt.Assert(t, qt.Equals(cb.String(), "a\nexec ok\nb\nexec ok\nc\nexec ok\n"))
}
07070100000036000081A4000000000000000000000001686AE5C0000005ED000000000000000000000000000000000000001F00000000sh-3.12.0/interp/os_notunix.go// Copyright (c) 2017, Andrey Nering <andrey.nering@gmail.com>
// See LICENSE for licensing information

//go:build !unix

package interp

import (
	"context"
	"fmt"

	"mvdan.cc/sh/v3/syntax"
)

func mkfifo(path string, mode uint32) error {
	return fmt.Errorf("unsupported")
}

// access attempts to emulate [unix.Access] on Windows.
// Windows seems to have a different system of permissions than Unix,
// so for now just rely on what [io/fs.FileInfo] gives us.
func (r *Runner) access(ctx context.Context, path string, mode uint32) error {
	info, err := r.lstat(ctx, path)
	if err != nil {
		return err
	}
	m := info.Mode()
	switch mode {
	case access_R_OK:
		if m&0o400 == 0 {
			return fmt.Errorf("file is not readable")
		}
	case access_W_OK:
		if m&0o200 == 0 {
			return fmt.Errorf("file is not writable")
		}
	case access_X_OK:
		if m&0o100 == 0 {
			return fmt.Errorf("file is not executable")
		}
	}
	return nil
}

// unTestOwnOrGrp panics. Under Unix, it implements the -O and -G unary tests,
// but under Windows, it's unclear how to implement those tests, since Windows
// doesn't have the concept of a file owner, just ACLs, and it's unclear how
// to map the one to the other.
func (r *Runner) unTestOwnOrGrp(ctx context.Context, op syntax.UnTestOperator, x string) bool {
	panic(fmt.Sprintf("unhandled unary test op: %v", op))
}

// waitStatus is a no-op on plan9 and windows.
type waitStatus struct{}

func (waitStatus) Signaled() bool { return false }
func (waitStatus) Signal() int    { return 0 }
07070100000037000081A4000000000000000000000001686AE5C0000004F6000000000000000000000000000000000000001C00000000sh-3.12.0/interp/os_unix.go// Copyright (c) 2017, Andrey Nering <andrey.nering@gmail.com>
// See LICENSE for licensing information

//go:build unix

package interp

import (
	"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)
}

// 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
	}
	u, err := user.Current()
	if err != nil {
		return false
	}
	if op == syntax.TsUsrOwn {
		uid, _ := strconv.Atoi(u.Uid)
		return uint32(uid) == info.Sys().(*syscall.Stat_t).Uid
	}
	gid, _ := strconv.Atoi(u.Gid)
	return uint32(gid) == info.Sys().(*syscall.Stat_t).Gid
}

type waitStatus = syscall.WaitStatus
07070100000038000081A4000000000000000000000001686AE5C000006562000000000000000000000000000000000000001B00000000sh-3.12.0/interp/runner.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"iter"
	"math"
	mathrand "math/rand/v2"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"mvdan.cc/sh/v3/expand"
	"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{
		Env: expandEnv{r},
		CmdSubst: func(w io.Writer, cs *syntax.CmdSubst) error {
			switch len(cs.Stmts) {
			case 0: // nothing to do
				return nil
			case 1: // $(<file)
				word := catShortcutArg(cs.Stmts[0])
				if word == nil {
					break
				}
				path := r.literal(word)
				f, err := r.open(ctx, path, os.O_RDONLY, 0, true)
				if err != nil {
					return err
				}
				_, err = io.Copy(w, f)
				f.Close()
				return err
			}
			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
			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" {
				return "", fmt.Errorf("TODO: support process substitution on Windows")
			}
			if len(ps.Stmts) == 0 { // nothing to do
				return os.DevNull, nil
			}

			// We can't atomically create a random unused temporary FIFO.
			// 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 = filepath.Join(r.tempDir, fifoNamePrefix+strconv.FormatUint(mathrand.Uint64(), 16))
				err := mkfifo(path, 0o666)
				if err == nil {
					break
				}
				if !os.IsExist(err) {
					return "", fmt.Errorf("cannot create fifo: %v", err)
				}
				if try++; try > 100 {
					return "", fmt.Errorf("giving up at creating fifo: %v", err)
				}
			}

			r2 := r.subshell(true)
			stdout := r.origStdout
			// 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 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\n", err)
						return
					}
					r2.stdout = f
					defer func() {
						if err := f.Close(); err != nil {
							r.errf("closing stdout fifo: %v\n", err)
						}
						os.Remove(path)
					}()
				default: // syntax.CmdOut
					f, err := os.OpenFile(path, os.O_RDONLY, 0)
					if err != nil {
						r.errf("cannot open fifo for stdin: %v\n", err)
						return
					}
					r2.stdin = f
					r2.stdout = stdout

					defer func() {
						f.Close()
						os.Remove(path)
					}()
				}
				r2.stmts(ctx, ps.Stmts)
				r2.exit.exiting = false // subshells don't exit the parent shell
			}()
			return path, nil
		},
	}
	r.updateExpandOpts()
}

// catShortcutArg checks if a statement is of the form "$(<file)". The redirect
// word is returned if there's a match, and nil otherwise.
func catShortcutArg(stmt *syntax.Stmt) *syntax.Word {
	if stmt.Cmd != nil || stmt.Negated || stmt.Background || stmt.Coprocess {
		return nil
	}
	if len(stmt.Redirs) != 1 {
		return nil
	}
	redir := stmt.Redirs[0]
	if redir.Op != syntax.RdrIn {
		return nil
	}
	return redir.Word
}

func (r *Runner) updateExpandOpts() {
	if r.opts[optNoGlob] {
		r.ecfg.ReadDir2 = nil
	} else {
		r.ecfg.ReadDir2 = func(s string) ([]fs.DirEntry, error) {
			return r.readDirHandler(r.handlerCtx(r.ectx, handlerKindReadDir, todoPos), s)
		}
	}
	r.ecfg.GlobStar = r.opts[optGlobStar]
	r.ecfg.NoCaseGlob = r.opts[optNoCaseGlob]
	r.ecfg.NullGlob = r.opts[optNullGlob]
	r.ecfg.NoUnset = r.opts[optNoUnset]
}

func (r *Runner) expandErr(err error) {
	if err == nil {
		return
	}
	errMsg := err.Error()
	fmt.Fprintln(r.stderr, errMsg)
	switch {
	case errors.As(err, &expand.UnsetParameterError{}):
	case errMsg == "invalid indirect expansion":
		// TODO: These errors are treated as fatal by bash.
		// Make the error type reflect that.
	case strings.HasSuffix(errMsg, "not supported"):
		// TODO: This "has suffix" is a temporary measure until the expand
		// package supports all syntax nodes like extended globbing.
	default:
		return // other cases do not exit
	}
	r.exit.code = 1
	r.exit.exiting = true
}

func (r *Runner) arithm(expr syntax.ArithmExpr) int {
	n, err := expand.Arithm(r.ecfg, expr)
	r.expandErr(err)
	return n
}

func (r *Runner) fields(words ...*syntax.Word) []string {
	strs, err := expand.Fields(r.ecfg, words...)
	r.expandErr(err)
	return strs
}

func (r *Runner) literal(word *syntax.Word) string {
	str, err := expand.Literal(r.ecfg, word)
	r.expandErr(err)
	return str
}

func (r *Runner) document(word *syntax.Word) string {
	str, err := expand.Document(r.ecfg, word)
	r.expandErr(err)
	return str
}

func (r *Runner) pattern(word *syntax.Word) string {
	str, err := expand.Pattern(r.ecfg, word)
	r.expandErr(err)
	return str
}

// expandEnviron exposes [Runner]'s variables to the expand package.
type expandEnv struct {
	r *Runner
}

var _ expand.WriteEnviron = expandEnv{}

func (e expandEnv) Get(name string) expand.Variable {
	return e.r.lookupVar(name)
}

func (e expandEnv) Set(name string, vr expand.Variable) error {
	e.r.setVar(name, vr)
	return nil // TODO: return any errors
}

func (e expandEnv) Each(fn func(name string, vr expand.Variable) bool) {
	e.r.writeEnv.Each(fn)
}

var todoPos syntax.Pos // for handlerCtx callers where we don't yet have a position

func (r *Runner) handlerCtx(ctx context.Context, kind handlerKind, pos syntax.Pos) context.Context {
	hc := HandlerContext{
		runner: r,
		kind:   kind,
		Env:    &overlayEnviron{parent: r.writeEnv},
		Dir:    r.Dir,
		Pos:    pos,
		Stdout: r.stdout,
		Stderr: r.stderr,
	}
	if r.stdin != nil { // do not leave hc.Stdin as a typed nil
		hc.Stdin = r.stdin
	}
	return context.WithValue(ctx, handlerCtxKey{}, hc)
}

func (r *Runner) out(s string) {
	io.WriteString(r.stdout, s)
}

func (r *Runner) outf(format string, a ...any) {
	fmt.Fprintf(r.stdout, format, a...)
}

func (r *Runner) errf(format string, a ...any) {
	fmt.Fprintf(r.stderr, format, a...)
}

func (r *Runner) stop(ctx context.Context) bool {
	// Some traps trigger on exit, so we do want those to run.
	if !r.handlingTrap && (r.exit.returning || r.exit.exiting) {
		return true
	}
	if err := ctx.Err(); err != nil {
		r.exit.fatal(err)
		return true
	}
	if r.opts[optNoExec] {
		return true
	}
	return false
}

func (r *Runner) stmt(ctx context.Context, st *syntax.Stmt) {
	if r.stop(ctx) {
		return
	}
	r.exit = exitStatus{}
	if st.Background {
		r2 := r.subshell(true)
		st2 := *st
		st2.Background = false
		bg := bgProc{
			done: make(chan struct{}),
			exit: new(exitStatus),
		}
		r.bgProcs = append(r.bgProcs, bg)
		go func() {
			r2.Run(ctx, &st2)
			r2.exit.exiting = false // subshells don't exit the parent shell
			*bg.exit = r2.exit
			close(bg.done)
		}()
	} else {
		r.stmtSync(ctx, st)
	}
	r.lastExit = r.exit
}

func (r *Runner) stmtSync(ctx context.Context, st *syntax.Stmt) {
	oldIn, oldOut, oldErr := r.stdin, r.stdout, r.stderr
	for _, rd := range st.Redirs {
		cls, err := r.redir(ctx, rd)
		if err != nil {
			r.exit.code = 1
			break
		}
		if cls != nil {
			defer cls.Close()
		}
	}
	if r.exit.ok() && st.Cmd != nil {
		r.cmd(ctx, st.Cmd)
	}
	if st.Negated {
		// TODO: negate the entire [exitStatus] here, wiping errors
		r.exit.oneIf(r.exit.ok())
	} else if b, ok := st.Cmd.(*syntax.BinaryCmd); ok && (b.Op == syntax.AndStmt || b.Op == syntax.OrStmt) {
	} else if !r.exit.ok() && !r.noErrExit {
		r.trapCallback(ctx, r.callbackErr, "error")
		// If the "errexit" option is set and a command failed, exit the shell. Exceptions:
		//
		//   conditions (if <cond>, while <cond>, etc)
		//   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
	}
}

func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
	if r.stop(ctx) {
		return
	}

	tracingEnabled := r.opts[optXTrace]
	trace := r.tracer()

	switch cm := cm.(type) {
	case *syntax.Block:
		r.stmts(ctx, cm.Stmts)
	case *syntax.Subshell:
		r2 := r.subshell(false)
		r2.stmts(ctx, cm.Stmts)
		r2.exit.exiting = false // subshells don't exit the parent shell
		r.exit = r2.exit
	case *syntax.CallExpr:
		// Use a new slice, to not modify the slice in the alias map.
		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 = slices.Replace(args, i, i+1, als.args...)
			if !als.blank {
				break
			}
			i += len(als.args)
		}
		r.lastExpandExit = exitStatus{}
		fields := r.fields(args...)
		if len(fields) == 0 {
			for _, as := range cm.Assigns {
				prev := r.lookupVar(as.Name.Value)
				// Here we have a naked "foo=bar", so if we inherited a local var from a parent
				// function we want to signal that we are modifying the parent var rather than
				// creating a new local var via "local foo=bar".
				// TODO: there is likely a better way to do this.
				prev.Local = false

				vr := r.assignVal(prev, as, "")
				r.setVarWithIndex(prev, as.Name.Value, as.Index, vr)

				if !tracingEnabled {
					continue
				}

				// Strangely enough, it seems like Bash prints original
				// source for arrays, but the expanded value otherwise.
				// TODO: add test cases for x[i]=y and x+=y.
				if as.Array != nil {
					trace.expr(as)
				} else if as.Value != nil {
					val, err := syntax.Quote(vr.String(), syntax.LangBash)
					if err != nil { // should never happen
						panic(err)
					}
					trace.stringf("%s=%s", as.Name.Value, 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.ok() {
				r.exit = r.lastExpandExit
			}
			break
		}

		type restoreVar struct {
			name string
			vr   expand.Variable
		}
		var restores []restoreVar

		for _, as := range cm.Assigns {
			name := as.Name.Value
			prev := r.lookupVar(name)

			vr := r.assignVal(prev, as, "")
			// Inline command vars are always exported.
			vr.Exported = true

			restores = append(restores, restoreVar{name, prev})

			r.setVar(name, vr)
		}

		trace.call(fields[0], fields[1:]...)
		trace.newLineFlush()

		r.call(ctx, cm.Args[0].Pos(), fields)
		for _, restore := range restores {
			r.setVar(restore.name, restore.vr)
		}
	case *syntax.BinaryCmd:
		switch cm.Op {
		case syntax.AndStmt, syntax.OrStmt:
			oldNoErrExit := r.noErrExit
			r.noErrExit = true
			r.stmt(ctx, cm.X)
			r.noErrExit = oldNoErrExit
			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.exit.fatal(err) // not being able to create a pipe is rare but critical
				return
			}
			r2 := r.subshell(true)
			r2.stdout = pw
			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, cm.X)
				r2.exit.exiting = false // subshells don't exit the parent shell
				pw.Close()
				wg.Done()
			}()
			r.stmt(ctx, cm.Y)
			pr.Close()
			wg.Wait()
			if r.opts[optPipeFail] && !r2.exit.ok() && r.exit.ok() {
				r.exit = r2.exit
			}
			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, cm.Cond)
		r.noErrExit = oldNoErrExit

		if r.exit.ok() {
			r.stmts(ctx, cm.Then)
			break
		}
		r.exit.code = 0
		if cm.Else != nil {
			r.cmd(ctx, cm.Else)
		}
	case *syntax.WhileClause:
		for !r.stop(ctx) {
			oldNoErrExit := r.noErrExit
			r.noErrExit = true
			r.stmts(ctx, cm.Cond)
			r.noErrExit = oldNoErrExit

			stop := r.exit.ok() == cm.Until
			r.exit.code = 0
			if stop || r.loopStmtsBroken(ctx, cm.Do) {
				break
			}
		}
	case *syntax.ForClause:
		switch y := cm.Loop.(type) {
		case *syntax.WordIter:
			name := y.Name.Value
			items := r.Params // for i; do ...

			inToken := y.InPos.IsValid()
			if inToken {
				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)
				if inToken {
					for _, item := range y.Items {
						trace.string(" ")
						trace.expr(item)
					}
				} else {
					trace.string(` "$@"`)
				}
				trace.newLineFlush()
				if r.loopStmtsBroken(ctx, cm.Do) {
					break
				}
			}
		case *syntax.CStyleLoop:
			if y.Init != nil {
				r.arithm(y.Init)
			}
			for y.Cond == nil || r.arithm(y.Cond) != 0 {
				if !r.exit.ok() || r.loopStmtsBroken(ctx, cm.Do) {
					break
				}
				if y.Post != nil {
					r.arithm(y.Post)
				}
			}
		}
	case *syntax.FuncDecl:
		r.setFunc(cm.Name.Value, cm.Body)
	case *syntax.ArithmCmd:
		r.exit.oneIf(r.arithm(cm.X) == 0)
	case *syntax.LetClause:
		var val int
		for _, expr := range cm.Exprs {
			val = r.arithm(expr)

			if !tracingEnabled {
				continue
			}

			switch expr := expr.(type) {
			case *syntax.Word:
				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(cm)
			case *syntax.ParenArithm:
				// TODO
			}
		}

		trace.newLineFlush()
		r.exit.oneIf(val == 0)
	case *syntax.CaseClause:
		trace.string("case ")
		trace.expr(cm.Word)
		trace.string(" in")
		trace.newLineFlush()
		str := r.literal(cm.Word)
		for _, ci := range cm.Items {
			for _, word := range ci.Patterns {
				pattern := r.pattern(word)
				if match(pattern, str) {
					r.stmts(ctx, ci.Stmts)
					return
				}
			}
		}
	case *syntax.TestClause:
		if r.bashTest(ctx, cm.X, false) == "" && r.exit.ok() {
			// to preserve exit status code 2 for regex errors, etc
			r.exit.code = 1
		}
	case *syntax.DeclClause:
		local, global := false, false
		var modes []string
		valType := ""
		switch cm.Variant.Value {
		case "declare":
			// When used in a function, "declare" acts as "local"
			// unless the "-g" option is used.
			local = r.inFunc
		case "local":
			if !r.inFunc {
				r.errf("local: can only be used in a function\n")
				r.exit.code = 1
				return
			}
			local = true
		case "export":
			modes = append(modes, "-x")
		case "readonly":
			modes = append(modes, "-r")
		case "nameref":
			valType = "-n"
		}
	assignLoop:
		for as := range r.flattenAssigns(cm.Args) {
			fp := flagParser{remaining: []string{as.Name.Value}}
			for fp.more() {
				switch flag := fp.flag(); flag {
				case "-x", "-r":
					modes = append(modes, flag)
				case "-a", "-A", "-n":
					valType = flag
				case "-g":
					global = true
				default:
					r.errf("declare: invalid option %q\n", flag)
					r.exit.code = 2
					return
				}
				continue assignLoop
			}
			name := as.Name.Value
			if !syntax.ValidName(name) {
				r.errf("declare: invalid name %q\n", name)
				r.exit.code = 1
				return
			}
			vr := r.lookupVar(as.Name.Value)
			if as.Naked {
				if valType == "-A" {
					vr.Kind = expand.Associative
				} else {
					vr.Kind = expand.KeepValue
				}
			} else {
				vr = r.assignVal(vr, as, valType)
			}
			if global {
				vr.Local = false
			} else if local {
				vr.Local = true
			}
			for _, mode := range modes {
				switch mode {
				case "-x":
					vr.Exported = true
				case "-r":
					vr.ReadOnly = true
				}
			}
			r.setVar(name, vr)
		}
	case *syntax.TimeClause:
		start := time.Now()
		if cm.Stmt != nil {
			r.stmt(ctx, cm.Stmt)
		}
		format := "%s\t%s\n"
		if cm.PosixFormat {
			format = "%s %s\n"
		} else {
			r.outf("\n")
		}
		real := time.Since(start)
		r.outf(format, "real", elapsedString(real, cm.PosixFormat))
		// TODO: can we do these?
		r.outf(format, "user", elapsedString(0, cm.PosixFormat))
		r.outf(format, "sys", elapsedString(0, cm.PosixFormat))
	default:
		panic(fmt.Sprintf("unhandled command node: %T", cm))
	}
}

func (r *Runner) trapCallback(ctx context.Context, callback, name string) {
	if callback == "" {
		return // nothing to do
	}
	if r.handlingTrap {
		return // don't recurse, as that could lead to cycles
	}
	r.handlingTrap = true

	p := syntax.NewParser()
	// TODO: do this parsing when "trap" is called?
	file, err := p.Parse(strings.NewReader(callback), name+" trap")
	if err != nil {
		r.errf(name+"trap: %v\n", err)
		// 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
}

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
				}
			}
		}
	}
}

func match(pat, name string) bool {
	expr, err := pattern.Regexp(pat, pattern.EntireString)
	if err != nil {
		return false
	}
	rx := regexp.MustCompile(expr)
	return rx.MatchString(name)
}

func elapsedString(d time.Duration, posix bool) string {
	if posix {
		return fmt.Sprintf("%.2f", d.Seconds())
	}
	min := int(d.Minutes())
	sec := math.Mod(d.Seconds(), 60.0)
	return fmt.Sprintf("%dm%.3fs", min, sec)
}

func (r *Runner) stmts(ctx context.Context, stmts []*syntax.Stmt) {
	for _, stmt := range stmts {
		r.stmt(ctx, stmt)
	}
}

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)
		go func() {
			pw.WriteString(hdoc)
			pw.Close()
		}()
		return pr, nil
	}
	var buf bytes.Buffer
	var cur []syntax.WordPart
	flushLine := func() {
		if buf.Len() > 0 {
			buf.WriteByte('\n')
		}
		buf.WriteString(r.document(&syntax.Word{Parts: cur}))
		cur = cur[:0]
	}
	for _, wp := range rd.Hdoc.Parts {
		lit, ok := wp.(*syntax.Lit)
		if !ok {
			cur = append(cur, wp)
			continue
		}
		for i, part := range strings.Split(lit.Value, "\n") {
			if i > 0 {
				flushLine()
				cur = cur[:0]
			}
			part = strings.TrimLeft(part, "\t")
			cur = append(cur, &syntax.Lit{Value: part})
		}
	}
	flushLine()
	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 {
		pr, err := r.hdocReader(rd)
		if err != nil {
			return nil, err
		}
		r.stdin = pr
		return pr, nil
	}

	orig := &r.stdout
	if rd.N != nil {
		switch rd.N.Value {
		case "0":
			// Note that the input redirects below always use stdin (0)
			// because we don't support anything else right now.
		case "1":
			// The default for the output redirects below.
		case "2":
			orig = &r.stderr
		default:
			panic(fmt.Sprintf("unsupported redirect fd: %v", rd.N.Value))
		}
	}
	arg := r.literal(rd.Word)
	switch rd.Op {
	case syntax.WordHdoc:
		pr, pw, err := os.Pipe()
		if err != nil {
			return nil, err
		}
		r.stdin = pr
		// We write to the pipe in a new goroutine,
		// as pipe writes may block once the buffer gets full.
		go func() {
			pw.WriteString(arg)
			pw.WriteString("\n")
			pw.Close()
		}()
		return pr, nil
	case syntax.DplOut:
		switch arg {
		case "1":
			*orig = r.stdout
		case "2":
			*orig = r.stderr
		case "-":
			*orig = io.Discard // closing the output writer
		default:
			panic(fmt.Sprintf("unhandled %v arg: %q", rd.Op, arg))
		}
		return nil, nil
	case syntax.RdrIn, syntax.RdrOut, syntax.AppOut,
		syntax.RdrAll, syntax.AppAll:
		// done further below
	case syntax.DplIn:
		switch arg {
		case "-":
			r.stdin = nil // closing the input file
		default:
			panic(fmt.Sprintf("unhandled %v arg: %q", rd.Op, arg))
		}
		return nil, nil
	default:
		panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op))
	}
	mode := os.O_RDONLY
	switch rd.Op {
	case syntax.AppOut, syntax.AppAll:
		mode = os.O_WRONLY | os.O_CREATE | os.O_APPEND
	case syntax.RdrOut, syntax.RdrAll:
		mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
	}
	f, err := r.open(ctx, arg, mode, 0o644, true)
	if err != nil {
		return nil, err
	}
	switch rd.Op {
	case syntax.RdrIn:
		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 f, nil
}

func (r *Runner) loopStmtsBroken(ctx context.Context, stmts []*syntax.Stmt) bool {
	oldInLoop := r.inLoop
	r.inLoop = true
	defer func() { r.inLoop = oldInLoop }()
	for _, stmt := range stmts {
		r.stmt(ctx, stmt)
		if r.contnEnclosing > 0 {
			r.contnEnclosing--
			return r.contnEnclosing > 0
		}
		if r.breakEnclosing > 0 {
			r.breakEnclosing--
			return true
		}
	}
	return false
}

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, handlerKindCall, pos), args)
		if err != nil {
			// handler's custom fatal error
			r.exit.fatal(err)
			return
		}
	}
	name := args[0]
	if body := r.Funcs[name]; body != nil {
		// stack them to support nested func calls
		oldParams := r.Params
		r.Params = args[1:]
		oldInFunc := r.inFunc
		r.inFunc = true

		// Functions run in a nested scope.
		// Note that [Runner.exec] below does something similar.
		origEnv := r.writeEnv
		r.writeEnv = &overlayEnviron{parent: r.writeEnv, funcScope: true}

		r.stmt(ctx, body)

		r.writeEnv = origEnv

		r.Params = oldParams
		r.inFunc = oldInFunc
		r.exit.returning = false
		return
	}
	if IsBuiltin(name) {
		r.exit = r.builtin(ctx, pos, name, args[1:])
		return
	}
	r.exec(ctx, pos, args)
}

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) {
	// 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.exit.fatal(err)
	}
	return nil, err
}

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) (fs.FileInfo, error) {
	path := absPath(r.Dir, name)
	return r.statHandler(ctx, path, false)
}
07070100000039000081A4000000000000000000000001686AE5C00000151C000000000000000000000000000000000000001900000000sh-3.12.0/interp/test.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp

import (
	"context"
	"fmt"
	"os"
	"regexp"

	"golang.org/x/term"

	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/syntax"
)

// non-empty string is true, empty string is false
func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic bool) string {
	switch x := expr.(type) {
	case *syntax.Word:
		if classic {
			// In the classic "test" mode, we already expanded and
			// split the list of words, so don't redo that work.
			return r.document(x)
		}
		return r.literal(x)
	case *syntax.ParenTest:
		return r.bashTest(ctx, x.X, classic)
	case *syntax.BinaryTest:
		switch x.Op {
		case syntax.TsMatchShort, syntax.TsMatch, syntax.TsNoMatch:
			str := r.literal(x.X.(*syntax.Word))
			yw := x.Y.(*syntax.Word)
			if classic { // test, [
				lit := r.literal(yw)
				if (str == lit) == (x.Op != syntax.TsNoMatch) {
					return "1"
				}
			} else { // [[
				pattern := r.pattern(yw)
				if match(pattern, str) == (x.Op != syntax.TsNoMatch) {
					return "1"
				}
			}
			return ""
		}
		if r.binTest(ctx, x.Op, r.bashTest(ctx, x.X, classic), r.bashTest(ctx, x.Y, classic)) {
			return "1"
		}
		return ""
	case *syntax.UnaryTest:
		if r.unTest(ctx, x.Op, r.bashTest(ctx, x.X, classic)) {
			return "1"
		}
		return ""
	}
	return ""
}

func (r *Runner) binTest(ctx context.Context, op syntax.BinTestOperator, x, y string) bool {
	switch op {
	case syntax.TsReMatch:
		re, err := regexp.Compile(y)
		if err != nil {
			r.exit.code = 2
			return false
		}
		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)
		if err1 != nil || err2 != nil {
			return false
		}
		return info1.ModTime().After(info2.ModTime())
	case syntax.TsOlder:
		info1, err1 := r.stat(ctx, x)
		info2, err2 := r.stat(ctx, y)
		if err1 != nil || err2 != nil {
			return false
		}
		return info1.ModTime().Before(info2.ModTime())
	case syntax.TsDevIno:
		info1, err1 := r.stat(ctx, x)
		info2, err2 := r.stat(ctx, y)
		if err1 != nil || err2 != nil {
			return false
		}
		return os.SameFile(info1, info2)
	case syntax.TsEql:
		return atoi(x) == atoi(y)
	case syntax.TsNeq:
		return atoi(x) != atoi(y)
	case syntax.TsLeq:
		return atoi(x) <= atoi(y)
	case syntax.TsGeq:
		return atoi(x) >= atoi(y)
	case syntax.TsLss:
		return atoi(x) < atoi(y)
	case syntax.TsGtr:
		return atoi(x) > atoi(y)
	case syntax.AndTest:
		return x != "" && y != ""
	case syntax.OrTest:
		return x != "" || y != ""
	case syntax.TsBefore:
		return x < y
	default: // syntax.TsAfter
		return x > y
	}
}

func (r *Runner) statMode(ctx context.Context, name string, mode os.FileMode) bool {
	info, err := r.stat(ctx, name)
	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:
		_, err := r.stat(ctx, x)
		return err == nil
	case syntax.TsRegFile:
		info, err := r.stat(ctx, x)
		return err == nil && info.Mode().IsRegular()
	case syntax.TsDirect:
		return r.statMode(ctx, x, os.ModeDir)
	case syntax.TsCharSp:
		return r.statMode(ctx, x, os.ModeCharDevice)
	case syntax.TsBlckSp:
		info, err := r.stat(ctx, x)
		return err == nil && info.Mode()&os.ModeDevice != 0 &&
			info.Mode()&os.ModeCharDevice == 0
	case syntax.TsNmPipe:
		return r.statMode(ctx, x, os.ModeNamedPipe)
	case syntax.TsSocket:
		return r.statMode(ctx, x, os.ModeSocket)
	case syntax.TsSmbLink:
		info, err := r.lstat(ctx, x)
		return err == nil && info.Mode()&os.ModeSymlink != 0
	case syntax.TsSticky:
		return r.statMode(ctx, x, os.ModeSticky)
	case syntax.TsUIDSet:
		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.TsRead:
		return r.access(ctx, r.absPath(x), access_R_OK) == nil
	case syntax.TsWrite:
		return r.access(ctx, r.absPath(x), access_W_OK) == nil
	case syntax.TsExec:
		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 any
		switch fd {
		case 0:
			f = r.stdin
		case 1:
			f = r.stdout
		case 2:
			f = r.stderr
		}
		if f, ok := f.(interface{ Fd() uintptr }); ok {
			// 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
		// "single process" mode.
		return false
	case syntax.TsEmpStr:
		return x == ""
	case syntax.TsNempStr:
		return x != ""
	case syntax.TsOptSet:
		if _, opt := r.optByName(x, false); opt != nil {
			return *opt
		}
		return false
	case syntax.TsVarSet:
		return r.lookupVar(x).IsSet()
	case syntax.TsRefVar:
		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))
	}
}
0707010000003A000081A4000000000000000000000001686AE5C000000F74000000000000000000000000000000000000002100000000sh-3.12.0/interp/test_classic.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp

import (
	"fmt"

	"mvdan.cc/sh/v3/syntax"
)

const illegalTok = 0

type testParser struct {
	eof bool
	val string
	rem []string

	err func(err error)
}

func (p *testParser) errf(format string, a ...any) {
	p.err(fmt.Errorf(format, a...))
}

func (p *testParser) next() {
	if p.eof || len(p.rem) == 0 {
		p.eof = true
		p.val = ""
		return
	}
	p.val = p.rem[0]
	p.rem = p.rem[1:]
}

func (p *testParser) followWord(fval string) *syntax.Word {
	if p.eof {
		p.errf("%s must be followed by a word", fval)
	}
	w := &syntax.Word{Parts: []syntax.WordPart{
		&syntax.Lit{Value: p.val},
	}}
	p.next()
	return w
}

func (p *testParser) classicTest(fval string, pastAndOr bool) syntax.TestExpr {
	var left syntax.TestExpr
	if pastAndOr {
		left = p.testExprBase(fval)
	} else {
		left = p.classicTest(fval, true)
	}
	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)
	}
	b := &syntax.BinaryTest{
		Op: op,
		X:  left,
	}
	p.next()
	switch b.Op {
	case syntax.AndTest, syntax.OrTest:
		if b.Y = p.classicTest(opStr, false); b.Y == nil {
			p.errf("%s must be followed by an expression", opStr)
		}
	default:
		b.Y = p.followWord(opStr)
	}
	return b
}

func (p *testParser) testExprBase(fval string) syntax.TestExpr {
	if p.eof || p.val == ")" {
		return nil
	}
	op := testUnaryOp(p.val)
	switch op {
	case syntax.TsNot:
		u := &syntax.UnaryTest{Op: op}
		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:
		u := &syntax.UnaryTest{Op: op}
		p.next()
		if p.eof {
			// make [ -e ] fall back to [ -n -e ], i.e. use
			// the operator as an argument
			return &syntax.Word{Parts: []syntax.WordPart{
				&syntax.Lit{Value: op.String()},
			}}
		}
		u.X = p.followWord(op.String())
		return u
	}
}

// testUnaryOp is an exact copy of syntax's.
func testUnaryOp(val string) syntax.UnTestOperator {
	switch val {
	case "!":
		return syntax.TsNot
	case "(":
		return syntax.TsParen
	case "-e", "-a":
		return syntax.TsExists
	case "-f":
		return syntax.TsRegFile
	case "-d":
		return syntax.TsDirect
	case "-c":
		return syntax.TsCharSp
	case "-b":
		return syntax.TsBlckSp
	case "-p":
		return syntax.TsNmPipe
	case "-S":
		return syntax.TsSocket
	case "-L", "-h":
		return syntax.TsSmbLink
	case "-k":
		return syntax.TsSticky
	case "-g":
		return syntax.TsGIDSet
	case "-u":
		return syntax.TsUIDSet
	case "-G":
		return syntax.TsGrpOwn
	case "-O":
		return syntax.TsUsrOwn
	case "-N":
		return syntax.TsModif
	case "-r":
		return syntax.TsRead
	case "-w":
		return syntax.TsWrite
	case "-x":
		return syntax.TsExec
	case "-s":
		return syntax.TsNoEmpty
	case "-t":
		return syntax.TsFdTerm
	case "-z":
		return syntax.TsEmpStr
	case "-n":
		return syntax.TsNempStr
	case "-o":
		return syntax.TsOptSet
	case "-v":
		return syntax.TsVarSet
	case "-R":
		return syntax.TsRefVar
	default:
		return illegalTok
	}
}

// testBinaryOp is like syntax's, but with -a and -o, and without =~.
func testBinaryOp(val string) syntax.BinTestOperator {
	switch val {
	case "-a":
		return syntax.AndTest
	case "-o":
		return syntax.OrTest
	case "==", "=":
		return syntax.TsMatch
	case "!=":
		return syntax.TsNoMatch
	case "-nt":
		return syntax.TsNewer
	case "-ot":
		return syntax.TsOlder
	case "-ef":
		return syntax.TsDevIno
	case "-eq":
		return syntax.TsEql
	case "-ne":
		return syntax.TsNeq
	case "-le":
		return syntax.TsLeq
	case "-ge":
		return syntax.TsGeq
	case "-lt":
		return syntax.TsLss
	case "-gt":
		return syntax.TsGtr
	default:
		return illegalTok
	}
}
0707010000003B000081A4000000000000000000000001686AE5C00000092E000000000000000000000000000000000000001A00000000sh-3.12.0/interp/trace.gopackage interp

import (
	"bytes"
	"fmt"
	"io"
	"strings"

	"mvdan.cc/sh/v3/syntax"
)

// tracer prints expressions like a shell would do if its
// options '-o' is set to either 'xtrace' or its shorthand, '-x'.
type tracer struct {
	buf       bytes.Buffer
	printer   *syntax.Printer
	output    io.Writer
	needsPlus bool
}

func (r *Runner) tracer() *tracer {
	if !r.opts[optXTrace] {
		return nil
	}

	return &tracer{
		printer:   syntax.NewPrinter(),
		output:    r.stderr,
		needsPlus: true,
	}
}

// string writes s to tracer.buf if tracer is non-nil,
// prepending "+" if tracer.needsPlus is true.
func (t *tracer) string(s string) {
	if t == nil {
		return
	}

	if t.needsPlus {
		t.buf.WriteString("+ ")
	}
	t.needsPlus = false
	t.buf.WriteString(s)
}

func (t *tracer) stringf(f string, a ...any) {
	if t == nil {
		return
	}

	t.string(fmt.Sprintf(f, a...))
}

// expr prints x to tracer.buf if tracer is non-nil,
// prepending "+" if tracer.isFirstPrint is true.
func (t *tracer) expr(x syntax.Node) {
	if t == nil {
		return
	}

	if t.needsPlus {
		t.buf.WriteString("+ ")
	}
	t.needsPlus = false
	if err := t.printer.Print(&t.buf, x); err != nil {
		panic(err)
	}
}

// flush writes the contents of tracer.buf to the tracer.stdout.
func (t *tracer) flush() {
	if t == nil {
		return
	}

	t.output.Write(t.buf.Bytes())
	t.buf.Reset()
}

// newLineFlush is like flush, but with extra new line before tracer.buf gets flushed.
func (t *tracer) newLineFlush() {
	if t == nil {
		return
	}

	t.buf.WriteString("\n")
	t.flush()
	// reset state
	t.needsPlus = true
}

// call prints a command and its arguments with varying formats depending on the cmd type,
// for example, built-in command's arguments are printed enclosed in single quotes,
// otherwise, call defaults to printing with double quotes.
func (t *tracer) call(cmd string, args ...string) {
	if t == nil {
		return
	}

	s := strings.Join(args, " ")
	if strings.TrimSpace(s) == "" {
		// fields may be empty for function () {} declarations
		t.string(cmd)
	} else if IsBuiltin(cmd) {
		if cmd == "set" {
			// TODO: only first occurrence of set is not printed, succeeding calls are printed
			return
		}

		qs, err := syntax.Quote(s, syntax.LangBash)
		if err != nil { // should never happen
			panic(err)
		}
		t.stringf("%s %s", cmd, qs)
	} else {
		t.stringf("%s %s", cmd, s)
	}
}
0707010000003C000081A4000000000000000000000001686AE5C00000038B000000000000000000000000000000000000002400000000sh-3.12.0/interp/unexported_test.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp

import (
	"testing"
	"time"
)

func TestElapsedString(t *testing.T) {
	t.Parallel()

	tests := []struct {
		in    time.Duration
		posix bool
		want  string
	}{
		{time.Nanosecond, false, "0m0.000s"},
		{time.Millisecond, false, "0m0.001s"},
		{time.Millisecond, true, "0.00"},
		{2500 * time.Millisecond, false, "0m2.500s"},
		{2500 * time.Millisecond, true, "2.50"},
		{
			10*time.Minute + 10*time.Second,
			false,
			"10m10.000s",
		},
		{
			10*time.Minute + 10*time.Second,
			true,
			"610.00",
		},
		{31 * time.Second, false, "0m31.000s"},
		{102 * time.Second, false, "1m42.000s"},
	}
	for _, tc := range tests {
		t.Run(tc.in.String(), func(t *testing.T) {
			got := elapsedString(tc.in, tc.posix)
			if got != tc.want {
				t.Fatalf("wanted %q, got %q", tc.want, got)
			}
		})
	}
}
0707010000003D000081A4000000000000000000000001686AE5C000000E28000000000000000000000000000000000000001E00000000sh-3.12.0/interp/unix_test.go// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

//go:build unix

package interp_test

import (
	"bufio"
	"context"
	"io"
	"os"
	"os/exec"
	"strings"
	"testing"

	"github.com/creack/pty"
	"mvdan.cc/sh/v3/interp"
)

func TestRunnerTerminalStdIO(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		files func(*testing.T) (secondary io.Writer, primary io.Reader)
		want  string
	}{
		{"Nil", func(t *testing.T) (io.Writer, io.Reader) {
			return nil, strings.NewReader("\n")
		}, "\n"},
		{"Pipe", func(t *testing.T) (io.Writer, io.Reader) {
			pr, pw := io.Pipe()
			return pw, pr
		}, "end\n"},
		{"Pseudo", func(t *testing.T) (io.Writer, io.Reader) {
			primary, secondary, err := pty.Open()
			if err != nil {
				t.Fatal(err)
			}
			return secondary, primary
		}, "012end\r\n"},
	}
	file := parse(t, nil, `
		for n in 0 1 2 3; do if [[ -t $n ]]; then echo -n $n; fi; done; echo end
	`)
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			secondary, primary := test.files(t)
			// some secondary ends can be used as stdin too
			secondaryReader, _ := secondary.(io.Reader)

			r, _ := interp.New(interp.StdIO(secondaryReader, secondary, secondary))
			go func() {
				// To mimic [os/exec.Cmd.Start], use a goroutine.
				if err := r.Run(context.Background(), file); err != nil {
					t.Error(err)
				}
			}()

			got, err := bufio.NewReader(primary).ReadString('\n')
			if err != nil {
				t.Fatal(err)
			}
			if got != test.want {
				t.Fatalf("\nwant: %q\ngot:  %q", test.want, got)
			}
			if closer, ok := secondary.(io.Closer); ok {
				if err := closer.Close(); err != nil {
					t.Fatal(err)
				}
			}
			if closer, ok := primary.(io.Closer); ok {
				if err := closer.Close(); err != nil {
					t.Fatal(err)
				}
			}
		})
	}
}

func TestRunnerTerminalExec(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name  string
		start func(*testing.T, *exec.Cmd) io.Reader
		want  string
	}{
		{"Nil", func(t *testing.T, cmd *exec.Cmd) io.Reader {
			if err := cmd.Start(); err != nil {
				t.Fatal(err)
			}
			return strings.NewReader("\n")
		}, "\n"},
		{"Pipe", func(t *testing.T, cmd *exec.Cmd) io.Reader {
			out, err := cmd.StdoutPipe()
			if err != nil {
				t.Fatal(err)
			}
			cmd.Stderr = cmd.Stdout
			if err := cmd.Start(); err != nil {
				t.Fatal(err)
			}
			return out
		}, "end\n"},
		{"Pseudo", func(t *testing.T, cmd *exec.Cmd) io.Reader {
			// Note that we avoid pty.Start,
			// as it closes the secondary terminal via a defer,
			// possibly before the command has finished.
			// That can lead to "signal: hangup" flakes.
			primary, secondary, err := pty.Open()
			if err != nil {
				t.Fatal(err)
			}
			cmd.Stdin = secondary
			cmd.Stdout = secondary
			cmd.Stderr = secondary
			if err := cmd.Start(); err != nil {
				t.Fatal(err)
			}
			return primary
		}, "012end\r\n"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			cmd := exec.Command(os.Getenv("GOSH_PROG"),
				"for n in 0 1 2 3; do if [[ -t $n ]]; then echo -n $n; fi; done; echo end")
			primary := test.start(t, cmd)

			got, err := bufio.NewReader(primary).ReadString('\n')
			if err != nil {
				t.Fatal(err)
			}
			if got != test.want {
				t.Fatalf("\nwant: %q\ngot:  %q", test.want, got)
			}
			if closer, ok := primary.(io.Closer); ok {
				if err := closer.Close(); err != nil {
					t.Fatal(err)
				}
			}
			if err := cmd.Wait(); err != nil {
				t.Fatal(err)
			}
		})
	}
}

func shortPathName(path string) (string, error) {
	panic("only works on windows")
}
0707010000003E000081A4000000000000000000000001686AE5C000002791000000000000000000000000000000000000001900000000sh-3.12.0/interp/vars.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp

import (
	cryptorand "crypto/rand"
	"encoding/binary"
	"fmt"
	"maps"
	mathrand "math/rand/v2"
	"os"
	"runtime"
	"slices"
	"strconv"
	"strings"

	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/syntax"
)

func newOverlayEnviron(parent expand.Environ, background bool) *overlayEnviron {
	oenv := &overlayEnviron{}
	if !background {
		oenv.parent = parent
	} else {
		// We could do better here if the parent is also an overlayEnviron;
		// measure with profiles or benchmarks before we choose to do so.
		oenv.values = make(map[string]expand.Variable)
		maps.Insert(oenv.values, parent.Each)
	}
	return oenv
}

// overlayEnviron is our main implementation of [expand.WriteEnviron].
type overlayEnviron struct {
	// parent is non-nil if [values] is an overlay over a parent environment
	// which we can safely reuse without data races, such as non-background subshells
	// or function calls.
	parent expand.Environ
	values map[string]expand.Variable

	// We need to know if the current scope is a function's scope, because
	// functions can modify global variables. When true, [parent] must not be nil.
	funcScope bool
}

func (o *overlayEnviron) Get(name string) expand.Variable {
	if vr, ok := o.values[name]; ok {
		return vr
	}
	if o.parent != nil {
		return o.parent.Get(name)
	}
	return expand.Variable{}
}

func (o *overlayEnviron) Set(name string, vr expand.Variable) error {
	prev, inOverlay := o.values[name]
	// Manipulation of a global var inside a function.
	if o.funcScope && !vr.Local && !prev.Local {
		// In a function, the parent environment is ours, so it's always read-write.
		return o.parent.(expand.WriteEnviron).Set(name, vr)
	}
	if !inOverlay && o.parent != nil {
		prev = o.parent.Get(name)
	}

	if o.values == nil {
		o.values = make(map[string]expand.Variable)
	}
	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)
	}
	// modifying the entire variable
	vr.Local = prev.Local || vr.Local
	o.values[name] = vr
	return nil
}

func (o *overlayEnviron) Each(f func(name string, vr expand.Variable) bool) {
	if o.parent != nil {
		o.parent.Each(f)
	}
	for name, vr := range o.values {
		if !f(name, vr) {
			return
		}
	}
}

func execEnv(env expand.Environ) []string {
	list := make([]string, 0, 64)
	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
			// list. Seems like zeroing the element is enough.
			// This is a linear search, but this scenario should be
			// rare, and the number of variables shouldn't be large.
			for i, kv := range list {
				if strings.HasPrefix(kv, name+"=") {
					list[i] = ""
				}
			}
		}
		if vr.Exported && vr.Kind == expand.String {
			list = append(list, name+"="+vr.String())
		}
	}
	return list
}

func (r *Runner) lookupVar(name string) expand.Variable {
	if name == "" {
		panic("variable name must not be empty")
	}
	var vr expand.Variable
	switch name {
	case "#":
		vr.Kind, vr.Str = expand.String, strconv.Itoa(len(r.Params))
	case "@", "*":
		vr.Kind = expand.Indexed
		if r.Params == nil {
			// r.Params may be nil but positional parameters always exist
			vr.List = []string{}
		} 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(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":
		vr.Kind = expand.String
		if r.filename != "" {
			vr.Str = r.filename
		} else {
			vr.Str = "gosh"
		}
	case "1", "2", "3", "4", "5", "6", "7", "8", "9":
		if i := int(name[0] - '1'); i < len(r.Params) {
			vr.Kind = expand.String
			vr.Str = r.Params[i]
		}
	}
	if vr.Kind != expand.Unknown {
		vr.Set = true
		return vr
	}
	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.Declared() {
			return vr
		}
	}
	return expand.Variable{}
}

func (r *Runner) envGet(name string) string {
	return r.lookupVar(name).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.code = 1
		return
	}
}

func (r *Runner) setVarString(name, value string) {
	r.setVar(name, expand.Variable{Set: true, Kind: expand.String, Str: value})
}

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.code = 1
		return
	}
}

func (r *Runner) setVarWithIndex(prev expand.Variable, name string, index syntax.ArithmExpr, vr expand.Variable) {
	prev.Set = true
	if name2, var2 := prev.Resolve(r.writeEnv); name2 != "" {
		name = name2
		prev = var2
	}

	if vr.Kind == expand.String && index == nil {
		// When assigning a string to an array, fall back to the
		// zero value for the index.
		switch prev.Kind {
		case expand.Indexed:
			index = &syntax.Word{Parts: []syntax.WordPart{
				&syntax.Lit{Value: "0"},
			}}
		case expand.Associative:
			index = &syntax.Word{Parts: []syntax.WordPart{
				&syntax.DblQuoted{},
			}}
		}
	}
	if index == nil {
		r.setVar(name, vr)
		return
	}

	// from the syntax package, we know that value must be a string if index
	// is non-nil; nested arrays are forbidden.
	valStr := vr.Str

	var list []string
	switch prev.Kind {
	case expand.String:
		list = append(list, prev.Str)
	case expand.Indexed:
		// 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
		w, ok := index.(*syntax.Word)
		if !ok {
			return
		}
		k := r.literal(w)

		// 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)
	for len(list) < k+1 {
		list = append(list, "")
	}
	list[k] = valStr
	prev.Kind = expand.Indexed
	prev.List = list
	r.setVar(name, prev)
}

func (r *Runner) setFunc(name string, body *syntax.Stmt) {
	if r.Funcs == nil {
		r.Funcs = make(map[string]*syntax.Stmt, 4)
	}
	r.Funcs[name] = body
}

func stringIndex(index syntax.ArithmExpr) bool {
	w, ok := index.(*syntax.Word)
	if !ok || len(w.Parts) != 1 {
		return false
	}
	switch w.Parts[0].(type) {
	case *syntax.DblQuoted, *syntax.SglQuoted:
		return true
	}
	return false
}

// TODO: make assignVal and [setVar] consistent with the [expand.WriteEnviron] interface

func (r *Runner) assignVal(prev expand.Variable, as *syntax.Assign, valType string) expand.Variable {
	prev.Set = true
	if as.Value != nil {
		s := r.literal(as.Value)
		if !as.Append {
			prev.Kind = expand.String
			if valType == "-n" {
				prev.Kind = expand.NameRef
			}
			prev.Str = s
			return prev
		}
		switch prev.Kind {
		case expand.String, expand.Unknown:
			prev.Kind = expand.String
			prev.Str += s
		case expand.Indexed:
			if len(prev.List) == 0 {
				prev.List = append(prev.List, "")
			}
			prev.List[0] += s
		case expand.Associative:
			// TODO
		}
		return prev
	}
	if as.Array == nil {
		// don't return the zero value, as that's an unset variable
		prev.Kind = expand.String
		if valType == "-n" {
			prev.Kind = expand.NameRef
		}
		prev.Str = ""
		return prev
	}
	// Array assignment.
	elems := as.Array.Elems
	if valType == "" {
		valType = "-a" // indexed
		if len(elems) > 0 && stringIndex(elems[0].Index) {
			valType = "-A" // associative
		}
	}
	if valType == "-A" {
		amap := make(map[string]string, len(elems))
		for _, elem := range elems {
			k := r.literal(elem.Index.(*syntax.Word))
			amap[k] = r.literal(elem.Value)
		}
		if !as.Append {
			prev.Kind = expand.Associative
			prev.Map = amap
			return prev
		}
		// TODO
		return prev
	}
	// Evaluate values for each array element.
	elemValues := make([]struct {
		index  int
		values []string
	}, len(elems))
	var index, maxIndex int
	for i, elem := range elems {
		if elem.Index != nil {
			// Index resets our index with a literal value.
			index = r.arithm(elem.Index)
			elemValues[i].values = []string{r.literal(elem.Value)}
		} else {
			// Implicit index, advancing for every word.
			elemValues[i].values = r.fields(elem.Value)
		}
		elemValues[i].index = index
		index += len(elemValues[i].values)
		maxIndex = max(maxIndex, index)
	}
	// Flatten down the values.
	strs := make([]string, maxIndex)
	for _, ev := range elemValues {
		for i, str := range ev.values {
			strs[ev.index+i] = str
		}
	}
	if !as.Append {
		prev.Kind = expand.Indexed
		prev.List = strs
		return prev
	}
	switch prev.Kind {
	case expand.Unknown:
		prev.Kind = expand.Indexed
		prev.List = strs
	case expand.String:
		prev.Kind = expand.Indexed
		prev.List = append([]string{prev.Str}, strs...)
	case expand.Indexed:
		prev.List = append(prev.List, strs...)
	case expand.Associative:
		// TODO
	default:
		panic(fmt.Sprintf("unhandled conversion of kind %d", prev.Kind))
	}
	return prev
}
0707010000003F000081A4000000000000000000000001686AE5C00000028B000000000000000000000000000000000000002100000000sh-3.12.0/interp/windows_test.go// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

//go:build windows

package interp_test

import "golang.org/x/sys/windows"

// shortPathName is used for testing against DOS short names.
//
// Only used for testing, so we assume that a short path always fits in
// 2*len(path) in UTF-16.
func shortPathName(path string) (string, error) {
	src, err := windows.UTF16FromString(path)
	if err != nil {
		return "", err
	}
	dst := make([]uint16, len(src)*2)
	if _, err := windows.GetShortPathName(&src[0], &dst[0], uint32(len(dst))); err != nil {
		return "", err
	}
	return windows.UTF16ToString(dst), nil
}
07070100000040000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001200000000sh-3.12.0/pattern07070100000041000081A4000000000000000000000001686AE5C000000381000000000000000000000000000000000000002200000000sh-3.12.0/pattern/example_test.go// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package pattern_test

import (
	"fmt"
	"regexp"

	"mvdan.cc/sh/v3/pattern"
)

func ExampleRegexp() {
	pat := "foo?bar*"
	fmt.Println(pat)

	expr, err := pattern.Regexp(pat, 0)
	if err != nil {
		return
	}
	fmt.Println(expr)

	rx := regexp.MustCompile(expr)
	fmt.Println(rx.MatchString("foo bar baz"))
	fmt.Println(rx.MatchString("foobarbaz"))
	// Output:
	// foo?bar*
	// (?s)foo.bar.*
	// true
	// false
}

func ExampleQuoteMeta() {
	pat := "foo?bar*"
	const mode = 0
	fmt.Println(pat)

	quoted := pattern.QuoteMeta(pat, mode)
	fmt.Println(quoted)

	expr, err := pattern.Regexp(quoted, mode)
	if err != nil {
		return
	}

	rx := regexp.MustCompile(expr)
	fmt.Println(rx.MatchString("foo bar baz"))
	fmt.Println(rx.MatchString("foo?bar*"))
	// Output:
	// foo?bar*
	// foo\?bar\*
	// false
	// true
}
07070100000042000081A4000000000000000000000001686AE5C0000021A3000000000000000000000000000000000000001D00000000sh-3.12.0/pattern/pattern.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package pattern allows working with shell pattern matching notation, also
// known as wildcards or globbing.
//
// For reference, see
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13.
package pattern

import (
	"fmt"
	"io"
	"regexp"
	"strings"
	"unicode/utf8"
)

// Mode can be used to supply a number of options to the package's functions.
// Not all functions change their behavior with all of the options below.
type Mode uint

type SyntaxError struct {
	msg string
	err error
}

func (e SyntaxError) Error() string { return e.msg }

func (e SyntaxError) Unwrap() error { return e.err }

// TODO(v4): flip NoGlobStar to be opt-in via GlobStar, matching bash
// TODO(v4): flip EntireString to be opt-out via PartialMatch, as EntireString causes subtle bugs when forgotten
// TODO(v4): rename NoGlobCase to CaseInsensitive for readability

const (
	Shortest     Mode = 1 << iota // prefer the shortest match.
	Filenames                     // "*" and "?" don't match slashes; only "**" does
	EntireString                  // match the entire string using ^$ delimiters
	NoGlobCase                    // Do case-insensitive match (that is, use (?i) in the regexp)
	NoGlobStar                    // Do not support "**"
)

// 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].
//
// For example, Regexp(`foo*bar?`, true) returns `foo.*bar.`.
//
// 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) {
	// 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
		}
	}
	var sb strings.Builder
	// Enable matching `\n` with the `.` metacharacter as globs match `\n`
	sb.WriteString("(?s")
	if mode&NoGlobCase != 0 {
		sb.WriteString("i")
	}
	if mode&Shortest != 0 {
		sb.WriteString("U")
	}
	sb.WriteString(")")
	if mode&EntireString != 0 {
		sb.WriteString("^")
	}
	sl := stringLexer{s: pat}
	for {
		if err := regexpNext(&sb, &sl, mode); err == io.EOF {
			break
		} else if err != nil {
			return "", err
		}
	}
	if mode&EntireString != 0 {
		sb.WriteString("$")
	}
	return sb.String(), nil
}

// stringLexer helps us tokenize a pattern string.
// Note that we can use the null byte '\x00' to signal "no character" as shell strings cannot contain null bytes.
// TODO: should the tokenization be based on runes? e.g: [á-é]
type stringLexer struct {
	s string
	i int
}

func (sl *stringLexer) next() byte {
	if sl.i >= len(sl.s) {
		return '\x00'
	}
	c := sl.s[sl.i]
	sl.i++
	return c
}

func (sl *stringLexer) last() byte {
	if sl.i < 2 {
		return '\x00'
	}
	return sl.s[sl.i-2]
}

func (sl *stringLexer) peekNext() byte {
	if sl.i >= len(sl.s) {
		return '\x00'
	}
	return sl.s[sl.i]
}

func (sl *stringLexer) peekRest() string {
	return sl.s[sl.i:]
}

func regexpNext(sb *strings.Builder, sl *stringLexer, mode Mode) error {
	switch c := sl.next(); c {
	case '\x00':
		return io.EOF
	case '*':
		if mode&Filenames == 0 {
			// * - matches anything when not in filename mode
			sb.WriteString(".*")
			break
		}
		// "**" only acts as globstar if it is alone as a path element.
		singleBefore := sl.i == 1 || sl.last() == '/'
		if sl.peekNext() == '*' {
			sl.i++
			singleAfter := sl.i == len(sl.s) || sl.peekNext() == '/'
			if mode&NoGlobStar == 0 && singleBefore && singleAfter {
				if sl.peekNext() == '/' {
					// **/ - like "**" but requiring a trailing slash when matching
					sl.i++
					sb.WriteString("((/|[^/.][^/]*)*/)?")
				} else {
					// ** - match any number of slashes or "*" path elements
					sb.WriteString("(/|[^/.][^/]*)*")
				}
				break
			}
			// foo**, **bar, or NoGlobStar - behaves like "*" below
		}
		// * - matches anything except slashes and leading dots
		if singleBefore {
			sb.WriteString("([^/.][^/]*)?")
		} else {
			sb.WriteString("[^/]*")
		}
	case '?':
		if mode&Filenames != 0 {
			sb.WriteString("[^/]")
		} else {
			sb.WriteByte('.')
		}
	case '\\':
		c = sl.next()
		if c == '\x00' {
			return &SyntaxError{msg: `\ at end of pattern`}
		}
		sb.WriteString(regexp.QuoteMeta(string(c)))
	case '[':
		// TODO: surely char classes can be mixed with others, e.g. [[:foo:]xyz]
		if name, err := charClass(sl.peekRest()); err != nil {
			return &SyntaxError{msg: "charClass invalid", err: err}
		} else if name != "" {
			sb.WriteByte('[')
			sb.WriteString(name)
			sl.i += len(name)
			break
		}
		if mode&Filenames != 0 {
			for _, c := range sl.peekRest() {
				if c == ']' {
					break
				} else if c == '/' {
					sb.WriteString("\\[")
					return nil
				}
			}
		}
		sb.WriteByte(c)
		if c = sl.next(); c == '\x00' {
			return &SyntaxError{msg: "[ was not matched with a closing ]"}
		}
		switch c {
		case '!', '^':
			sb.WriteByte('^')
			if c = sl.next(); c == '\x00' {
				return &SyntaxError{msg: "[ was not matched with a closing ]"}
			}
		}
		if c == ']' {
			sb.WriteByte(']')
			if c = sl.next(); c == '\x00' {
				return &SyntaxError{msg: "[ was not matched with a closing ]"}
			}
		}
		for {
			sb.WriteByte(c)
			switch c {
			case '\x00':
				return &SyntaxError{msg: "[ was not matched with a closing ]"}
			case '\\':
				if c = sl.next(); c != '0' {
					sb.WriteByte(c)
				}
			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.WriteByte(c)
		} else {
			sb.WriteString(regexp.QuoteMeta(string(c)))
		}
	}
	return nil
}

func charClass(s string) (string, error) {
	if strings.HasPrefix(s, "[.") || strings.HasPrefix(s, "[=") {
		return "", fmt.Errorf("collating features not available")
	}
	name, ok := strings.CutPrefix(s, "[:")
	if !ok {
		return "", nil
	}
	name, _, ok = strings.Cut(name, ":]]")
	if !ok {
		return "", fmt.Errorf("[[: was not matched with a closing :]]")
	}
	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)+5], nil
}

// HasMeta returns whether a string contains any unescaped pattern
// metacharacters: '*', '?', or '['. When the function returns false, the given
// pattern can only match at most one string.
//
// For example, HasMeta(`foo\*bar`) returns false, but HasMeta(`foo*bar`)
// returns true.
//
// 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] {
		case '\\':
			i++
		case '*', '?', '[':
			return true
		}
	}
	return false
}

// QuoteMeta returns a string that quotes all pattern metacharacters in the
// 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 {
	needsEscaping := false
loop:
	for _, r := range pat {
		switch r {
		case '*', '?', '[', '\\':
			needsEscaping = true
			break loop
		}
	}
	if !needsEscaping { // short-cut without a string copy
		return pat
	}
	var sb strings.Builder
	for _, r := range pat {
		switch r {
		case '*', '?', '[', '\\':
			sb.WriteByte('\\')
		}
		sb.WriteRune(r)
	}
	return sb.String()
}
07070100000043000081A4000000000000000000000001686AE5C000001A75000000000000000000000000000000000000002200000000sh-3.12.0/pattern/pattern_test.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package pattern

import (
	"fmt"
	"regexp"
	"regexp/syntax"
	"testing"

	"github.com/go-quicktest/qt"
)

var regexpTests = []struct {
	pat     string
	mode    Mode
	want    string
	wantErr string

	mustMatch    []string
	mustNotMatch []string
}{
	{pat: ``, want: ``},
	{pat: `foo`, want: `foo`},
	{
		pat: `foo`, mode: NoGlobCase, want: `(?si)foo`,
		mustMatch:    []string{"foo", "FOO", "Foo"},
		mustNotMatch: []string{"bar"},
	},
	{pat: `foóà中`, mode: Filenames, want: `foóà中`},
	{pat: `.`, want: `(?s)\.`},
	{pat: `foo*`, want: `(?s)foo.*`},
	{pat: `foo*`, mode: Shortest, want: `(?sU)foo.*`},
	{pat: `foo*`, mode: Shortest | Filenames, want: `(?sU)foo[^/]*`},
	{
		pat: `*foo*`, mode: EntireString, want: `(?s)^.*foo.*$`,
		mustMatch:    []string{"foo", "prefix-foo", "foo-suffix", "foo.suffix", ".foo.", "a\nbfooc\nd"},
		mustNotMatch: []string{"bar"},
	},
	{
		pat: `foo*`, mode: Filenames | EntireString, want: `(?s)^foo[^/]*$`,
		mustMatch:    []string{"foo", "foo-suffix", "foo.suffix", "foo\nsuffix"},
		mustNotMatch: []string{"prefix-foo", "foo/suffix"},
	},
	{
		pat: `foo/*`, mode: Filenames | EntireString, want: `(?s)^foo/([^/.][^/]*)?$`,
		mustMatch:    []string{"foo/", "foo/suffix"},
		mustNotMatch: []string{"foo/.suffix"},
	},
	{pat: `*foo`, mode: Filenames, want: `(?s)([^/.][^/]*)?foo`},
	{
		pat: `*foo`, mode: Filenames | EntireString, want: `(?s)^([^/.][^/]*)?foo$`,
		mustMatch:    []string{"foo", "prefix-foo", "prefix.foo"},
		mustNotMatch: []string{"foo-suffix", "/prefix/foo", ".foo", ".prefix-foo"},
	},
	{pat: `**`, want: `(?s).*.*`},
	{
		pat: `**`, mode: Filenames | EntireString, want: `(?s)^(/|[^/.][^/]*)*$`,
		mustMatch:    []string{"/foo", "/prefix/foo", "/a.b.c/foo", "/a/b/c/foo", "/foo/suffix.ext", "/a\n/\nb"},
		mustNotMatch: []string{"/.prefix/foo", "/prefix/.foo"},
	},
	{
		pat: `**`, mode: Filenames | NoGlobStar | EntireString, want: `(?s)^([^/.][^/]*)?$`,
		mustMatch:    []string{"foo.bar"},
		mustNotMatch: []string{"foo/bar", ".foo"},
	},
	{pat: `/**/foo`, want: `(?s)/.*.*/foo`},
	{
		pat: `/**/foo`, mode: Filenames | EntireString, want: `(?s)^/((/|[^/.][^/]*)*/)?foo$`,
		mustMatch:    []string{"/foo", "/prefix/foo", "/a.b.c/foo", "/a/b/c/foo"},
		mustNotMatch: []string{"/foo/suffix", "prefix/foo", "/.prefix/foo", "/prefix/.foo"},
	},
	{pat: `/**/foo`, mode: Filenames | NoGlobStar, want: `(?s)/([^/.][^/]*)?/foo`},
	{pat: `/**/à`, mode: Filenames, want: `(?s)/((/|[^/.][^/]*)*/)?à`},
	{
		pat: `/**foo`, mode: Filenames, want: `(?s)/([^/.][^/]*)?foo`,
		// These all match because without EntireString, we match substrings.
		mustMatch: []string{"/foo", "/prefix-foo", "/foo-suffix", "/sub/foo"},
	},
	{
		pat: `/**foo`, mode: Filenames | EntireString, want: `(?s)^/([^/.][^/]*)?foo$`,
		mustMatch:    []string{"/foo", "/prefix-foo"},
		mustNotMatch: []string{"/foo-suffix", "/sub/foo", "/.foo", "/.prefix-foo"},
	},
	{
		pat: `/foo**`, mode: Filenames | EntireString, want: `(?s)^/foo[^/]*$`,
		mustMatch:    []string{"/foo", "/foo-suffix", "/foo.suffix"},
		mustNotMatch: []string{"/prefix-foo", "/foo/sub"},
	},
	{pat: `\*`, want: `(?s)\*`},
	{pat: `\`, wantErr: `^\\ at end of pattern$`},
	{pat: `?`, want: `(?s).`},
	{
		pat: `?`, mode: EntireString, want: `(?s)^.$`,
		mustMatch:    []string{"a", "\n", " "},
		mustNotMatch: []string{"abc", ""},
	},
	{pat: `?`, mode: Filenames, want: `(?s)[^/]`},
	{pat: `?à`, want: `(?s).à`},
	{pat: `\a`, want: `(?s)a`},
	{pat: `(`, want: `(?s)\(`},
	{pat: `a|b`, want: `(?s)a\|b`},
	{pat: `x{3}`, want: `(?s)x\{3\}`},
	{pat: `{3,4}`, want: `(?s)\{3,4\}`},
	{pat: `[a]`, want: `(?s)[a]`},
	{pat: `[abc]`, want: `(?s)[abc]`},
	{pat: `[^bc]`, want: `(?s)[^bc]`},
	{pat: `[!bc]`, want: `(?s)[^bc]`},
	{pat: `[[]`, want: `(?s)[[]`},
	{pat: `[\]]`, want: `(?s)[\]]`},
	{pat: `[\]]`, mode: Filenames, want: `(?s)[\]]`},
	{pat: `[]]`, want: `(?s)[]]`},
	{pat: `[!]]`, want: `(?s)[^]]`},
	{pat: `[^]]`, want: `(?s)[^]]`},
	{pat: `[a/b]`, want: `(?s)[a/b]`},
	{pat: `[a/b]`, mode: Filenames, want: `(?s)\[a/b\]`},
	{pat: `[`, wantErr: `^\[ was not matched with a closing \]$`},
	{pat: `[\`, wantErr: `^\[ was not matched with a closing \]$`},
	{pat: `[^`, wantErr: `^\[ was not matched with a closing \]$`},
	{pat: `[!`, wantErr: `^\[ was not matched with a closing \]$`},
	{pat: `[!bc]`, want: `(?s)[^bc]`},
	{pat: `[]`, wantErr: `^\[ was not matched with a closing \]$`},
	{pat: `[^]`, wantErr: `^\[ was not matched with a closing \]$`},
	{pat: `[!]`, wantErr: `^\[ was not matched with a closing \]$`},
	{pat: `[ab`, wantErr: `^\[ was not matched with a closing \]$`},
	{pat: `[a-]`, want: `(?s)[a-]`},
	{pat: `[z-a]`, wantErr: `^invalid range: z-a$`},
	{pat: `[a-a]`, want: `(?s)[a-a]`},
	{pat: `[aa]`, want: `(?s)[aa]`},
	{pat: `[0-4A-Z]`, want: `(?s)[0-4A-Z]`},
	{pat: `[-a]`, want: `(?s)[-a]`},
	{pat: `[^-a]`, want: `(?s)[^-a]`},
	{pat: `[a-]`, want: `(?s)[a-]`},
	{pat: `[[:digit:]]`, want: `(?s)[[:digit:]]`},
	{pat: `[[:`, wantErr: `^charClass invalid$`},
	{pat: `[[:digit`, wantErr: `^charClass invalid$`},
	{pat: `[[:wrong:]]`, wantErr: `^charClass invalid$`},
	{pat: `[[=x=]]`, wantErr: `^charClass invalid$`},
	{pat: `[[.x.]]`, wantErr: `^charClass invalid$`},
}

func TestRegexp(t *testing.T) {
	t.Parallel()
	for i, tc := range regexpTests {
		t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
			t.Logf("pattern input: %q\n", tc.pat)
			got, gotErr := Regexp(tc.pat, tc.mode)
			if tc.wantErr != "" {
				qt.Assert(t, qt.ErrorMatches(gotErr, tc.wantErr))
			} else {
				qt.Assert(t, qt.IsNil(gotErr))
			}
			if got != tc.want {
				t.Fatalf("(%q, %#b) got %q, wanted %q", tc.pat, tc.mode, got, tc.want)
			}
			_, rxErr := syntax.Parse(got, syntax.Perl)
			if gotErr == nil && rxErr != nil {
				t.Fatalf("regexp/syntax.Parse(%q) failed with %q", got, rxErr)
			}
			rx := regexp.MustCompile(got)
			for _, s := range tc.mustMatch {
				qt.Check(t, qt.IsTrue(rx.MatchString(s)), qt.Commentf("must match: %q", s))
			}
			for _, s := range tc.mustNotMatch {
				qt.Check(t, qt.IsFalse(rx.MatchString(s)), qt.Commentf("must not match: %q", s))
			}
		})
	}
}

var metaTests = []struct {
	pat       string
	wantHas   bool
	wantQuote string
}{
	{``, false, ``},
	{`foo`, false, `foo`},
	{`.`, false, `.`},
	{`*`, true, `\*`},
	{`foo?`, true, `foo\?`},
	{`\[`, false, `\\\[`},
	{`{`, false, `{`},
}

func TestMeta(t *testing.T) {
	t.Parallel()
	for _, tc := range metaTests {
		if got := HasMeta(tc.pat, 0); got != tc.wantHas {
			t.Errorf("HasMeta(%q, 0) got %t, wanted %t",
				tc.pat, got, tc.wantHas)
		}
		if got := QuoteMeta(tc.pat, 0); got != tc.wantQuote {
			t.Errorf("QuoteMeta(%q, 0) got %q, wanted %q",
				tc.pat, got, tc.wantQuote)
		}
	}
}
07070100000044000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001000000000sh-3.12.0/shell07070100000045000081A4000000000000000000000001686AE5C000000259000000000000000000000000000000000000001700000000sh-3.12.0/shell/doc.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package shell contains high-level features that use the syntax, expand, and
// interp packages under the hood.
//
// Please note that this package uses POSIX Shell syntax. As such, path names on
// Windows need to use double backslashes or be within single quotes when given
// to functions like Fields. For example:
//
//	shell.Fields("echo /foo/bar")     // on Unix-like
//	shell.Fields("echo C:\\foo\\bar") // on Windows
//	shell.Fields("echo 'C:\foo\bar'") // on Windows, with quotes
package shell
07070100000046000081A4000000000000000000000001686AE5C000000477000000000000000000000000000000000000002000000000sh-3.12.0/shell/example_test.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package shell_test

import (
	"fmt"

	"mvdan.cc/sh/v3/shell"
)

func ExampleExpand() {
	env := func(name string) string {
		switch name {
		case "HOME":
			return "/home/user"
		}
		return "" // leave the rest unset
	}
	out, _ := shell.Expand("No place like $HOME", env)
	fmt.Println(out)

	out, _ = shell.Expand("Some vars are ${missing:-awesome}", env)
	fmt.Println(out)

	out, _ = shell.Expand("Math is fun! $((12 * 34))", nil)
	fmt.Println(out)
	// Output:
	// No place like /home/user
	// Some vars are awesome
	// Math is fun! 408
}

func ExampleFields() {
	env := func(name string) string {
		switch name {
		case "foo":
			return "bar baz"
		}
		return "" // leave the rest unset
	}
	out, _ := shell.Fields(`"many quoted" ' strings '`, env)
	fmt.Printf("%#v\n", out)

	out, _ = shell.Fields("unquoted $foo", env)
	fmt.Printf("%#v\n", out)

	out, _ = shell.Fields(`quoted "$foo"`, env)
	fmt.Printf("%#v\n", out)
	// Output:
	// []string{"many quoted", " strings "}
	// []string{"unquoted", "bar", "baz"}
	// []string{"quoted", "bar baz"}
}
07070100000047000081A4000000000000000000000001686AE5C000000794000000000000000000000000000000000000001A00000000sh-3.12.0/shell/expand.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package shell

import (
	"os"
	"strings"

	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/syntax"
)

// Expand performs shell expansion on s as if it were within double quotes,
// using env to resolve variables. This includes parameter expansion, arithmetic
// expansion, and quote removal.
//
// If env is nil, the current environment variables are used. Empty variables
// are treated as unset; to support variables which are set but empty, use the
// expand package directly.
//
// Command substitutions like $(echo foo) aren't supported to avoid running
// arbitrary code. To support those, use an interpreter with the expand package.
//
// An error will be reported if the input string had invalid syntax.
func Expand(s string, env func(string) string) (string, error) {
	p := syntax.NewParser()
	word, err := p.Document(strings.NewReader(s))
	if err != nil {
		return "", err
	}
	if env == nil {
		env = os.Getenv
	}
	cfg := &expand.Config{Env: expand.FuncEnviron(env)}
	return expand.Document(cfg, word)
}

// Fields performs shell expansion on s as if it were a command's arguments,
// using env to resolve variables. It is similar to Expand, but includes brace
// expansion, tilde expansion, and globbing.
//
// If env is nil, the current environment variables are used. Empty variables
// are treated as unset; to support variables which are set but empty, use the
// expand package directly.
//
// An error will be reported if the input string had invalid syntax.
func Fields(s string, env func(string) string) ([]string, error) {
	p := syntax.NewParser()
	var words []*syntax.Word
	for w, err := range p.WordsSeq(strings.NewReader(s)) {
		if err != nil {
			return nil, err
		}
		words = append(words, w)
	}
	if env == nil {
		env = os.Getenv
	}
	cfg := &expand.Config{Env: expand.FuncEnviron(env)}
	return expand.Fields(cfg, words...)
}
07070100000048000081A4000000000000000000000001686AE5C000000ABD000000000000000000000000000000000000001F00000000sh-3.12.0/shell/expand_test.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package shell

import (
	"fmt"
	"os"
	"reflect"
	"runtime"
	"strings"
	"testing"
)

func strEnviron(pairs ...string) func(string) string {
	return func(name string) string {
		prefix := name + "="
		for _, pair := range pairs {
			if val, ok := strings.CutPrefix(pair, prefix); ok {
				return val
			}
		}
		return ""
	}
}

var expandTests = []struct {
	in   string
	env  func(name string) string
	want string
}{
	{"foo", nil, "foo"},
	{"\nfoo\n", nil, "\nfoo\n"},
	{"a-$b-c", nil, "a--c"},
	{"${INTERP_GLOBAL:+hasOsEnv}", nil, "hasOsEnv"},
	{"a-$b-c", strEnviron(), "a--c"},
	{"a-$b-c", strEnviron("b=b_val"), "a-b_val-c"},
	{"${x//o/a}", strEnviron("x=foo"), "faa"},
	{"*.go", nil, "*.go"},
	{"~", nil, "~"},
}

func TestExpand(t *testing.T) {
	os.Setenv("INTERP_GLOBAL", "value")
	for _, tc := range expandTests {
		t.Run("", func(t *testing.T) {
			t.Parallel()
			got, err := Expand(tc.in, tc.env)
			if err != nil {
				t.Fatal(err)
			}
			if got != tc.want {
				t.Fatalf("\nwant: %q\ngot:  %q", tc.want, got)
			}
		})
	}
}

func TestUnexpectedCmdSubst(t *testing.T) {
	t.Parallel()
	want := "unexpected command substitution at 1:6"
	for _, fn := range []func() error{
		func() error {
			_, err := Expand("echo $(uname -a)", nil)
			return err
		},
		func() error {
			_, err := Fields("echo $(uname -a)", nil)
			return err
		},
	} {
		got := fmt.Sprint(fn())
		if !strings.Contains(got, want) {
			t.Fatalf("wanted error %q, got: %s", want, got)
		}
	}
}

var fieldsTests = []struct {
	in   string
	env  func(name string) string
	want []string
}{
	{"foo", nil, []string{"foo"}},
	{"\nfoo\n", nil, []string{"foo"}},
	{"foo bar", nil, []string{"foo", "bar"}},
	{"foo 'bar baz'", nil, []string{"foo", "bar baz"}},
	{"$x", strEnviron("x=foo bar"), []string{"foo", "bar"}},
	{`"$x"`, strEnviron("x=foo bar"), []string{"foo bar"}},
	{"~", strEnviron("HOME=/my/home"), []string{"/my/home"}},
	{"~/foo/bar", strEnviron("HOME=/my/home"), []string{"/my/home/foo/bar"}},
	{"~foo/file", strEnviron("HOME foo=/bar"), []string{"/bar/file"}},
	{"*.go", nil, []string{"*.go"}},

	{"~", func(name string) string {
		switch runtime.GOOS {
		case "windows":
			if name == "USERPROFILE" {
				return "/my/home"
			}
		default:
			if name == "HOME" {
				return "/my/home"
			}
		}
		return ""
	}, []string{"/my/home"}},
}

func TestFields(t *testing.T) {
	os.Setenv("INTERP_GLOBAL", "value")
	for _, tc := range fieldsTests {
		t.Run("", func(t *testing.T) {
			t.Parallel()
			got, err := Fields(tc.in, tc.env)
			if err != nil {
				t.Fatal(err)
			}
			if !reflect.DeepEqual(got, tc.want) {
				t.Fatalf("\nwant: %q\ngot:  %q", tc.want, got)
			}
		})
	}
}
07070100000049000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001100000000sh-3.12.0/syntax0707010000004A000081A4000000000000000000000001686AE5C0000004C2000000000000000000000000000000000000001F00000000sh-3.12.0/syntax/bench_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"io"
	"strings"
	"testing"
)

func BenchmarkParse(b *testing.B) {
	b.ReportAllocs()
	src := "" +
		strings.Repeat("\n\n\t\t        \n", 10) +
		"# " + strings.Repeat("foo bar ", 10) + "\n" +
		strings.Repeat("longlit_", 10) + "\n" +
		"'" + strings.Repeat("foo bar ", 10) + "'\n" +
		`"` + strings.Repeat("foo bar ", 10) + `"` + "\n" +
		strings.Repeat("aa bb cc dd; ", 6) +
		"a() { (b); { c; }; }; $(d; `e`)\n" +
		"foo=bar; a=b; c=d$foo${bar}e $simple ${complex:-default}\n" +
		"if a; then while b; do for c in d e; do f; done; done; fi\n" +
		"a | b && c || d | e && g || f\n" +
		"foo >a <b <<<c 2>&1 <<EOF\n" +
		strings.Repeat("somewhat long heredoc line\n", 10) +
		"EOF" +
		""
	p := NewParser(KeepComments(true))
	in := strings.NewReader(src)
	for i := 0; i < b.N; i++ {
		if _, err := p.Parse(in, ""); err != nil {
			b.Fatal(err)
		}
		in.Reset(src)
	}
}

func BenchmarkPrint(b *testing.B) {
	b.ReportAllocs()
	prog := parsePath(b, canonicalPath)
	printer := NewPrinter()
	for i := 0; i < b.N; i++ {
		if err := printer.Print(io.Discard, prog); err != nil {
			b.Fatal(err)
		}
	}
}
0707010000004B000081A4000000000000000000000001686AE5C000000F90000000000000000000000000000000000000001B00000000sh-3.12.0/syntax/braces.go// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"strconv"
	"strings"
)

var (
	litLeftBrace  = &Lit{Value: "{"}
	litComma      = &Lit{Value: ","}
	litDots       = &Lit{Value: ".."}
	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.
//
// 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".
//
// 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 {
	if !strings.Contains(word.Lit(), "{") {
		// In the common case where a word has no braces, skip any allocs.
		return false
	}
	top := &Word{}
	acc := top
	var cur *BraceExp
	open := []*BraceExp{}

	pop := func() *BraceExp {
		old := cur
		open = open[:len(open)-1]
		if len(open) == 0 {
			cur = nil
			acc = top
		} else {
			cur = open[len(open)-1]
			acc = cur.Elems[len(cur.Elems)-1]
		}
		return old
	}
	addLit := func(lit *Lit) {
		acc.Parts = append(acc.Parts, lit)
	}

	for _, wp := range word.Parts {
		lit, ok := wp.(*Lit)
		if !ok {
			acc.Parts = append(acc.Parts, wp)
			continue
		}
		last := 0
		for j := 0; j < len(lit.Value); j++ {
			addlitidx := func() {
				if last == j {
					return // empty lit
				}
				l2 := *lit
				l2.Value = l2.Value[last:j]
				addLit(&l2)
			}
			switch lit.Value[j] {
			case '{':
				addlitidx()
				acc = &Word{}
				cur = &BraceExp{Elems: []*Word{acc}}
				open = append(open, cur)
			case ',':
				if cur == nil {
					continue
				}
				addlitidx()
				acc = &Word{}
				cur.Elems = append(cur.Elems, acc)
			case '.':
				if cur == nil {
					continue
				}
				if j+1 >= len(lit.Value) || lit.Value[j+1] != '.' {
					continue
				}
				addlitidx()
				cur.Sequence = true
				acc = &Word{}
				cur.Elems = append(cur.Elems, acc)
				j++
			case '}':
				if cur == nil {
					continue
				}
				addlitidx()
				br := pop()
				if len(br.Elems) == 1 {
					// return {x} to a non-brace
					addLit(litLeftBrace)
					acc.Parts = append(acc.Parts, br.Elems[0].Parts...)
					addLit(litRightBrace)
					break
				}
				if !br.Sequence {
					acc.Parts = append(acc.Parts, br)
					break
				}
				var chars [2]bool
				broken := false
				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') ||
							('A' <= val[0] && val[0] <= 'Z')) {
						chars[i] = true
					} else {
						broken = true
					}
				}
				if len(br.Elems) == 3 {
					// increment must be a number
					val := br.Elems[2].Lit()
					if _, err := strconv.Atoi(val); err != nil {
						broken = true
					}
				}
				// are start and end both chars or
				// non-chars?
				if chars[0] != chars[1] {
					broken = true
				}
				if !broken {
					acc.Parts = append(acc.Parts, br)
					break
				}
				// return broken {x..y[..incr]} to a non-brace
				addLit(litLeftBrace)
				for i, elem := range br.Elems {
					if i > 0 {
						addLit(litDots)
					}
					acc.Parts = append(acc.Parts, elem.Parts...)
				}
				addLit(litRightBrace)
			default:
				continue
			}
			last = j + 1
		}
		if last == 0 {
			addLit(lit)
		} else {
			left := *lit
			left.Value = left.Value[last:]
			addLit(&left)
		}
	}
	// open braces that were never closed fall back to non-braces
	for acc != top {
		br := pop()
		addLit(litLeftBrace)
		for i, elem := range br.Elems {
			if i > 0 {
				if br.Sequence {
					addLit(litDots)
				} else {
					addLit(litComma)
				}
			}
			acc.Parts = append(acc.Parts, elem.Parts...)
		}
	}
	*word = *top
	return true
}
0707010000004C000081A4000000000000000000000001686AE5C00000013D000000000000000000000000000000000000001E00000000sh-3.12.0/syntax/canonical.sh#!/bin/bash

# separate comment

! foo bar >a &

foo() { bar; }

{
	var1="some long value" # var1 comment
	var2=short             # var2 comment
}

if foo; then bar; fi

for foo in a b c; do
	bar
done

case $foo in
a) A ;;
b)
	B
	;;
esac

foo | bar
foo &&
	$(bar) &&
	(more)

foo 2>&1
foo <<-EOF
	bar
EOF

$((3 + 4))
0707010000004D000081A4000000000000000000000001686AE5C0000000E2000000000000000000000000000000000000001800000000sh-3.12.0/syntax/doc.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package syntax implements parsing and formatting of shell programs.
// It supports POSIX Shell, Bash, and mksh.
package syntax
0707010000004E000081A4000000000000000000000001686AE5C000001379000000000000000000000000000000000000002100000000sh-3.12.0/syntax/example_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax_test

import (
	"fmt"
	"os"
	"strings"

	"mvdan.cc/sh/v3/syntax"
)

func Example() {
	r := strings.NewReader("{ foo; bar; }")
	f, err := syntax.NewParser().Parse(r, "")
	if err != nil {
		return
	}
	syntax.NewPrinter().Print(os.Stdout, f)
	// Output:
	// {
	//	foo
	//	bar
	// }
}

func ExampleWord() {
	r := strings.NewReader("echo foo${bar}'baz'")
	f, err := syntax.NewParser().Parse(r, "")
	if err != nil {
		return
	}

	printer := syntax.NewPrinter()
	args := f.Stmts[0].Cmd.(*syntax.CallExpr).Args
	for i, word := range args {
		fmt.Printf("Word number %d:\n", i)
		for _, part := range word.Parts {
			fmt.Printf("%-20T - ", part)
			printer.Print(os.Stdout, part)
			fmt.Println()
		}
		fmt.Println()
	}

	// Output:
	// Word number 0:
	// *syntax.Lit          - echo
	//
	// Word number 1:
	// *syntax.Lit          - foo
	// *syntax.ParamExp     - ${bar}
	// *syntax.SglQuoted    - 'baz'
}

func ExampleCommand() {
	r := strings.NewReader("echo foo; if x; then y; fi; foo | bar")
	f, err := syntax.NewParser().Parse(r, "")
	if err != nil {
		return
	}

	printer := syntax.NewPrinter()
	for i, stmt := range f.Stmts {
		fmt.Printf("Cmd %d: %-20T - ", i, stmt.Cmd)
		printer.Print(os.Stdout, stmt.Cmd)
		fmt.Println()
	}

	// Output:
	// Cmd 0: *syntax.CallExpr     - echo foo
	// Cmd 1: *syntax.IfClause     - if x; then y; fi
	// Cmd 2: *syntax.BinaryCmd    - foo | bar
}

func ExampleNewParser_options() {
	src := "for ((i = 0; i < 5; i++)); do echo $i >f; done"

	// LangBash is the default
	r := strings.NewReader(src)
	f, err := syntax.NewParser().Parse(r, "")
	fmt.Println(err)

	// Parser errors with LangPOSIX
	r = strings.NewReader(src)
	_, err = syntax.NewParser(syntax.Variant(syntax.LangPOSIX)).Parse(r, "")
	fmt.Println(err)

	syntax.NewPrinter().Print(os.Stdout, f)
	syntax.NewPrinter(syntax.SpaceRedirects(true)).Print(os.Stdout, f)

	// Output:
	// <nil>
	// 1:5: c-style fors are a bash feature; tried parsing as posix
	// for ((i = 0; i < 5; i++)); do echo $i >f; done
	// for ((i = 0; i < 5; i++)); do echo $i > f; done
}

// Keep in sync with FuzzQuote.

func ExampleQuote() {
	for _, s := range []string{
		"foo",
		"bar $baz",
		`"won't"`,
		"~/home",
		"#1304",
		"name=value",
		"for",
		"glob-*",
		"invalid-\xe2'",
		"nonprint-\x0b\x1b",
	} {
		// We assume Bash syntax here.
		// For general shell syntax quoting, use syntax.LangPOSIX.
		quoted, err := syntax.Quote(s, syntax.LangBash)
		if err != nil {
			fmt.Printf("%q cannot be quoted: %v\n", s, err)
		} else {
			fmt.Printf("Quote(%17q): %s\n", s, quoted)
		}
	}
	// Output:
	// Quote(            "foo"): foo
	// Quote(       "bar $baz"): 'bar $baz'
	// Quote(      "\"won't\""): "\"won't\""
	// Quote(         "~/home"): '~/home'
	// Quote(          "#1304"): '#1304'
	// Quote(     "name=value"): 'name=value'
	// Quote(            "for"): 'for'
	// Quote(         "glob-*"): 'glob-*'
	// Quote(  "invalid-\xe2'"): $'invalid-\xe2\''
	// Quote("nonprint-\v\x1b"): $'nonprint-\v\x1b'
}

func ExampleWalk() {
	in := strings.NewReader(`echo $foo "and $bar"`)
	f, err := syntax.NewParser().Parse(in, "")
	if err != nil {
		return
	}
	syntax.Walk(f, func(node syntax.Node) bool {
		switch node := node.(type) {
		case *syntax.ParamExp:
			node.Param.Value = strings.ToUpper(node.Param.Value)
		}
		return true
	})
	syntax.NewPrinter().Print(os.Stdout, f)
	// Output: echo $FOO "and $BAR"
}

func ExampleDebugPrint() {
	in := strings.NewReader(`echo 'foo'`)
	f, err := syntax.NewParser().Parse(in, "")
	if err != nil {
		return
	}
	syntax.DebugPrint(os.Stdout, f)
	// Output:
	// *syntax.File {
	// .  Name: ""
	// .  Stmts: []*syntax.Stmt (len = 1) {
	// .  .  0: *syntax.Stmt {
	// .  .  .  Comments: []syntax.Comment (len = 0) {}
	// .  .  .  Cmd: *syntax.CallExpr {
	// .  .  .  .  Assigns: []*syntax.Assign (len = 0) {}
	// .  .  .  .  Args: []*syntax.Word (len = 2) {
	// .  .  .  .  .  0: *syntax.Word {
	// .  .  .  .  .  .  Parts: []syntax.WordPart (len = 1) {
	// .  .  .  .  .  .  .  0: *syntax.Lit {
	// .  .  .  .  .  .  .  .  ValuePos: 1:1
	// .  .  .  .  .  .  .  .  ValueEnd: 1:5
	// .  .  .  .  .  .  .  .  Value: "echo"
	// .  .  .  .  .  .  .  }
	// .  .  .  .  .  .  }
	// .  .  .  .  .  }
	// .  .  .  .  .  1: *syntax.Word {
	// .  .  .  .  .  .  Parts: []syntax.WordPart (len = 1) {
	// .  .  .  .  .  .  .  0: *syntax.SglQuoted {
	// .  .  .  .  .  .  .  .  Left: 1:6
	// .  .  .  .  .  .  .  .  Right: 1:10
	// .  .  .  .  .  .  .  .  Dollar: false
	// .  .  .  .  .  .  .  .  Value: "foo"
	// .  .  .  .  .  .  .  }
	// .  .  .  .  .  .  }
	// .  .  .  .  .  }
	// .  .  .  .  }
	// .  .  .  }
	// .  .  .  Position: 1:1
	// .  .  .  Semicolon: 0:0
	// .  .  .  Negated: false
	// .  .  .  Background: false
	// .  .  .  Coprocess: false
	// .  .  .  Redirs: []*syntax.Redirect (len = 0) {}
	// .  .  }
	// .  }
	// .  Last: []syntax.Comment (len = 0) {}
	// }
}
0707010000004F000081A4000000000000000000000001686AE5C000017D79000000000000000000000000000000000000002300000000sh-3.12.0/syntax/filetests_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"reflect"
	"strings"
	"testing"
)

func prepareTest(c *testCase) {
	c.common = fullProg(c.common)
	c.bash = fullProg(c.bash)
	c.posix = fullProg(c.posix)
	c.mksh = fullProg(c.mksh)
	c.bsmk = fullProg(c.bsmk) // bash AND mksh
	c.bats = fullProg(c.bats)
	if f, ok := c.common.(*File); ok && f != nil {
		c.All = append(c.All, f)
		c.Bash = f
		c.Posix = f
		c.MirBSDKorn = f
		c.Bats = f
	}
	if f, ok := c.bash.(*File); ok && f != nil {
		c.All = append(c.All, f)
		c.Bash = f
		c.Bats = f
	}
	if f, ok := c.posix.(*File); ok && f != nil {
		c.All = append(c.All, f)
		c.Posix = f
	}
	if f, ok := c.mksh.(*File); ok && f != nil {
		c.All = append(c.All, f)
		c.MirBSDKorn = f
	}
	if f, ok := c.bsmk.(*File); ok && f != nil {
		c.All = append(c.All, f)
		c.Bash = f
		c.MirBSDKorn = f
	}
	if f, ok := c.bats.(*File); ok && f != nil {
		c.All = append(c.All, f)
		c.Bats = f
	}
}

func init() {
	for i := range fileTests {
		prepareTest(&fileTests[i])
	}
	for i := range fileTestsNoPrint {
		prepareTest(&fileTestsNoPrint[i])
	}
	for i := range fileTestsKeepComments {
		prepareTest(&fileTestsKeepComments[i])
	}
}

func lit(s string) *Lit         { return &Lit{Value: s} }
func word(ps ...WordPart) *Word { return &Word{Parts: ps} }
func litWord(s string) *Word    { return word(lit(s)) }
func litWords(strs ...string) []*Word {
	l := make([]*Word, 0, len(strs))
	for _, s := range strs {
		l = append(l, litWord(s))
	}
	return l
}

func call(words ...*Word) *CallExpr    { return &CallExpr{Args: words} }
func litCall(strs ...string) *CallExpr { return call(litWords(strs...)...) }

func stmt(cmd Command) *Stmt { return &Stmt{Cmd: cmd} }
func stmts(cmds ...Command) []*Stmt {
	l := make([]*Stmt, len(cmds))
	for i, cmd := range cmds {
		l[i] = stmt(cmd)
	}
	return l
}

func litStmt(strs ...string) *Stmt { return stmt(litCall(strs...)) }
func litStmts(strs ...string) []*Stmt {
	l := make([]*Stmt, len(strs))
	for i, s := range strs {
		l[i] = litStmt(s)
	}
	return l
}

func sglQuoted(s string) *SglQuoted        { return &SglQuoted{Value: s} }
func sglDQuoted(s string) *SglQuoted       { return &SglQuoted{Dollar: true, Value: s} }
func dblQuoted(ps ...WordPart) *DblQuoted  { return &DblQuoted{Parts: ps} }
func dblDQuoted(ps ...WordPart) *DblQuoted { return &DblQuoted{Dollar: true, Parts: ps} }
func block(sts ...*Stmt) *Block            { return &Block{Stmts: sts} }
func subshell(sts ...*Stmt) *Subshell      { return &Subshell{Stmts: sts} }
func arithmExp(e ArithmExpr) *ArithmExp    { return &ArithmExp{X: e} }
func arithmExpBr(e ArithmExpr) *ArithmExp  { return &ArithmExp{Bracket: true, X: e} }
func arithmCmd(e ArithmExpr) *ArithmCmd    { return &ArithmCmd{X: e} }
func parenArit(e ArithmExpr) *ParenArithm  { return &ParenArithm{X: e} }
func parenTest(e TestExpr) *ParenTest      { return &ParenTest{X: e} }

func cmdSubst(sts ...*Stmt) *CmdSubst { return &CmdSubst{Stmts: sts} }
func litParamExp(s string) *ParamExp {
	return &ParamExp{Short: true, Param: lit(s)}
}

func letClause(exps ...ArithmExpr) *LetClause {
	return &LetClause{Exprs: exps}
}

func arrValues(words ...*Word) *ArrayExpr {
	ae := &ArrayExpr{}
	for _, w := range words {
		ae.Elems = append(ae.Elems, &ArrayElem{Value: w})
	}
	return ae
}

type testCase struct {
	Strs        []string
	common      any
	bash, posix any
	bsmk, mksh  any
	bats        any
	All         []*File
	Bash, Posix *File
	MirBSDKorn  *File
	Bats        *File
}

var fileTests = []testCase{
	{
		Strs:   []string{"", " ", "\t", "\n \n", "\r \r\n"},
		common: &File{},
	},
	{
		Strs:   []string{"", "# foo", "# foo ( bar", "# foo'bar"},
		common: &File{},
	},
	{
		Strs:   []string{"foo", "foo ", " foo", "foo # bar"},
		common: litWord("foo"),
	},
	{
		Strs:   []string{`\`},
		common: litWord(`\`),
	},
	{
		Strs:   []string{`foo\`, "f\\\noo\\"},
		common: litWord(`foo\`),
	},
	{
		Strs:   []string{`foo\a`, "f\\\noo\\a"},
		common: litWord(`foo\a`),
	},
	{
		Strs: []string{
			"foo\nbar",
			"foo; bar;",
			"foo;bar;",
			"\nfoo\nbar\n",
			"foo\r\nbar\r\n",
		},
		common: litStmts("foo", "bar"),
	},
	{
		Strs:   []string{"foo a b", " foo  a  b ", "foo \\\n a b", "foo \\\r\n a b"},
		common: litCall("foo", "a", "b"),
	},
	{
		Strs:   []string{"foobar", "foo\\\nbar", "foo\\\nba\\\nr"},
		common: litWord("foobar"),
	},
	{
		Strs:   []string{"foo", "foo \\\n", "foo \\\r\n"},
		common: litWord("foo"),
	},
	{
		Strs:   []string{"foo'bar'"},
		common: word(lit("foo"), sglQuoted("bar")),
	},
	{
		Strs:   []string{"(foo)", "(foo;)", "(\nfoo\n)"},
		common: subshell(litStmt("foo")),
	},
	{
		Strs:   []string{"(\n\tfoo\n\tbar\n)", "(foo; bar)"},
		common: subshell(litStmt("foo"), litStmt("bar")),
	},
	{
		Strs:   []string{"{ foo; }", "{\nfoo\n}"},
		common: block(litStmt("foo")),
	},
	{
		Strs: []string{
			"{ if a; then b; fi; }",
			"{ if a; then b; fi }",
		},
		common: block(stmt(&IfClause{
			Cond: litStmts("a"),
			Then: litStmts("b"),
		})),
	},
	{
		Strs: []string{
			"if a; then b; fi",
			"if a\nthen\nb\nfi",
			"if a;\nthen\nb\nfi",
			"if a \nthen\nb\nfi",
			"if\x00 a; th\x00en b; \x00fi",
		},
		common: &IfClause{
			Cond: litStmts("a"),
			Then: litStmts("b"),
		},
	},
	{
		Strs: []string{
			"if a; then b; else c; fi",
			"if a\nthen b\nelse\nc\nfi",
		},
		common: &IfClause{
			Cond: litStmts("a"),
			Then: litStmts("b"),
			Else: &IfClause{
				Then: litStmts("c"),
			},
		},
	},
	{
		Strs: []string{
			"if a; then a; elif b; then b; else c; fi",
		},
		common: &IfClause{
			Cond: litStmts("a"),
			Then: litStmts("a"),
			Else: &IfClause{
				Cond: litStmts("b"),
				Then: litStmts("b"),
				Else: &IfClause{
					Then: litStmts("c"),
				},
			},
		},
	},
	{
		Strs: []string{
			"if a; then a; elif b; then b; elif c; then c; else d; fi",
			"if a\nthen a\nelif b\nthen b\nelif c\nthen c\nelse\nd\nfi",
		},
		common: &IfClause{
			Cond: litStmts("a"),
			Then: litStmts("a"),
			Else: &IfClause{
				Cond: litStmts("b"),
				Then: litStmts("b"),
				Else: &IfClause{
					Cond: litStmts("c"),
					Then: litStmts("c"),
					Else: &IfClause{
						Then: litStmts("d"),
					},
				},
			},
		},
	},
	{
		Strs: []string{
			"if\n\ta1\n\ta2 foo\n\ta3 bar\nthen b; fi",
			"if a1; a2 foo; a3 bar; then b; fi",
		},
		common: &IfClause{
			Cond: []*Stmt{
				litStmt("a1"),
				litStmt("a2", "foo"),
				litStmt("a3", "bar"),
			},

			Then: litStmts("b"),
		},
	},
	{
		Strs: []string{`((a == 2))`},
		bsmk: arithmCmd(&BinaryArithm{
			Op: Eql,
			X:  litWord("a"),
			Y:  litWord("2"),
		}),
		posix: subshell(stmt(subshell(litStmt("a", "==", "2")))),
	},
	{
		Strs: []string{"if (($# > 2)); then b; fi"},
		bsmk: &IfClause{
			Cond: stmts(arithmCmd(&BinaryArithm{
				Op: Gtr,
				X:  word(litParamExp("#")),
				Y:  litWord("2"),
			})),
			Then: litStmts("b"),
		},
	},
	{
		Strs: []string{
			"(($(date -u) > DATE))",
			"((`date -u` > DATE))",
		},
		bsmk: arithmCmd(&BinaryArithm{
			Op: Gtr,
			X:  word(cmdSubst(litStmt("date", "-u"))),
			Y:  litWord("DATE"),
		}),
	},
	{
		Strs: []string{": $((0x$foo == 10))"},
		common: call(
			litWord(":"),
			word(arithmExp(&BinaryArithm{
				Op: Eql,
				X:  word(lit("0x"), litParamExp("foo")),
				Y:  litWord("10"),
			})),
		),
	},
	{
		Strs: []string{"((# 1 + 2))", "(( # 1 + 2 ))"},
		mksh: &ArithmCmd{
			X: &BinaryArithm{
				Op: Add,
				X:  litWord("1"),
				Y:  litWord("2"),
			},
			Unsigned: true,
		},
	},
	{
		Strs: []string{"$((# 1 + 2))", "$(( # 1 + 2 ))"},
		mksh: &ArithmExp{
			X: &BinaryArithm{
				Op: Add,
				X:  litWord("1"),
				Y:  litWord("2"),
			},
			Unsigned: true,
		},
	},
	{
		Strs: []string{"((3#20))"},
		bsmk: arithmCmd(litWord("3#20")),
	},
	{
		Strs: []string{
			"while a; do b; done",
			"wh\\\nile a; do b; done",
			"wh\\\r\nile a; do b; done",
			"while a\ndo\nb\ndone",
			"while a;\ndo\nb\ndone",
		},
		common: &WhileClause{
			Cond: litStmts("a"),
			Do:   litStmts("b"),
		},
	},
	{
		Strs: []string{"while { a; }; do b; done", "while { a; } do b; done"},
		common: &WhileClause{
			Cond: stmts(block(litStmt("a"))),
			Do:   litStmts("b"),
		},
	},
	{
		Strs: []string{"while (a); do b; done", "while (a) do b; done"},
		common: &WhileClause{
			Cond: stmts(subshell(litStmt("a"))),
			Do:   litStmts("b"),
		},
	},
	{
		Strs: []string{"while ((1 > 2)); do b; done"},
		bsmk: &WhileClause{
			Cond: stmts(arithmCmd(&BinaryArithm{
				Op: Gtr,
				X:  litWord("1"),
				Y:  litWord("2"),
			})),
			Do: litStmts("b"),
		},
	},
	{
		Strs: []string{"until a; do b; done", "until a\ndo\nb\ndone"},
		common: &WhileClause{
			Until: true,
			Cond:  litStmts("a"),
			Do:    litStmts("b"),
		},
	},
	{
		Strs: []string{
			"for i; do foo; done",
			"for i do foo; done",
			"for i\ndo foo\ndone",
			"for i;\ndo foo\ndone",
			"for i in; do foo; done",
		},
		common: &ForClause{
			Loop: &WordIter{Name: lit("i")},
			Do:   litStmts("foo"),
		},
	},
	{
		Strs: []string{
			"for i in 1 2 3; do echo $i; done",
			"for i in 1 2 3\ndo echo $i\ndone",
			"for i in 1 2 3;\ndo echo $i\ndone",
			"for i in 1 2 3 #foo\ndo echo $i\ndone",
		},
		common: &ForClause{
			Loop: &WordIter{
				Name:  lit("i"),
				Items: litWords("1", "2", "3"),
			},
			Do: stmts(call(
				litWord("echo"),
				word(litParamExp("i")),
			)),
		},
	},
	{
		Strs: []string{
			"for i in \\\n\t1 2 3; do #foo\n\techo $i\ndone",
			"for i #foo\n\tin 1 2 3; do\n\techo $i\ndone",
		},
		common: &ForClause{
			Loop: &WordIter{
				Name:  lit("i"),
				Items: litWords("1", "2", "3"),
			},
			Do: stmts(call(
				litWord("echo"),
				word(litParamExp("i")),
			)),
		},
	},
	{
		Strs: []string{
			"for i; do foo; done",
			"for i; { foo; }",
		},
		bsmk: &ForClause{
			Loop: &WordIter{Name: lit("i")},
			Do:   litStmts("foo"),
		},
	},
	{
		Strs: []string{
			"for i in 1 2 3; do echo $i; done",
			"for i in 1 2 3; { echo $i; }",
		},
		bsmk: &ForClause{
			Loop: &WordIter{
				Name:  lit("i"),
				Items: litWords("1", "2", "3"),
			},
			Do: stmts(call(
				litWord("echo"),
				word(litParamExp("i")),
			)),
		},
	},
	{
		Strs: []string{
			"for ((i = 0; i < 10; i++)); do echo $i; done",
			"for ((i=0;i<10;i++)) do echo $i; done",
			"for (( i = 0 ; i < 10 ; i++ ))\ndo echo $i\ndone",
			"for (( i = 0 ; i < 10 ; i++ ));\ndo echo $i\ndone",
		},
		bash: &ForClause{
			Loop: &CStyleLoop{
				Init: &BinaryArithm{
					Op: Assgn,
					X:  litWord("i"),
					Y:  litWord("0"),
				},
				Cond: &BinaryArithm{
					Op: Lss,
					X:  litWord("i"),
					Y:  litWord("10"),
				},
				Post: &UnaryArithm{
					Op:   Inc,
					Post: true,
					X:    litWord("i"),
				},
			},
			Do: stmts(call(
				litWord("echo"),
				word(litParamExp("i")),
			)),
		},
	},
	{
		Strs: []string{
			"for (( ; ; )); do foo; done",
			"for ((;;)); do foo; done",
		},
		bash: &ForClause{
			Loop: &CStyleLoop{},
			Do:   litStmts("foo"),
		},
	},
	{
		Strs: []string{
			"for ((i = 0; ; )); do foo; done",
			"for ((i = 0;;)); do foo; done",
		},
		bash: &ForClause{
			Loop: &CStyleLoop{
				Init: &BinaryArithm{
					Op: Assgn,
					X:  litWord("i"),
					Y:  litWord("0"),
				},
			},
			Do: litStmts("foo"),
		},
	},
	{
		Strs: []string{
			"select i; do foo; done",
			// TODO: bash won't allow this - bug?
			//"select i in; do foo; done",
		},
		bsmk: &ForClause{
			Select: true,
			Loop:   &WordIter{Name: lit("i")},
			Do:     litStmts("foo"),
		},
	},
	{
		Strs: []string{
			"select i in 1 2 3; do echo $i; done",
			"select i in 1 2 3\ndo echo $i\ndone",
			"select i in 1 2 3 #foo\ndo echo $i\ndone",
		},
		bsmk: &ForClause{
			Select: true,
			Loop: &WordIter{
				Name:  lit("i"),
				Items: litWords("1", "2", "3"),
			},
			Do: stmts(call(
				litWord("echo"),
				word(litParamExp("i")),
			)),
		},
	},
	{
		Strs:  []string{"select foo bar"},
		posix: litStmt("select", "foo", "bar"),
	},
	{
		Strs: []string{`' ' "foo bar"`},
		common: call(
			word(sglQuoted(" ")),
			word(dblQuoted(lit("foo bar"))),
		),
	},
	{
		Strs:   []string{`"foo \" bar"`},
		common: word(dblQuoted(lit(`foo \" bar`))),
	},
	{
		Strs: []string{"\">foo\" \"\nbar\""},
		common: call(
			word(dblQuoted(lit(">foo"))),
			word(dblQuoted(lit("\nbar"))),
		),
	},
	{
		Strs:   []string{`foo \" bar`},
		common: litCall(`foo`, `\"`, `bar`),
	},
	{
		Strs:   []string{`'"'`},
		common: sglQuoted(`"`),
	},
	{
		Strs:   []string{"'`'"},
		common: sglQuoted("`"),
	},
	{
		Strs:   []string{`"'"`},
		common: dblQuoted(lit("'")),
	},
	{
		Strs:   []string{`""`},
		common: dblQuoted(),
	},
	{
		Strs:   []string{"=a s{s s=s"},
		common: litCall("=a", "s{s", "s=s"),
	},
	{
		Strs: []string{"foo && bar", "foo&&bar", "foo &&\nbar"},
		common: &BinaryCmd{
			Op: AndStmt,
			X:  litStmt("foo"),
			Y:  litStmt("bar"),
		},
	},
	{
		Strs: []string{"foo &&\n\tbar"},
		common: &BinaryCmd{
			Op: AndStmt,
			X:  litStmt("foo"),
			Y:  litStmt("bar"),
		},
	},
	{
		Strs: []string{"foo || bar", "foo||bar", "foo ||\nbar"},
		common: &BinaryCmd{
			Op: OrStmt,
			X:  litStmt("foo"),
			Y:  litStmt("bar"),
		},
	},
	{
		Strs: []string{"if a; then b; fi || while a; do b; done"},
		common: &BinaryCmd{
			Op: OrStmt,
			X: stmt(&IfClause{
				Cond: litStmts("a"),
				Then: litStmts("b"),
			}),
			Y: stmt(&WhileClause{
				Cond: litStmts("a"),
				Do:   litStmts("b"),
			}),
		},
	},
	{
		Strs: []string{"foo && bar1 || bar2"},
		common: &BinaryCmd{
			Op: OrStmt,
			X: stmt(&BinaryCmd{
				Op: AndStmt,
				X:  litStmt("foo"),
				Y:  litStmt("bar1"),
			}),
			Y: litStmt("bar2"),
		},
	},
	{
		Strs: []string{"a || b || c || d"},
		common: &BinaryCmd{
			Op: OrStmt,
			X: stmt(&BinaryCmd{
				Op: OrStmt,
				X: stmt(&BinaryCmd{
					Op: OrStmt,
					X:  litStmt("a"),
					Y:  litStmt("b"),
				}),
				Y: litStmt("c"),
			}),
			Y: litStmt("d"),
		},
	},
	{
		Strs: []string{"foo | bar", "foo|bar", "foo |\n#etc\nbar"},
		common: &BinaryCmd{
			Op: Pipe,
			X:  litStmt("foo"),
			Y:  litStmt("bar"),
		},
	},
	{
		Strs: []string{"foo | bar | extra"},
		common: &BinaryCmd{
			Op: Pipe,
			X: stmt(&BinaryCmd{
				Op: Pipe,
				X:  litStmt("foo"),
				Y:  litStmt("bar"),
			}),
			Y: litStmt("extra"),
		},
	},
	{
		Strs: []string{"foo | a=b bar"},
		common: &BinaryCmd{
			Op: Pipe,
			X:  litStmt("foo"),
			Y: stmt(&CallExpr{
				Assigns: []*Assign{{
					Name:  lit("a"),
					Value: litWord("b"),
				}},
				Args: litWords("bar"),
			}),
		},
	},
	{
		Strs: []string{"foo |&"},
		mksh: &Stmt{Cmd: litCall("foo"), Coprocess: true},
	},
	{
		Strs: []string{"foo \\\n\t|&"},
		mksh: &Stmt{Cmd: litCall("foo"), Coprocess: true},
	},
	{
		Strs: []string{"foo |& bar", "foo|&bar"},
		bash: &BinaryCmd{
			Op: PipeAll,
			X:  litStmt("foo"),
			Y:  litStmt("bar"),
		},
		mksh: []*Stmt{
			{Cmd: litCall("foo"), Coprocess: true},
			litStmt("bar"),
		},
	},
	{
		Strs: []string{
			"foo() {\n\ta\n\tb\n}",
			"foo() { a; b; }",
			"foo ( ) {\na\nb\n}",
			"foo()\n{\na\nb\n}",
		},
		common: &FuncDecl{
			Parens: true,
			Name:   lit("foo"),
			Body:   stmt(block(litStmt("a"), litStmt("b"))),
		},
	},
	{
		Strs: []string{"foo() { a; }\nbar", "foo() {\na\n}; bar"},
		common: []Command{
			&FuncDecl{
				Parens: true,
				Name:   lit("foo"),
				Body:   stmt(block(litStmt("a"))),
			},
			litCall("bar"),
		},
	},
	{
		Strs: []string{"foO_123() { a; }"},
		common: &FuncDecl{
			Parens: true,
			Name:   lit("foO_123"),
			Body:   stmt(block(litStmt("a"))),
		},
	},
	{
		Strs: []string{"-foo_.,+-bar() { a; }"},
		bsmk: &FuncDecl{
			Parens: true,
			Name:   lit("-foo_.,+-bar"),
			Body:   stmt(block(litStmt("a"))),
		},
	},
	{
		Strs: []string{
			"function foo() {\n\ta\n\tb\n}",
			"function foo() { a; b; }",
		},
		bsmk: &FuncDecl{
			RsrvWord: true,
			Parens:   true,
			Name:     lit("foo"),
			Body:     stmt(block(litStmt("a"), litStmt("b"))),
		},
	},
	{
		Strs: []string{
			"function foo {\n\ta\n\tb\n}",
			"function foo { a; b; }",
		},
		bsmk: &FuncDecl{
			RsrvWord: true,
			Name:     lit("foo"),
			Body:     stmt(block(litStmt("a"), litStmt("b"))),
		},
	},
	{
		Strs: []string{"function foo() (a)"},
		bash: &FuncDecl{
			RsrvWord: true,
			Parens:   true,
			Name:     lit("foo"),
			Body:     stmt(subshell(litStmt("a"))),
		},
	},
	{
		Strs: []string{"a=b foo=$bar foo=start$bar"},
		common: &CallExpr{
			Assigns: []*Assign{
				{Name: lit("a"), Value: litWord("b")},
				{Name: lit("foo"), Value: word(litParamExp("bar"))},
				{Name: lit("foo"), Value: word(
					lit("start"),
					litParamExp("bar"),
				)},
			},
		},
	},
	{
		Strs: []string{"a=\"\nbar\""},
		common: &CallExpr{
			Assigns: []*Assign{{
				Name:  lit("a"),
				Value: word(dblQuoted(lit("\nbar"))),
			}},
		},
	},
	{
		Strs: []string{"A_3a= foo"},
		common: &CallExpr{
			Assigns: []*Assign{{Name: lit("A_3a")}},
			Args:    litWords("foo"),
		},
	},
	{
		Strs: []string{"a=b=c"},
		common: &CallExpr{
			Assigns: []*Assign{{Name: lit("a"), Value: litWord("b=c")}},
		},
	},
	{
		Strs:   []string{"à=b foo"},
		common: litStmt("à=b", "foo"),
	},
	{
		Strs: []string{
			"foo >a >>b <c",
			"foo > a >> b < c",
			">a >>b <c foo",
		},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("a")},
				{Op: AppOut, Word: litWord("b")},
				{Op: RdrIn, Word: litWord("c")},
			},
		},
	},
	{
		Strs: []string{
			"foo bar >a",
			"foo >a bar",
		},
		common: &Stmt{
			Cmd: litCall("foo", "bar"),
			Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("a")},
			},
		},
	},
	{
		Strs: []string{`>a >\b`},
		common: &Stmt{
			Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("a")},
				{Op: RdrOut, Word: litWord(`\b`)},
			},
		},
	},
	{
		Strs: []string{">a\n>b", ">a; >b"},
		common: []*Stmt{
			{Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("a")},
			}},
			{Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("b")},
			}},
		},
	},
	{
		Strs: []string{"foo1\nfoo2 >r2", "foo1; >r2 foo2"},
		common: []*Stmt{
			litStmt("foo1"),
			{
				Cmd: litCall("foo2"),
				Redirs: []*Redirect{
					{Op: RdrOut, Word: litWord("r2")},
				},
			},
		},
	},
	{
		Strs: []string{"foo >bar$(etc)", "foo >b\\\nar`etc`"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{
				{Op: RdrOut, Word: word(
					lit("bar"),
					cmdSubst(litStmt("etc")),
				)},
			},
		},
	},
	{
		Strs: []string{
			"a=b c=d foo >x <y",
			"a=b c=d >x <y foo",
			">x a=b c=d <y foo",
			">x <y a=b c=d foo",
			"a=b >x c=d foo <y",
		},
		common: &Stmt{
			Cmd: &CallExpr{
				Assigns: []*Assign{
					{Name: lit("a"), Value: litWord("b")},
					{Name: lit("c"), Value: litWord("d")},
				},
				Args: litWords("foo"),
			},
			Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("x")},
				{Op: RdrIn, Word: litWord("y")},
			},
		},
	},
	{
		Strs: []string{
			"foo <<EOF\nbar\nEOF",
			"foo <<EOF \nbar\nEOF",
			"foo <<EOF\t\nbar\nEOF",
			"foo <<EOF\r\nbar\r\nEOF\r\n",
		},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<EOF\n\nbar\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("\nbar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<EOF\nbar\n\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("bar\n\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<EOF\n1\n2\n3\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("1\n2\n3\n"),
			}},
		},
	},
	{
		Strs: []string{"a <<EOF\nfoo$bar\nEOF"},
		common: &Stmt{
			Cmd: litCall("a"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: word(
					lit("foo"),
					litParamExp("bar"),
					lit("\n"),
				),
			}},
		},
	},
	{
		Strs: []string{"a <<EOF\n\"$bar\"\nEOF"},
		common: &Stmt{
			Cmd: litCall("a"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: word(
					lit(`"`),
					litParamExp("bar"),
					lit("\"\n"),
				),
			}},
		},
	},
	{
		Strs: []string{"a <<EOF\n$''$bar\nEOF"},
		bash: &Stmt{
			Cmd: litCall("a"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: word(
					lit("$"),
					lit("''"),
					litParamExp("bar"),
					lit("\n"),
				),
			}},
		},
	},
	{
		Strs: []string{
			"a <<EOF\n$(b)\nc\nEOF",
			"a <<EOF\n`b`\nc\nEOF",
		},
		common: &Stmt{
			Cmd: litCall("a"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: word(
					cmdSubst(litStmt("b")),
					lit("\nc\n"),
				),
			}},
		},
	},
	{
		Strs: []string{
			"a <<EOF\nfoo$(bar)baz\nEOF",
			"a <<EOF\nfoo`bar`baz\nEOF",
		},
		common: &Stmt{
			Cmd: litCall("a"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: word(
					lit("foo"),
					cmdSubst(litStmt("bar")),
					lit("baz\n"),
				),
			}},
		},
	},
	{
		Strs: []string{"a <<EOF\n\\${\nEOF"},
		common: &Stmt{
			Cmd: litCall("a"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("\\${\n"),
			}},
		},
	},
	{
		Strs: []string{
			"{\n\tfoo <<EOF\nbar\nEOF\n}",
			"{ foo <<EOF\nbar\nEOF\n}",
		},
		common: block(&Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("bar\n"),
			}},
		}),
	},
	{
		Strs: []string{
			"$(\n\tfoo <<EOF\nbar\nEOF\n)",
			"$(foo <<EOF\nbar\nEOF\n)",
			"`\nfoo <<EOF\nbar\nEOF\n`",
			"`foo <<EOF\nbar\nEOF`",
		},
		common: cmdSubst(&Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("bar\n"),
			}},
		}),
	},
	{
		Strs: []string{
			"foo <<EOF\nbar\nEOF$(oops)\nEOF",
			"foo <<EOF\nbar\nEOF`oops`\nEOF",
		},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: word(
					lit("bar\nEOF"),
					cmdSubst(litStmt("oops")),
					lit("\n"),
				),
			}},
		},
	},
	{
		Strs: []string{
			"foo <<EOF\nbar\nNOTEOF$(oops)\nEOF",
			"foo <<EOF\nbar\nNOTEOF`oops`\nEOF",
		},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: word(
					lit("bar\nNOTEOF"),
					cmdSubst(litStmt("oops")),
					lit("\n"),
				),
			}},
		},
	},
	{
		Strs: []string{
			"$(\n\tfoo <<'EOF'\nbar\nEOF\n)",
			"$(foo <<'EOF'\nbar\nEOF\n)",
			"`\nfoo <<'EOF'\nbar\nEOF\n`",
			"`foo <<'EOF'\nbar\nEOF`",
		},
		common: cmdSubst(&Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: word(sglQuoted("EOF")),
				Hdoc: litWord("bar\n"),
			}},
		}),
	},
	{
		Strs: []string{"foo <<'EOF'\nbar\nEOF`oops`\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: word(sglQuoted("EOF")),
				Hdoc: litWord("bar\nEOF`oops`\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<'EOF'\nbar\nNOTEOF`oops`\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: word(sglQuoted("EOF")),
				Hdoc: litWord("bar\nNOTEOF`oops`\n"),
			}},
		},
	},
	{
		Strs: []string{"$(<foo)", "`<foo`"},
		common: cmdSubst(&Stmt{
			Redirs: []*Redirect{{
				Op:   RdrIn,
				Word: litWord("foo"),
			}},
		}),
	},
	{
		Strs: []string{"foo <<EOF >f\nbar\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{
				{
					Op:   Hdoc,
					Word: litWord("EOF"),
					Hdoc: litWord("bar\n"),
				},
				{Op: RdrOut, Word: litWord("f")},
			},
		},
	},
	{
		Strs: []string{"foo <<EOF && {\nbar\nEOF\n\tetc\n}"},
		common: &BinaryCmd{
			Op: AndStmt,
			X: &Stmt{
				Cmd: litCall("foo"),
				Redirs: []*Redirect{{
					Op:   Hdoc,
					Word: litWord("EOF"),
					Hdoc: litWord("bar\n"),
				}},
			},
			Y: stmt(block(litStmt("etc"))),
		},
	},
	{
		Strs: []string{
			"$(\n\tfoo\n) <<EOF\nbar\nEOF",
			"<<EOF $(\n\tfoo\n)\nbar\nEOF",
		},
		// note that dash won't accept the second one
		bsmk: &Stmt{
			Cmd: call(word(cmdSubst(litStmt("foo")))),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{
			"$(\n\tfoo\n) <<EOF\nbar\nEOF",
			"`\n\tfoo\n` <<EOF\nbar\nEOF",
			"<<EOF `\n\tfoo\n`\nbar\nEOF",
		},
		common: &Stmt{
			Cmd: call(word(cmdSubst(litStmt("foo")))),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{
			"$((foo)) <<EOF\nbar\nEOF",
			"<<EOF $((\n\tfoo\n))\nbar\nEOF",
		},
		common: &Stmt{
			Cmd: call(word(arithmExp(litWord("foo")))),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{"if true; then\n\tfoo <<-EOF\n\t\tbar\n\tEOF\nfi"},
		common: &IfClause{
			Cond: litStmts("true"),
			Then: []*Stmt{{
				Cmd: litCall("foo"),
				Redirs: []*Redirect{{
					Op:   DashHdoc,
					Word: litWord("EOF"),
					Hdoc: litWord("\t\tbar\n\t"),
				}},
			}},
		},
	},
	{
		Strs: []string{"if true; then\n\tfoo <<-EOF\n\tEOF\nfi"},
		common: &IfClause{
			Cond: litStmts("true"),
			Then: []*Stmt{{
				Cmd: litCall("foo"),
				Redirs: []*Redirect{{
					Op:   DashHdoc,
					Word: litWord("EOF"),
					Hdoc: litWord("\t"),
				}},
			}},
		},
	},
	{
		Strs: []string{"foo <<EOF\nEOF_body\nEOF\nfoo2"},
		common: []*Stmt{
			{
				Cmd: litCall("foo"),
				Redirs: []*Redirect{{
					Op:   Hdoc,
					Word: litWord("EOF"),
					Hdoc: litWord("EOF_body\n"),
				}},
			},
			litStmt("foo2"),
		},
	},
	{
		Strs: []string{"foo <<FOOBAR\nbar\nFOOBAR"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("FOOBAR"),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<\"EOF\"\nbar\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: word(dblQuoted(lit("EOF"))),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<'EOF'\nEOF_body\nEOF\nfoo2"},
		common: []*Stmt{
			{
				Cmd: litCall("foo"),
				Redirs: []*Redirect{{
					Op:   Hdoc,
					Word: word(sglQuoted("EOF")),
					Hdoc: litWord("EOF_body\n"),
				}},
			},
			litStmt("foo2"),
		},
	},
	{
		Strs: []string{"foo <<'EOF'\n${\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: word(sglQuoted("EOF")),
				Hdoc: litWord("${\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<'EOF'\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: word(sglQuoted("EOF")),
			}},
		},
	},
	{
		Strs: []string{"foo <<\"EOF\"2\nbar\nEOF2"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: word(dblQuoted(lit("EOF")), lit("2")),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<\\EOF\nbar\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("\\EOF"),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<EOF\nbar\\\nbaz\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: word(lit("bar"), lit("baz\n")),
			}},
		},
	},
	{
		Strs: []string{
			"foo <<'EOF'\nbar\\\nEOF",
			"foo <<'EOF'\nbar\\\r\nEOF",
			"foo <<'EOF'\nbar\\\r\nEOF\r\n",
		},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: word(sglQuoted("EOF")),
				Hdoc: litWord("bar\\\n"),
			}},
		},
	},
	{
		Strs: []string{
			"foo <<-EOF\n\tbar\nEOF",
			"foo <<-EOF\r\n\tbar\r\nEOF\r\n",
		},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   DashHdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("\tbar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<EOF\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
			}},
		},
	},
	{
		Strs: []string{"foo <<-EOF\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   DashHdoc,
				Word: litWord("EOF"),
			}},
		},
	},
	{
		Strs: []string{"foo <<-EOF\n\tbar\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   DashHdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("\tbar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo <<-'EOF'\n\tbar\nEOF"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   DashHdoc,
				Word: word(sglQuoted("EOF")),
				Hdoc: litWord("\tbar\n"),
			}},
		},
	},
	{
		Strs: []string{
			"f1 <<EOF1\nh1\nEOF1\nf2 <<EOF2\nh2\nEOF2",
			"f1 <<EOF1; f2 <<EOF2\nh1\nEOF1\nh2\nEOF2",
		},
		common: []*Stmt{
			{
				Cmd: litCall("f1"),
				Redirs: []*Redirect{{
					Op:   Hdoc,
					Word: litWord("EOF1"),
					Hdoc: litWord("h1\n"),
				}},
			},
			{
				Cmd: litCall("f2"),
				Redirs: []*Redirect{{
					Op:   Hdoc,
					Word: litWord("EOF2"),
					Hdoc: litWord("h2\n"),
				}},
			},
		},
	},
	{
		Strs: []string{
			"a <<EOF\nfoo\nEOF\nb\nb\nb\nb\nb\nb\nb\nb\nb",
			"a <<EOF;b;b;b;b;b;b;b;b;b\nfoo\nEOF",
		},
		common: []*Stmt{
			{
				Cmd: litCall("a"),
				Redirs: []*Redirect{{
					Op:   Hdoc,
					Word: litWord("EOF"),
					Hdoc: litWord("foo\n"),
				}},
			},
			litStmt("b"), litStmt("b"), litStmt("b"),
			litStmt("b"), litStmt("b"), litStmt("b"),
			litStmt("b"), litStmt("b"), litStmt("b"),
		},
	},
	{
		Strs: []string{
			"foo \"\narg\" <<EOF\nbar\nEOF",
			"foo <<EOF \"\narg\"\nbar\nEOF",
		},
		common: &Stmt{
			Cmd: call(
				litWord("foo"),
				word(dblQuoted(lit("\narg"))),
			),
			Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("bar\n"),
			}},
		},
	},
	{
		Strs: []string{"foo >&2 <&0 2>file 345>file <>f2"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{
				{Op: DplOut, Word: litWord("2")},
				{Op: DplIn, Word: litWord("0")},
				{Op: RdrOut, N: lit("2"), Word: litWord("file")},
				{Op: RdrOut, N: lit("345"), Word: litWord("file")},
				{Op: RdrInOut, Word: litWord("f2")},
			},
		},
	},
	{
		Strs: []string{
			"foo bar >file",
			"foo bar>file",
		},
		common: &Stmt{
			Cmd: litCall("foo", "bar"),
			Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("file")},
			},
		},
	},
	{
		Strs: []string{"foo &>a &>>b"},
		bsmk: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{
				{Op: RdrAll, Word: litWord("a")},
				{Op: AppAll, Word: litWord("b")},
			},
		},
	},
	{
		Strs: []string{"foo 2>file bar", "2>file foo bar"},
		common: &Stmt{
			Cmd: litCall("foo", "bar"),
			Redirs: []*Redirect{
				{Op: RdrOut, N: lit("2"), Word: litWord("file")},
			},
		},
	},
	{
		Strs: []string{"a >f1\nb >f2", "a >f1; b >f2"},
		common: []*Stmt{
			{
				Cmd:    litCall("a"),
				Redirs: []*Redirect{{Op: RdrOut, Word: litWord("f1")}},
			},
			{
				Cmd:    litCall("b"),
				Redirs: []*Redirect{{Op: RdrOut, Word: litWord("f2")}},
			},
		},
	},
	{
		Strs: []string{"foo >|bar"},
		common: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{
				{Op: ClbOut, Word: litWord("bar")},
			},
		},
	},
	{
		Strs: []string{
			"foo <<<input",
			"foo <<< input",
		},
		bsmk: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   WordHdoc,
				Word: litWord("input"),
			}},
		},
	},
	{
		Strs: []string{
			`foo <<<"spaced input"`,
			`foo <<< "spaced input"`,
		},
		bsmk: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op:   WordHdoc,
				Word: word(dblQuoted(lit("spaced input"))),
			}},
		},
	},
	{
		Strs: []string{"foo >(foo)"},
		bash: call(
			litWord("foo"),
			word(&ProcSubst{
				Op:    CmdOut,
				Stmts: litStmts("foo"),
			}),
		),
	},
	{
		Strs: []string{"foo < <(foo)"},
		bash: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{{
				Op: RdrIn,
				Word: word(&ProcSubst{
					Op:    CmdIn,
					Stmts: litStmts("foo"),
				}),
			}},
		},
	},
	{
		Strs: []string{"a<(b) c>(d)"},
		bash: call(
			word(lit("a"), &ProcSubst{
				Op:    CmdIn,
				Stmts: litStmts("b"),
			}),
			word(lit("c"), &ProcSubst{
				Op:    CmdOut,
				Stmts: litStmts("d"),
			}),
		),
	},
	{
		Strs: []string{"foo {fd}<f"},
		bash: &Stmt{
			Cmd: litCall("foo"),
			Redirs: []*Redirect{
				{Op: RdrIn, N: lit("{fd}"), Word: litWord("f")},
			},
		},
	},
	{
		Strs: []string{"! foo"},
		common: &Stmt{
			Negated: true,
			Cmd:     litCall("foo"),
		},
	},
	{
		Strs: []string{"foo &\nbar", "foo & bar", "foo&bar"},
		common: []*Stmt{
			{Cmd: litCall("foo"), Background: true},
			litStmt("bar"),
		},
	},
	{
		Strs: []string{
			"! if foo; then bar; fi >/dev/null &",
			"! if foo; then bar; fi>/dev/null&",
		},
		common: &Stmt{
			Negated: true,
			Cmd: &IfClause{
				Cond: litStmts("foo"),
				Then: litStmts("bar"),
			},
			Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("/dev/null")},
			},
			Background: true,
		},
	},
	{
		Strs: []string{"! foo && bar"},
		common: &BinaryCmd{
			Op: AndStmt,
			X: &Stmt{
				Cmd:     litCall("foo"),
				Negated: true,
			},
			Y: litStmt("bar"),
		},
	},
	{
		Strs: []string{"! foo | bar"},
		common: &Stmt{
			Cmd: &BinaryCmd{
				Op: Pipe,
				X:  litStmt("foo"),
				Y:  litStmt("bar"),
			},
			Negated: true,
		},
	},
	{
		Strs: []string{
			"a && b &\nc",
			"a && b & c",
		},
		common: []*Stmt{
			{
				Cmd: &BinaryCmd{
					Op: AndStmt,
					X:  litStmt("a"),
					Y:  litStmt("b"),
				},
				Background: true,
			},
			litStmt("c"),
		},
	},
	{
		Strs: []string{"a | b &"},
		common: &Stmt{
			Cmd: &BinaryCmd{
				Op: Pipe,
				X:  litStmt("a"),
				Y:  litStmt("b"),
			},
			Background: true,
		},
	},
	{
		Strs:   []string{"foo#bar"},
		common: litWord("foo#bar"),
	},
	{
		Strs: []string{"$foo#bar foo#$bar"},
		common: call(
			word(litParamExp("foo"), lit("#bar")),
			word(lit("foo#"), litParamExp("bar")),
		),
	},
	{
		Strs:   []string{"{ echo } }; }"},
		common: block(litStmt("echo", "}", "}")),
	},
	{
		Strs: []string{"$({ echo; })"},
		common: cmdSubst(stmt(
			block(litStmt("echo")),
		)),
	},
	{
		Strs: []string{
			"$( (echo foo bar))",
			"$( (echo foo bar) )",
			"`(echo foo bar)`",
		},
		common: cmdSubst(stmt(
			subshell(litStmt("echo", "foo", "bar")),
		)),
	},
	{
		Strs:   []string{"$()"},
		common: cmdSubst(),
	},
	{
		Strs: []string{"()"},
		mksh: subshell(), // not common, as dash/bash wrongly error
	},
	{
		Strs: []string{
			"$(\n\t(a)\n\tb\n)",
			"$( (a); b)",
			"`(a); b`",
		},
		common: cmdSubst(
			stmt(subshell(litStmt("a"))),
			litStmt("b"),
		),
	},
	{
		Strs: []string{
			`$(echo \')`,
			"`" + `echo \\'` + "`",
		},
		common: cmdSubst(litStmt("echo", `\'`)),
	},
	{
		Strs: []string{
			`$(echo \\)`,
			"`" + `echo \\\\` + "`",
		},
		common: cmdSubst(litStmt("echo", `\\`)),
	},
	{
		Strs: []string{
			`$(echo '\' 'a\b' "\\" "a\a")`,
			"`" + `echo '\' 'a\\b' "\\\\" "a\a"` + "`",
		},
		common: cmdSubst(stmt(call(
			litWord("echo"),
			word(sglQuoted(`\`)),
			word(sglQuoted(`a\b`)),
			word(dblQuoted(lit(`\\`))),
			word(dblQuoted(lit(`a\a`))),
		))),
	},
	{
		Strs: []string{
			"$(echo $(x))",
			"`echo \\`x\\``",
		},
		common: cmdSubst(stmt(call(
			litWord("echo"),
			word(cmdSubst(litStmt("x"))),
		))),
	},
	{
		Strs: []string{
			"$($(foo bar))",
			"`\\`foo bar\\``",
		},
		common: cmdSubst(stmt(call(
			word(cmdSubst(litStmt("foo", "bar"))),
		))),
	},
	{
		Strs: []string{"$( (a) | b)"},
		common: cmdSubst(
			stmt(&BinaryCmd{
				Op: Pipe,
				X:  stmt(subshell(litStmt("a"))),
				Y:  litStmt("b"),
			}),
		),
	},
	{
		Strs: []string{`"$( (foo))"`},
		common: dblQuoted(cmdSubst(stmt(
			subshell(litStmt("foo")),
		))),
	},
	{
		Strs:   []string{"\"foo\\\nbar\""},
		common: dblQuoted(lit("foo"), lit("bar")),
	},
	{
		Strs:   []string{"'foo\\\nbar'", "'foo\\\r\nbar'"},
		common: sglQuoted("foo\\\nbar"),
	},
	{
		Strs: []string{"$({ echo; })", "`{ echo; }`"},
		common: cmdSubst(stmt(
			block(litStmt("echo")),
		)),
	},
	{
		Strs:   []string{`{foo}`},
		common: litWord(`{foo}`),
	},
	{
		Strs:   []string{`{"foo"`},
		common: word(lit("{"), dblQuoted(lit("foo"))),
	},
	{
		Strs:   []string{`foo"bar"`, "fo\\\no\"bar\"", "fo\\\r\no\"bar\""},
		common: word(lit("foo"), dblQuoted(lit("bar"))),
	},
	{
		Strs:   []string{`!foo`},
		common: litWord(`!foo`),
	},
	{
		Strs:   []string{"$(foo bar)", "`foo bar`"},
		common: cmdSubst(litStmt("foo", "bar")),
	},
	{
		Strs: []string{"$(foo | bar)", "`foo | bar`"},
		common: cmdSubst(
			stmt(&BinaryCmd{
				Op: Pipe,
				X:  litStmt("foo"),
				Y:  litStmt("bar"),
			}),
		),
	},
	{
		Strs: []string{"$(foo | >f)", "`foo | >f`"},
		common: cmdSubst(
			stmt(&BinaryCmd{
				Op: Pipe,
				X:  litStmt("foo"),
				Y: &Stmt{Redirs: []*Redirect{{
					Op:   RdrOut,
					Word: litWord("f"),
				}}},
			}),
		),
	},
	{
		Strs: []string{"$(foo $(b1 b2))"},
		common: cmdSubst(stmt(call(
			litWord("foo"),
			word(cmdSubst(litStmt("b1", "b2"))),
		))),
	},
	{
		Strs: []string{`"$(foo "bar")"`},
		common: dblQuoted(cmdSubst(stmt(call(
			litWord("foo"),
			word(dblQuoted(lit("bar"))),
		)))),
	},
	{
		Strs:   []string{"$(foo)", "`fo\\\no`"},
		common: cmdSubst(litStmt("foo")),
	},
	{
		Strs: []string{"foo $(bar)", "foo `bar`"},
		common: call(
			litWord("foo"),
			word(cmdSubst(litStmt("bar"))),
		),
	},
	{
		Strs: []string{"$(foo 'bar')", "`foo 'bar'`"},
		common: cmdSubst(stmt(call(
			litWord("foo"),
			word(sglQuoted("bar")),
		))),
	},
	{
		Strs: []string{`$(foo "bar")`, "`foo \"bar\"`"},
		common: cmdSubst(stmt(call(
			litWord("foo"),
			word(dblQuoted(lit("bar"))),
		))),
	},
	{
		Strs: []string{`"$(foo "bar")"`, "\"`foo \"bar\"`\""},
		common: dblQuoted(cmdSubst(stmt(call(
			litWord("foo"),
			word(dblQuoted(lit("bar"))),
		)))),
	},
	{
		Strs: []string{"${ foo;}", "${\n\tfoo; }", "${\tfoo;}"},
		mksh: &CmdSubst{
			Stmts:    litStmts("foo"),
			TempFile: true,
		},
	},
	{
		Strs: []string{"${\n\tfoo\n\tbar\n}", "${ foo; bar;}"},
		mksh: &CmdSubst{
			Stmts:    litStmts("foo", "bar"),
			TempFile: true,
		},
	},
	{
		Strs: []string{"${|foo;}", "${| foo; }"},
		mksh: &CmdSubst{
			Stmts:    litStmts("foo"),
			ReplyVar: true,
		},
	},
	{
		Strs: []string{"${|\n\tfoo\n\tbar\n}", "${|foo; bar;}"},
		mksh: &CmdSubst{
			Stmts:    litStmts("foo", "bar"),
			ReplyVar: true,
		},
	},
	{
		Strs:   []string{`"$foo"`},
		common: dblQuoted(litParamExp("foo")),
	},
	{
		Strs:   []string{`"#foo"`},
		common: dblQuoted(lit("#foo")),
	},
	{
		Strs: []string{`$@a $*a $#a $$a $?a $!a $-a $0a $30a $_a`},
		common: call(
			word(litParamExp("@"), lit("a")),
			word(litParamExp("*"), lit("a")),
			word(litParamExp("#"), lit("a")),
			word(litParamExp("$"), lit("a")),
			word(litParamExp("?"), lit("a")),
			word(litParamExp("!"), lit("a")),
			word(litParamExp("-"), lit("a")),
			word(litParamExp("0"), lit("a")),
			word(litParamExp("3"), lit("0a")),
			word(litParamExp("_a")),
		),
	},
	{
		Strs:   []string{`$`, `$ #`},
		common: litWord("$"),
	},
	{
		Strs: []string{`${@} ${*} ${#} ${$} ${?} ${!} ${0} ${29} ${-}`},
		common: call(
			word(&ParamExp{Param: lit("@")}),
			word(&ParamExp{Param: lit("*")}),
			word(&ParamExp{Param: lit("#")}),
			word(&ParamExp{Param: lit("$")}),
			word(&ParamExp{Param: lit("?")}),
			word(&ParamExp{Param: lit("!")}),
			word(&ParamExp{Param: lit("0")}),
			word(&ParamExp{Param: lit("29")}),
			word(&ParamExp{Param: lit("-")}),
		),
	},
	{
		Strs: []string{`${#$} ${#@} ${#*} ${##}`},
		common: call(
			word(&ParamExp{Length: true, Param: lit("$")}),
			word(&ParamExp{Length: true, Param: lit("@")}),
			word(&ParamExp{Length: true, Param: lit("*")}),
			word(&ParamExp{Length: true, Param: lit("#")}),
		),
	},
	{
		Strs:   []string{`${foo}`},
		common: &ParamExp{Param: lit("foo")},
	},
	{
		Strs: []string{`${foo}"bar"`},
		common: word(
			&ParamExp{Param: lit("foo")},
			dblQuoted(lit("bar")),
		),
	},
	{
		Strs: []string{`$a/b $a-b $a:b $a}b $a]b $a.b $a,b $a*b $a_b $a2b`},
		common: call(
			word(litParamExp("a"), lit("/b")),
			word(litParamExp("a"), lit("-b")),
			word(litParamExp("a"), lit(":b")),
			word(litParamExp("a"), lit("}b")),
			word(litParamExp("a"), lit("]b")),
			word(litParamExp("a"), lit(".b")),
			word(litParamExp("a"), lit(",b")),
			word(litParamExp("a"), lit("*b")),
			word(litParamExp("a_b")),
			word(litParamExp("a2b")),
		),
	},
	{
		Strs: []string{`$aàb $àb $,b`},
		common: call(
			word(litParamExp("a"), lit("àb")),
			word(lit("$"), lit("àb")),
			word(lit("$"), lit(",b")),
		),
	},
	{
		Strs:   []string{"$à", "$\\\nà", "$\\\r\nà"},
		common: word(lit("$"), lit("à")),
	},
	{
		Strs: []string{"$foobar", "$foo\\\nbar"},
		common: call(
			word(litParamExp("foobar")),
		),
	},
	{
		Strs: []string{"$foo\\bar"},
		common: call(
			word(litParamExp("foo"), lit("\\bar")),
		),
	},
	{
		Strs: []string{`echo -e "$foo\nbar"`},
		common: call(
			litWord("echo"), litWord("-e"),
			word(dblQuoted(
				litParamExp("foo"), lit(`\nbar`),
			)),
		),
	},
	{
		Strs: []string{`${foo-bar}`},
		common: &ParamExp{
			Param: lit("foo"),
			Exp: &Expansion{
				Op:   DefaultUnset,
				Word: litWord("bar"),
			},
		},
	},
	{
		Strs: []string{`${foo+}"bar"`},
		common: word(
			&ParamExp{
				Param: lit("foo"),
				Exp:   &Expansion{Op: AlternateUnset},
			},
			dblQuoted(lit("bar")),
		),
	},
	{
		Strs: []string{`${foo:=<"bar"}`},
		common: &ParamExp{
			Param: lit("foo"),
			Exp: &Expansion{
				Op:   AssignUnsetOrNull,
				Word: word(lit("<"), dblQuoted(lit("bar"))),
			},
		},
	},
	{
		Strs: []string{
			"${foo:=b${c}$(d)}",
			"${foo:=b${c}`d`}",
		},
		common: &ParamExp{
			Param: lit("foo"),
			Exp: &Expansion{
				Op: AssignUnsetOrNull,
				Word: word(
					lit("b"),
					&ParamExp{Param: lit("c")},
					cmdSubst(litStmt("d")),
				),
			},
		},
	},
	{
		Strs: []string{`${foo?"${bar}"}`},
		common: &ParamExp{
			Param: lit("foo"),
			Exp: &Expansion{
				Op: ErrorUnset,
				Word: word(dblQuoted(
					&ParamExp{Param: lit("bar")},
				)),
			},
		},
	},
	{
		Strs: []string{`${foo:?bar1 bar2}`},
		common: &ParamExp{
			Param: lit("foo"),
			Exp: &Expansion{
				Op:   ErrorUnsetOrNull,
				Word: litWord("bar1 bar2"),
			},
		},
	},
	{
		Strs: []string{`${a:+b}${a:-b}${a=b}`},
		common: word(
			&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   AlternateUnsetOrNull,
					Word: litWord("b"),
				},
			},
			&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   DefaultUnsetOrNull,
					Word: litWord("b"),
				},
			},
			&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   AssignUnset,
					Word: litWord("b"),
				},
			},
		),
	},
	{
		Strs: []string{`${3:-'$x'}`},
		common: &ParamExp{
			Param: lit("3"),
			Exp: &Expansion{
				Op:   DefaultUnsetOrNull,
				Word: word(sglQuoted("$x")),
			},
		},
	},
	{
		Strs: []string{`${@:-$x}`},
		common: &ParamExp{
			Param: lit("@"),
			Exp: &Expansion{
				Op:   DefaultUnsetOrNull,
				Word: word(litParamExp("x")),
			},
		},
	},
	{
		Strs: []string{`${var#*'="'}`},
		common: &ParamExp{
			Param: lit("var"),
			Exp: &Expansion{
				Op:   RemSmallPrefix,
				Word: word(lit("*"), sglQuoted(`="`)),
			},
		},
	},
	{
		Strs: []string{`${var/'a'/b'c'd}`},
		bsmk: &ParamExp{
			Param: lit("var"),
			Repl: &Replace{
				Orig: word(sglQuoted("a")),
				With: word(lit("b"), sglQuoted("c"), lit("d")),
			},
		},
	},
	{
		Strs: []string{`${foo%bar}${foo%%bar*}`},
		common: word(
			&ParamExp{
				Param: lit("foo"),
				Exp: &Expansion{
					Op:   RemSmallSuffix,
					Word: litWord("bar"),
				},
			},
			&ParamExp{
				Param: lit("foo"),
				Exp: &Expansion{
					Op:   RemLargeSuffix,
					Word: litWord("bar*"),
				},
			},
		),
	},
	{
		Strs: []string{`${3#bar}${-##bar*}`},
		common: word(
			&ParamExp{
				Param: lit("3"),
				Exp: &Expansion{
					Op:   RemSmallPrefix,
					Word: litWord("bar"),
				},
			},
			&ParamExp{
				Param: lit("-"),
				Exp: &Expansion{
					Op:   RemLargePrefix,
					Word: litWord("bar*"),
				},
			},
		),
	},
	{
		Strs: []string{`${foo%?}`},
		common: &ParamExp{
			Param: lit("foo"),
			Exp: &Expansion{
				Op:   RemSmallSuffix,
				Word: litWord("?"),
			},
		},
	},
	{
		Strs: []string{
			`${foo[1]}`,
			`${foo[ 1 ]}`,
		},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Index: litWord("1"),
		},
	},
	{
		Strs: []string{`${foo[-1]}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Index: &UnaryArithm{
				Op: Minus,
				X:  litWord("1"),
			},
		},
	},
	{
		Strs: []string{`${foo[@]}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Index: litWord("@"),
		},
	},
	{
		Strs: []string{`${foo[*]-etc}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Index: litWord("*"),
			Exp: &Expansion{
				Op:   DefaultUnset,
				Word: litWord("etc"),
			},
		},
	},
	{
		Strs: []string{`${foo[bar]}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Index: litWord("bar"),
		},
	},
	{
		Strs: []string{`${foo[$bar]}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Index: word(litParamExp("bar")),
		},
	},
	{
		Strs: []string{`${foo[${bar}]}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Index: word(&ParamExp{Param: lit("bar")}),
		},
	},
	{
		Strs: []string{`${foo:1}`, `${foo: 1 }`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Slice: &Slice{Offset: litWord("1")},
		},
	},
	{
		Strs: []string{`${foo:1:2}`, `${foo: 1 : 2 }`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Slice: &Slice{
				Offset: litWord("1"),
				Length: litWord("2"),
			},
		},
	},
	{
		Strs: []string{`${foo:a:b}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Slice: &Slice{
				Offset: litWord("a"),
				Length: litWord("b"),
			},
		},
	},
	{
		Strs: []string{`${foo:1:-2}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Slice: &Slice{
				Offset: litWord("1"),
				Length: &UnaryArithm{Op: Minus, X: litWord("2")},
			},
		},
	},
	{
		Strs: []string{`${foo::+3}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Slice: &Slice{
				Length: &UnaryArithm{Op: Plus, X: litWord("3")},
			},
		},
	},
	{
		Strs: []string{`${foo: -1}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Slice: &Slice{
				Offset: &UnaryArithm{Op: Minus, X: litWord("1")},
			},
		},
	},
	{
		Strs: []string{`${foo: +2+3}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Slice: &Slice{
				Offset: &BinaryArithm{
					Op: Add,
					X:  &UnaryArithm{Op: Plus, X: litWord("2")},
					Y:  litWord("3"),
				},
			},
		},
	},
	{
		Strs: []string{`${foo:a?1:2:3}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Slice: &Slice{
				Offset: &BinaryArithm{
					Op: TernQuest,
					X:  litWord("a"),
					Y: &BinaryArithm{
						Op: TernColon,
						X:  litWord("1"),
						Y:  litWord("2"),
					},
				},
				Length: litWord("3"),
			},
		},
	},
	{
		Strs: []string{`${foo/a/b}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl:  &Replace{Orig: litWord("a"), With: litWord("b")},
		},
	},
	{
		Strs: []string{"${foo/ /\t}"},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl:  &Replace{Orig: litWord(" "), With: litWord("\t")},
		},
	},
	{
		Strs: []string{`${foo/[/]-}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl:  &Replace{Orig: litWord("["), With: litWord("]-")},
		},
	},
	{
		Strs: []string{`${foo/bar/b/a/r}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl: &Replace{
				Orig: litWord("bar"),
				With: litWord("b/a/r"),
			},
		},
	},
	{
		Strs: []string{`${foo/$a/$'\''}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl: &Replace{
				Orig: word(litParamExp("a")),
				With: word(sglDQuoted(`\'`)),
			},
		},
	},
	{
		Strs: []string{`${foo//b1/b2}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl: &Replace{
				All:  true,
				Orig: litWord("b1"),
				With: litWord("b2"),
			},
		},
	},
	{
		Strs: []string{`${foo///}`, `${foo//}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl:  &Replace{All: true},
		},
	},
	{
		Strs: []string{`${foo/-//}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl:  &Replace{Orig: litWord("-"), With: litWord("/")},
		},
	},
	{
		Strs: []string{`${foo//#/}`, `${foo//#}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl:  &Replace{All: true, Orig: litWord("#")},
		},
	},
	{
		Strs: []string{`${foo//[42]/}`},
		bsmk: &ParamExp{
			Param: lit("foo"),
			Repl:  &Replace{All: true, Orig: litWord("[42]")},
		},
	},
	{
		Strs: []string{`${a^b} ${a^^b} ${a,b} ${a,,b}`},
		bash: call(
			word(&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   UpperFirst,
					Word: litWord("b"),
				},
			}),
			word(&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   UpperAll,
					Word: litWord("b"),
				},
			}),
			word(&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   LowerFirst,
					Word: litWord("b"),
				},
			}),
			word(&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   LowerAll,
					Word: litWord("b"),
				},
			}),
		),
	},
	{
		Strs: []string{`${a@E} ${b@a} ${@@Q} ${!ref@P}`},
		bash: call(
			word(&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   OtherParamOps,
					Word: litWord("E"),
				},
			}),
			word(&ParamExp{
				Param: lit("b"),
				Exp: &Expansion{
					Op:   OtherParamOps,
					Word: litWord("a"),
				},
			}),
			word(&ParamExp{
				Param: lit("@"),
				Exp: &Expansion{
					Op:   OtherParamOps,
					Word: litWord("Q"),
				},
			}),
			word(&ParamExp{
				Excl:  true,
				Param: lit("ref"),
				Exp: &Expansion{
					Op:   OtherParamOps,
					Word: litWord("P"),
				},
			}),
		),
	},
	{
		Strs: []string{`${a@K} ${b@k}`},
		bash: call(
			word(&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   OtherParamOps,
					Word: litWord("K"),
				},
			}),
			word(&ParamExp{
				Param: lit("b"),
				Exp: &Expansion{
					Op:   OtherParamOps,
					Word: litWord("k"),
				},
			}),
		),
	},
	{
		Strs: []string{`${a@Q} ${b@#}`},
		mksh: call(
			word(&ParamExp{
				Param: lit("a"),
				Exp: &Expansion{
					Op:   OtherParamOps,
					Word: litWord("Q"),
				},
			}),
			word(&ParamExp{
				Param: lit("b"),
				Exp: &Expansion{
					Op:   OtherParamOps,
					Word: litWord("#"),
				},
			}),
		),
	},
	{
		Strs: []string{`${#foo}`},
		common: &ParamExp{
			Length: true,
			Param:  lit("foo"),
		},
	},
	{
		Strs: []string{`${%foo}`},
		mksh: &ParamExp{
			Width: true,
			Param: lit("foo"),
		},
	},
	{
		Strs: []string{`${!foo} ${!bar[@]}`},
		bsmk: call(
			word(&ParamExp{
				Excl:  true,
				Param: lit("foo"),
			}),
			word(&ParamExp{
				Excl:  true,
				Param: lit("bar"),
				Index: litWord("@"),
			}),
		),
	},
	{
		Strs: []string{`${!foo*} ${!bar@}`},
		bash: call(
			word(&ParamExp{
				Excl:  true,
				Param: lit("foo"),
				Names: NamesPrefix,
			}),
			word(&ParamExp{
				Excl:  true,
				Param: lit("bar"),
				Names: NamesPrefixWords,
			}),
		),
	},
	{
		Strs: []string{`${#?}`},
		common: call(
			word(&ParamExp{Length: true, Param: lit("?")}),
		),
	},
	{
		Strs: []string{`${#-foo} ${#?bar}`},
		common: call(
			word(&ParamExp{
				Param: lit("#"),
				Exp: &Expansion{
					Op:   DefaultUnset,
					Word: litWord("foo"),
				},
			}),
			word(&ParamExp{
				Param: lit("#"),
				Exp: &Expansion{
					Op:   ErrorUnset,
					Word: litWord("bar"),
				},
			}),
		),
	},
	{
		Strs:   []string{`"${foo}"`},
		common: dblQuoted(&ParamExp{Param: lit("foo")}),
	},
	{
		Strs:   []string{`"(foo)"`},
		common: dblQuoted(lit("(foo)")),
	},
	{
		Strs: []string{`"${foo}>"`},
		common: dblQuoted(
			&ParamExp{Param: lit("foo")},
			lit(">"),
		),
	},
	{
		Strs:   []string{`"$(foo)"`, "\"`foo`\""},
		common: dblQuoted(cmdSubst(litStmt("foo"))),
	},
	{
		Strs: []string{
			`"$(foo bar)"`,
			`"$(foo  bar)"`,
			"\"`foo bar`\"",
			"\"`foo  bar`\"",
		},
		common: dblQuoted(cmdSubst(litStmt("foo", "bar"))),
	},
	{
		Strs:   []string{`'${foo}'`},
		common: sglQuoted("${foo}"),
	},
	{
		Strs:   []string{"$((1))"},
		common: arithmExp(litWord("1")),
	},
	{
		Strs: []string{"$((1 + 3))", "$((1+3))"},
		common: arithmExp(&BinaryArithm{
			Op: Add,
			X:  litWord("1"),
			Y:  litWord("3"),
		}),
	},
	{
		Strs: []string{`"$((foo))"`},
		common: dblQuoted(arithmExp(
			litWord("foo"),
		)),
	},
	{
		Strs: []string{`$((a)) b`},
		common: call(
			word(arithmExp(litWord("a"))),
			litWord("b"),
		),
	},
	{
		Strs: []string{`$((arr[0]++))`},
		bsmk: arithmExp(&UnaryArithm{
			Op: Inc, Post: true,
			X: word(&ParamExp{
				Short: true,
				Param: lit("arr"),
				Index: litWord("0"),
			}),
		}),
	},
	{
		Strs: []string{`$((++arr[0]))`},
		bsmk: arithmExp(&UnaryArithm{
			Op: Inc,
			X: word(&ParamExp{
				Short: true,
				Param: lit("arr"),
				Index: litWord("0"),
			}),
		}),
	},
	{
		Strs: []string{`$((${a:-1}))`},
		bsmk: arithmExp(word(&ParamExp{
			Param: lit("a"),
			Exp: &Expansion{
				Op:   DefaultUnsetOrNull,
				Word: litWord("1"),
			},
		})),
	},
	{
		Strs: []string{"$((5 * 2 - 1))", "$((5*2-1))"},
		common: arithmExp(&BinaryArithm{
			Op: Sub,
			X: &BinaryArithm{
				Op: Mul,
				X:  litWord("5"),
				Y:  litWord("2"),
			},
			Y: litWord("1"),
		}),
	},
	{
		Strs: []string{"$((i | 13))"},
		common: arithmExp(&BinaryArithm{
			Op: Or,
			X:  litWord("i"),
			Y:  litWord("13"),
		}),
	},
	{
		Strs: []string{
			"$(((a) + ((b))))",
			"$((\n(a) + \n(\n(b)\n)\n))",
		},
		common: arithmExp(&BinaryArithm{
			Op: Add,
			X:  parenArit(litWord("a")),
			Y:  parenArit(parenArit(litWord("b"))),
		}),
	},
	{
		Strs: []string{
			"$((3 % 7))",
			"$((3\n% 7))",
			"$((3\\\n % 7))",
			"$((3\\\r\n % 7))",
		},
		common: arithmExp(&BinaryArithm{
			Op: Rem,
			X:  litWord("3"),
			Y:  litWord("7"),
		}),
	},
	{
		Strs: []string{`"$((1 / 3))"`},
		common: dblQuoted(arithmExp(&BinaryArithm{
			Op: Quo,
			X:  litWord("1"),
			Y:  litWord("3"),
		})),
	},
	{
		Strs: []string{"$((2 ** 10))"},
		common: arithmExp(&BinaryArithm{
			Op: Pow,
			X:  litWord("2"),
			Y:  litWord("10"),
		}),
	},
	{
		Strs: []string{`$(((1) ^ 3))`},
		common: arithmExp(&BinaryArithm{
			Op: Xor,
			X:  parenArit(litWord("1")),
			Y:  litWord("3"),
		}),
	},
	{
		Strs: []string{`$((1 >> (3 << 2)))`},
		common: arithmExp(&BinaryArithm{
			Op: Shr,
			X:  litWord("1"),
			Y: parenArit(&BinaryArithm{
				Op: Shl,
				X:  litWord("3"),
				Y:  litWord("2"),
			}),
		}),
	},
	{
		Strs: []string{`$((-(1)))`},
		common: arithmExp(&UnaryArithm{
			Op: Minus,
			X:  parenArit(litWord("1")),
		}),
	},
	{
		Strs: []string{`$((i++))`},
		common: arithmExp(&UnaryArithm{
			Op:   Inc,
			Post: true,
			X:    litWord("i"),
		}),
	},
	{
		Strs:   []string{`$((--i))`},
		common: arithmExp(&UnaryArithm{Op: Dec, X: litWord("i")}),
	},
	{
		Strs:   []string{`$((!i))`},
		common: arithmExp(&UnaryArithm{Op: Not, X: litWord("i")}),
	},
	{
		Strs:   []string{`$((~i))`},
		common: arithmExp(&UnaryArithm{Op: BitNegation, X: litWord("i")}),
	},
	{
		Strs: []string{`$((-!+i))`},
		common: arithmExp(&UnaryArithm{
			Op: Minus,
			X: &UnaryArithm{
				Op: Not,
				X:  &UnaryArithm{Op: Plus, X: litWord("i")},
			},
		}),
	},
	{
		Strs: []string{`$((!!i))`},
		common: arithmExp(&UnaryArithm{
			Op: Not,
			X:  &UnaryArithm{Op: Not, X: litWord("i")},
		}),
	},
	{
		Strs: []string{`$((~~i))`},
		common: arithmExp(&UnaryArithm{
			Op: BitNegation,
			X:  &UnaryArithm{Op: BitNegation, X: litWord("i")},
		}),
	},
	{
		Strs: []string{`$((1 < 3))`},
		common: arithmExp(&BinaryArithm{
			Op: Lss,
			X:  litWord("1"),
			Y:  litWord("3"),
		}),
	},
	{
		Strs: []string{`$((i = 2))`, `$((i=2))`},
		common: arithmExp(&BinaryArithm{
			Op: Assgn,
			X:  litWord("i"),
			Y:  litWord("2"),
		}),
	},
	{
		Strs: []string{`((a[i] = 4))`, `((a[i]=4))`},
		bsmk: arithmCmd(&BinaryArithm{
			Op: Assgn,
			X: word(&ParamExp{
				Short: true,
				Param: lit("a"),
				Index: litWord("i"),
			}),
			Y: litWord("4"),
		}),
	},
	{
		Strs: []string{"$((a += 2, b -= 3))"},
		common: arithmExp(&BinaryArithm{
			Op: Comma,
			X: &BinaryArithm{
				Op: AddAssgn,
				X:  litWord("a"),
				Y:  litWord("2"),
			},
			Y: &BinaryArithm{
				Op: SubAssgn,
				X:  litWord("b"),
				Y:  litWord("3"),
			},
		}),
	},
	{
		Strs: []string{"$((a >>= 2, b <<= 3))"},
		common: arithmExp(&BinaryArithm{
			Op: Comma,
			X: &BinaryArithm{
				Op: ShrAssgn,
				X:  litWord("a"),
				Y:  litWord("2"),
			},
			Y: &BinaryArithm{
				Op: ShlAssgn,
				X:  litWord("b"),
				Y:  litWord("3"),
			},
		}),
	},
	{
		Strs: []string{"$((a == b && c > d))"},
		common: arithmExp(&BinaryArithm{
			Op: AndArit,
			X: &BinaryArithm{
				Op: Eql,
				X:  litWord("a"),
				Y:  litWord("b"),
			},
			Y: &BinaryArithm{
				Op: Gtr,
				X:  litWord("c"),
				Y:  litWord("d"),
			},
		}),
	},
	{
		Strs: []string{"$((a != b))"},
		common: arithmExp(&BinaryArithm{
			Op: Neq,
			X:  litWord("a"),
			Y:  litWord("b"),
		}),
	},
	{
		Strs: []string{"$((a &= b))"},
		common: arithmExp(&BinaryArithm{
			Op: AndAssgn,
			X:  litWord("a"),
			Y:  litWord("b"),
		}),
	},
	{
		Strs: []string{"$((a |= b))"},
		common: arithmExp(&BinaryArithm{
			Op: OrAssgn,
			X:  litWord("a"),
			Y:  litWord("b"),
		}),
	},
	{
		Strs: []string{"$((a %= b))"},
		common: arithmExp(&BinaryArithm{
			Op: RemAssgn,
			X:  litWord("a"),
			Y:  litWord("b"),
		}),
	},
	{
		Strs: []string{"$((a /= b))", "$((a/=b))"},
		common: arithmExp(&BinaryArithm{
			Op: QuoAssgn,
			X:  litWord("a"),
			Y:  litWord("b"),
		}),
	},
	{
		Strs: []string{"$((a ^= b))"},
		common: arithmExp(&BinaryArithm{
			Op: XorAssgn,
			X:  litWord("a"),
			Y:  litWord("b"),
		}),
	},
	{
		Strs: []string{"$((i *= 3))"},
		common: arithmExp(&BinaryArithm{
			Op: MulAssgn,
			X:  litWord("i"),
			Y:  litWord("3"),
		}),
	},
	{
		Strs: []string{"$((2 >= 10))"},
		common: arithmExp(&BinaryArithm{
			Op: Geq,
			X:  litWord("2"),
			Y:  litWord("10"),
		}),
	},
	{
		Strs: []string{"$((foo ? b1 : b2))"},
		common: arithmExp(&BinaryArithm{
			Op: TernQuest,
			X:  litWord("foo"),
			Y: &BinaryArithm{
				Op: TernColon,
				X:  litWord("b1"),
				Y:  litWord("b2"),
			},
		}),
	},
	{
		Strs: []string{`$((a <= (1 || 2)))`},
		common: arithmExp(&BinaryArithm{
			Op: Leq,
			X:  litWord("a"),
			Y: parenArit(&BinaryArithm{
				Op: OrArit,
				X:  litWord("1"),
				Y:  litWord("2"),
			}),
		}),
	},
	{
		Strs:   []string{"foo$", "foo$\n"},
		common: word(lit("foo"), lit("$")),
	},
	{
		Strs:   []string{"foo$", "foo$\\\n", "foo$\\\r\n"},
		common: word(lit("foo"), lit("$")),
	},
	{
		Strs:  []string{`$''`},
		bsmk:  sglDQuoted(""),
		posix: word(lit("$"), sglQuoted("")),
	},
	{
		Strs:  []string{`$""`},
		bsmk:  dblDQuoted(),
		posix: word(lit("$"), dblQuoted()),
	},
	{
		Strs:  []string{`$'foo'`},
		bsmk:  sglDQuoted("foo"),
		posix: word(lit("$"), sglQuoted("foo")),
	},
	{
		Strs: []string{`$'f+oo${'`},
		bsmk: sglDQuoted("f+oo${"),
	},
	{
		Strs: []string{"$'foo bar`'"},
		bsmk: sglDQuoted("foo bar`"),
	},
	{
		Strs: []string{"$'a ${b} c'"},
		bsmk: sglDQuoted("a ${b} c"),
	},
	{
		Strs: []string{`$"a ${b} c"`},
		bsmk: dblDQuoted(
			lit("a "),
			&ParamExp{Param: lit("b")},
			lit(" c"),
		),
	},
	{
		Strs:   []string{`"a $b c"`},
		common: dblQuoted(lit("a "), litParamExp("b"), lit(" c")),
	},
	{
		Strs: []string{`$"a $b c"`},
		bsmk: dblDQuoted(
			lit("a "),
			litParamExp("b"),
			lit(" c"),
		),
	},
	{
		Strs: []string{"$'f\\'oo\n'"},
		bsmk: sglDQuoted("f\\'oo\n"),
	},
	{
		Strs:  []string{`$"foo"`},
		bsmk:  dblDQuoted(lit("foo")),
		posix: word(lit("$"), dblQuoted(lit("foo"))),
	},
	{
		Strs: []string{`$"foo$"`},
		bsmk: dblDQuoted(lit("foo"), lit("$")),
	},
	{
		Strs: []string{`$"foo bar"`},
		bsmk: dblDQuoted(lit("foo bar")),
	},
	{
		Strs: []string{`$'f\'oo'`},
		bsmk: sglDQuoted(`f\'oo`),
	},
	{
		Strs: []string{`$"f\"oo"`},
		bsmk: dblDQuoted(lit(`f\"oo`)),
	},
	{
		Strs:   []string{`"foo$"`},
		common: dblQuoted(lit("foo"), lit("$")),
	},
	{
		Strs:   []string{`"foo$$"`},
		common: dblQuoted(lit("foo"), litParamExp("$")),
	},
	{
		Strs:   []string{`"a $\"b\" c"`},
		common: dblQuoted(lit(`a `), lit(`$`), lit(`\"b\" c`)),
	},
	{
		Strs: []string{"$(foo$)", "`foo$`"},
		common: cmdSubst(
			stmt(call(word(lit("foo"), lit("$")))),
		),
	},
	{
		Strs:   []string{"foo$bar"},
		common: word(lit("foo"), litParamExp("bar")),
	},
	{
		Strs:   []string{"foo$(bar)"},
		common: word(lit("foo"), cmdSubst(litStmt("bar"))),
	},
	{
		Strs:   []string{"foo${bar}"},
		common: word(lit("foo"), &ParamExp{Param: lit("bar")}),
	},
	{
		Strs:   []string{"'foo${bar'"},
		common: sglQuoted("foo${bar"),
	},
	{
		Strs: []string{"(foo)\nbar", "(foo); bar"},
		common: []Command{
			subshell(litStmt("foo")),
			litCall("bar"),
		},
	},
	{
		Strs: []string{"foo\n(bar)", "foo; (bar)"},
		common: []Command{
			litCall("foo"),
			subshell(litStmt("bar")),
		},
	},
	{
		Strs: []string{"foo\n(bar)", "foo; (bar)"},
		common: []Command{
			litCall("foo"),
			subshell(litStmt("bar")),
		},
	},
	{
		Strs: []string{
			"case $i in 1) foo ;; 2 | 3*) bar ;; esac",
			"case $i in 1) foo;; 2 | 3*) bar; esac",
			"case $i in (1) foo;; 2 | 3*) bar;; esac",
			"case $i\nin\n#etc\n1)\nfoo\n;;\n2 | 3*)\nbar\n;;\nesac",
		},
		common: &CaseClause{
			Word: word(litParamExp("i")),
			Items: []*CaseItem{
				{
					Op:       Break,
					Patterns: litWords("1"),
					Stmts:    litStmts("foo"),
				},
				{
					Op:       Break,
					Patterns: litWords("2", "3*"),
					Stmts:    litStmts("bar"),
				},
			},
		},
	},
	{
		Strs: []string{"case i in 1) a ;& 2) ;; esac"},
		bsmk: &CaseClause{
			Word: litWord("i"),
			Items: []*CaseItem{
				{
					Op:       Fallthrough,
					Patterns: litWords("1"),
					Stmts:    litStmts("a"),
				},
				{Op: Break, Patterns: litWords("2")},
			},
		},
	},
	{
		Strs: []string{
			"case i in 1) a ;; esac",
			"case i { 1) a ;; }",
			"case i {\n1) a ;;\n}",
		},
		mksh: &CaseClause{
			Word: litWord("i"),
			Items: []*CaseItem{{
				Op:       Break,
				Patterns: litWords("1"),
				Stmts:    litStmts("a"),
			}},
		},
	},
	{
		Strs: []string{"case i in 1) a ;;& 2) b ;; esac"},
		bash: &CaseClause{
			Word: litWord("i"),
			Items: []*CaseItem{
				{
					Op:       Resume,
					Patterns: litWords("1"),
					Stmts:    litStmts("a"),
				},
				{
					Op:       Break,
					Patterns: litWords("2"),
					Stmts:    litStmts("b"),
				},
			},
		},
	},
	{
		Strs: []string{"case i in 1) a ;| 2) b ;; esac"},
		mksh: &CaseClause{
			Word: litWord("i"),
			Items: []*CaseItem{
				{
					Op:       ResumeKorn,
					Patterns: litWords("1"),
					Stmts:    litStmts("a"),
				},
				{
					Op:       Break,
					Patterns: litWords("2"),
					Stmts:    litStmts("b"),
				},
			},
		},
	},
	{
		Strs: []string{"case $i in 1) cat <<EOF ;;\nfoo\nEOF\nesac"},
		common: &CaseClause{
			Word: word(litParamExp("i")),
			Items: []*CaseItem{{
				Op:       Break,
				Patterns: litWords("1"),
				Stmts: []*Stmt{{
					Cmd: litCall("cat"),
					Redirs: []*Redirect{{
						Op:   Hdoc,
						Word: litWord("EOF"),
						Hdoc: litWord("foo\n"),
					}},
				}},
			}},
		},
	},
	{
		Strs: []string{"foo | while read a; do b; done"},
		common: &BinaryCmd{
			Op: Pipe,
			X:  litStmt("foo"),
			Y: stmt(&WhileClause{
				Cond: []*Stmt{litStmt("read", "a")},

				Do: litStmts("b"),
			}),
		},
	},
	{
		Strs: []string{"while read l; do foo || bar; done"},
		common: &WhileClause{
			Cond: []*Stmt{litStmt("read", "l")},
			Do: stmts(&BinaryCmd{
				Op: OrStmt,
				X:  litStmt("foo"),
				Y:  litStmt("bar"),
			}),
		},
	},
	{
		Strs:   []string{"echo if while"},
		common: litCall("echo", "if", "while"),
	},
	{
		Strs:   []string{"${foo}if"},
		common: word(&ParamExp{Param: lit("foo")}, lit("if")),
	},
	{
		Strs:   []string{"$if'|'"},
		common: word(litParamExp("if"), sglQuoted("|")),
	},
	{
		Strs: []string{"if a; then b=; fi", "if a; then b=\nfi"},
		common: &IfClause{
			Cond: litStmts("a"),
			Then: stmts(&CallExpr{
				Assigns: []*Assign{
					{Name: lit("b")},
				},
			}),
		},
	},
	{
		Strs: []string{"if a; then >f; fi", "if a; then >f\nfi"},
		common: &IfClause{
			Cond: litStmts("a"),
			Then: []*Stmt{{
				Redirs: []*Redirect{
					{Op: RdrOut, Word: litWord("f")},
				},
			}},
		},
	},
	{
		Strs: []string{"if a; then (a); fi", "if a; then (a) fi"},
		common: &IfClause{
			Cond: litStmts("a"),
			Then: stmts(subshell(litStmt("a"))),
		},
	},
	{
		Strs: []string{"a=b\nc=d", "a=b; c=d"},
		common: []Command{
			&CallExpr{Assigns: []*Assign{
				{Name: lit("a"), Value: litWord("b")},
			}},
			&CallExpr{Assigns: []*Assign{
				{Name: lit("c"), Value: litWord("d")},
			}},
		},
	},
	{
		Strs: []string{"foo && write | read"},
		common: &BinaryCmd{
			Op: AndStmt,
			X:  litStmt("foo"),
			Y: stmt(&BinaryCmd{
				Op: Pipe,
				X:  litStmt("write"),
				Y:  litStmt("read"),
			}),
		},
	},
	{
		Strs: []string{"write | read && bar"},
		common: &BinaryCmd{
			Op: AndStmt,
			X: stmt(&BinaryCmd{
				Op: Pipe,
				X:  litStmt("write"),
				Y:  litStmt("read"),
			}),
			Y: litStmt("bar"),
		},
	},
	{
		Strs: []string{"foo >f | bar"},
		common: &BinaryCmd{
			Op: Pipe,
			X: &Stmt{
				Cmd: litCall("foo"),
				Redirs: []*Redirect{
					{Op: RdrOut, Word: litWord("f")},
				},
			},
			Y: litStmt("bar"),
		},
	},
	{
		Strs: []string{"(foo) >f | bar"},
		common: &BinaryCmd{
			Op: Pipe,
			X: &Stmt{
				Cmd: subshell(litStmt("foo")),
				Redirs: []*Redirect{
					{Op: RdrOut, Word: litWord("f")},
				},
			},
			Y: litStmt("bar"),
		},
	},
	{
		Strs: []string{"foo | >f"},
		common: &BinaryCmd{
			Op: Pipe,
			X:  litStmt("foo"),
			Y: &Stmt{Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("f")},
			}},
		},
	},
	{
		Strs:  []string{"[[ a ]]"},
		bsmk:  &TestClause{X: litWord("a")},
		posix: litStmt("[[", "a", "]]"),
	},
	{
		Strs: []string{"[[ a ]]\nb"},
		bsmk: stmts(
			&TestClause{X: litWord("a")},
			litCall("b"),
		),
	},
	{
		Strs: []string{"[[ a > b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: TsAfter,
			X:  litWord("a"),
			Y:  litWord("b"),
		}},
	},
	{
		Strs: []string{"[[ 1 -nt 2 ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: TsNewer,
			X:  litWord("1"),
			Y:  litWord("2"),
		}},
	},
	{
		Strs: []string{"[[ 1 -eq 2 ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: TsEql,
			X:  litWord("1"),
			Y:  litWord("2"),
		}},
	},
	{
		Strs: []string{
			"[[ -R a ]]",
			"[[\n-R a\n]]",
		},
		bash: &TestClause{X: &UnaryTest{
			Op: TsRefVar,
			X:  litWord("a"),
		}},
	},
	{
		Strs: []string{"[[ a =~ b ]]", "[[ a =~ b ]];"},
		bash: &TestClause{X: &BinaryTest{
			Op: TsReMatch,
			X:  litWord("a"),
			Y:  litWord("b"),
		}},
	},
	{
		Strs: []string{`[[ a =~ " foo "$bar ]]`},
		bash: &TestClause{X: &BinaryTest{
			Op: TsReMatch,
			X:  litWord("a"),
			Y: word(
				dblQuoted(lit(" foo ")),
				litParamExp("bar"),
			),
		}},
	},
	{
		Strs: []string{`[[ a =~ foo"bar" ]]`},
		bash: &TestClause{X: &BinaryTest{
			Op: TsReMatch,
			X:  litWord("a"),
			Y: word(
				lit("foo"),
				dblQuoted(lit("bar")),
			),
		}},
	},
	{
		Strs: []string{`[[ a =~ [ab](c |d) ]]`},
		bash: &TestClause{X: &BinaryTest{
			Op: TsReMatch,
			X:  litWord("a"),
			Y:  litWord("[ab](c |d)"),
		}},
	},
	{
		Strs: []string{`[[ a =~ ( ]]<>;&) ]]`},
		bash: &TestClause{X: &BinaryTest{
			Op: TsReMatch,
			X:  litWord("a"),
			Y:  litWord("( ]]<>;&)"),
		}},
	},
	{
		Strs: []string{`[[ a =~ ($foo) ]]`},
		bash: &TestClause{X: &BinaryTest{
			Op: TsReMatch,
			X:  litWord("a"),
			Y:  word(lit("("), litParamExp("foo"), lit(")")),
		}},
	},
	{
		Strs: []string{`[[ a =~ b\ c|d ]]`},
		bash: &TestClause{X: &BinaryTest{
			Op: TsReMatch,
			X:  litWord("a"),
			Y:  litWord(`b\ c|d`),
		}},
	},
	{
		Strs: []string{`[[ a == -n ]]`},
		bsmk: &TestClause{X: &BinaryTest{
			Op: TsMatch,
			X:  litWord("a"),
			Y:  litWord("-n"),
		}},
	},
	{
		Strs: []string{`[[ a =~ -n ]]`},
		bash: &TestClause{X: &BinaryTest{
			Op: TsReMatch,
			X:  litWord("a"),
			Y:  litWord("-n"),
		}},
	},
	{
		Strs: []string{"[[ a =~ b$ || c =~ d$ ]]"},
		bash: &TestClause{X: &BinaryTest{
			Op: OrTest,
			X: &BinaryTest{
				Op: TsReMatch,
				X:  litWord("a"),
				Y:  word(lit("b"), lit("$")),
			},
			Y: &BinaryTest{
				Op: TsReMatch,
				X:  litWord("c"),
				Y:  word(lit("d"), lit("$")),
			},
		}},
	},
	{
		Strs: []string{"[[ -n $a ]]"},
		bsmk: &TestClause{
			X: &UnaryTest{Op: TsNempStr, X: word(litParamExp("a"))},
		},
	},
	{
		Strs: []string{"[[ ! $a < 'b' ]]"},
		bsmk: &TestClause{X: &UnaryTest{
			Op: TsNot,
			X: &BinaryTest{
				Op: TsBefore,
				X:  word(litParamExp("a")),
				Y:  word(sglQuoted("b")),
			},
		}},
	},
	{
		Strs: []string{
			"[[ ! -e $a ]]",
			"[[ ! -a $a ]]",
			"[[\n!\n-a $a\n]]",
		},
		bsmk: &TestClause{X: &UnaryTest{
			Op: TsNot,
			X:  &UnaryTest{Op: TsExists, X: word(litParamExp("a"))},
		}},
	},
	{
		Strs: []string{
			"[[ a && b ]]",
			"[[\na &&\nb ]]",
			"[[\n\na &&\n\nb ]]",
		},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  litWord("a"),
			Y:  litWord("b"),
		}},
	},
	{
		Strs: []string{"[[ (a && b) ]]"},
		bsmk: &TestClause{X: parenTest(&BinaryTest{
			Op: AndTest,
			X:  litWord("a"),
			Y:  litWord("b"),
		})},
	},
	{
		Strs: []string{
			"[[ a && (b) ]]",
			"[[ a &&\n(\nb) ]]",
		},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  litWord("a"),
			Y:  parenTest(litWord("b")),
		}},
	},
	{
		Strs: []string{"[[ (a && b) || -f c ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: OrTest,
			X: parenTest(&BinaryTest{
				Op: AndTest,
				X:  litWord("a"),
				Y:  litWord("b"),
			}),
			Y: &UnaryTest{Op: TsRegFile, X: litWord("c")},
		}},
	},
	{
		Strs: []string{
			"[[ -S a && -L b ]]",
			"[[ -S a && -h b ]]",
		},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsSocket, X: litWord("a")},
			Y:  &UnaryTest{Op: TsSmbLink, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -k a && -N b ]]"},
		bash: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsSticky, X: litWord("a")},
			Y:  &UnaryTest{Op: TsModif, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -G a && -O b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsGrpOwn, X: litWord("a")},
			Y:  &UnaryTest{Op: TsUsrOwn, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -d a && -c b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsDirect, X: litWord("a")},
			Y:  &UnaryTest{Op: TsCharSp, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -b a && -p b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsBlckSp, X: litWord("a")},
			Y:  &UnaryTest{Op: TsNmPipe, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -g a && -u b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsGIDSet, X: litWord("a")},
			Y:  &UnaryTest{Op: TsUIDSet, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -r a && -w b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsRead, X: litWord("a")},
			Y:  &UnaryTest{Op: TsWrite, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -x a && -s b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsExec, X: litWord("a")},
			Y:  &UnaryTest{Op: TsNoEmpty, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -t a && -z b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsFdTerm, X: litWord("a")},
			Y:  &UnaryTest{Op: TsEmpStr, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ -o a && -v b ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X:  &UnaryTest{Op: TsOptSet, X: litWord("a")},
			Y:  &UnaryTest{Op: TsVarSet, X: litWord("b")},
		}},
	},
	{
		Strs: []string{"[[ a -ot b && c -ef d ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X: &BinaryTest{
				Op: TsOlder,
				X:  litWord("a"),
				Y:  litWord("b"),
			},
			Y: &BinaryTest{
				Op: TsDevIno,
				X:  litWord("c"),
				Y:  litWord("d"),
			},
		}},
	},
	{
		Strs: []string{"[[ a = b && c != d ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X: &BinaryTest{
				Op: TsMatchShort,
				X:  litWord("a"),
				Y:  litWord("b"),
			},
			Y: &BinaryTest{
				Op: TsNoMatch,
				X:  litWord("c"),
				Y:  litWord("d"),
			},
		}},
	},
	{
		Strs: []string{"[[ a -ne b && c -le d ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X: &BinaryTest{
				Op: TsNeq,
				X:  litWord("a"),
				Y:  litWord("b"),
			},
			Y: &BinaryTest{
				Op: TsLeq,
				X:  litWord("c"),
				Y:  litWord("d"),
			},
		}},
	},
	{
		Strs: []string{"[[ c -ge d ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: TsGeq,
			X:  litWord("c"),
			Y:  litWord("d"),
		}},
	},
	{
		Strs: []string{"[[ a -lt b && c -gt d ]]"},
		bsmk: &TestClause{X: &BinaryTest{
			Op: AndTest,
			X: &BinaryTest{
				Op: TsLss,
				X:  litWord("a"),
				Y:  litWord("b"),
			},
			Y: &BinaryTest{
				Op: TsGtr,
				X:  litWord("c"),
				Y:  litWord("d"),
			},
		}},
	},
	{
		Strs:   []string{"declare -f func"},
		common: litStmt("declare", "-f", "func"),
		bash: &DeclClause{
			Variant: lit("declare"),
			Args: []*Assign{
				{Naked: true, Value: litWord("-f")},
				{Naked: true, Name: lit("func")},
			},
		},
	},
	{
		Strs: []string{"(local bar)"},
		bsmk: subshell(stmt(&DeclClause{
			Variant: lit("local"),
			Args: []*Assign{{
				Naked: true,
				Name:  lit("bar"),
			}},
		})),
		posix: subshell(litStmt("local", "bar")),
	},
	{
		Strs:  []string{"typeset"},
		bsmk:  &DeclClause{Variant: lit("typeset")},
		posix: litStmt("typeset"),
	},
	{
		Strs: []string{"export bar"},
		bsmk: &DeclClause{
			Variant: lit("export"),
			Args: []*Assign{{
				Naked: true,
				Name:  lit("bar"),
			}},
		},
		posix: litStmt("export", "bar"),
	},
	{
		Strs: []string{"readonly -n"},
		bsmk: &DeclClause{
			Variant: lit("readonly"),
			Args:    []*Assign{{Naked: true, Value: litWord("-n")}},
		},
		posix: litStmt("readonly", "-n"),
	},
	{
		Strs: []string{"nameref bar="},
		bsmk: &DeclClause{
			Variant: lit("nameref"),
			Args: []*Assign{{
				Name: lit("bar"),
			}},
		},
		posix: litStmt("nameref", "bar="),
	},
	{
		Strs: []string{"declare -a +n -b$o foo=bar"},
		bash: &DeclClause{
			Variant: lit("declare"),
			Args: []*Assign{
				{Naked: true, Value: litWord("-a")},
				{Naked: true, Value: litWord("+n")},
				{Naked: true, Value: word(lit("-b"), litParamExp("o"))},
				{Name: lit("foo"), Value: litWord("bar")},
			},
		},
	},
	{
		Strs: []string{
			"declare -a foo=(b1 $(b2))",
			"declare -a foo=(b1 `b2`)",
		},
		bash: &DeclClause{
			Variant: lit("declare"),
			Args: []*Assign{
				{Naked: true, Value: litWord("-a")},
				{
					Name: lit("foo"),
					Array: arrValues(
						litWord("b1"),
						word(cmdSubst(litStmt("b2"))),
					),
				},
			},
		},
	},
	{
		Strs: []string{"local -a foo=(b1)"},
		bash: &DeclClause{
			Variant: lit("local"),
			Args: []*Assign{
				{Naked: true, Value: litWord("-a")},
				{
					Name:  lit("foo"),
					Array: arrValues(litWord("b1")),
				},
			},
		},
	},
	{
		Strs: []string{"declare -A foo=([a]=b)"},
		bash: &DeclClause{
			Variant: lit("declare"),
			Args: []*Assign{
				{Naked: true, Value: litWord("-A")},
				{
					Name: lit("foo"),
					Array: &ArrayExpr{Elems: []*ArrayElem{{
						Index: litWord("a"),
						Value: litWord("b"),
					}}},
				},
			},
		},
	},
	{
		Strs: []string{"declare foo[a]="},
		bash: &DeclClause{
			Variant: lit("declare"),
			Args: []*Assign{{
				Name:  lit("foo"),
				Index: litWord("a"),
			}},
		},
	},
	{
		Strs: []string{"declare foo[*]"},
		bash: &DeclClause{
			Variant: lit("declare"),
			Args: []*Assign{{
				Name:  lit("foo"),
				Index: litWord("*"),
				Naked: true,
			}},
		},
	},
	{
		Strs: []string{`declare foo["x y"]`},
		bash: &DeclClause{
			Variant: lit("declare"),
			Args: []*Assign{{
				Name:  lit("foo"),
				Index: word(dblQuoted(lit("x y"))),
				Naked: true,
			}},
		},
	},
	{
		Strs: []string{`declare foo['x y']`},
		bash: &DeclClause{
			Variant: lit("declare"),
			Args: []*Assign{{
				Name:  lit("foo"),
				Index: word(sglQuoted("x y")),
				Naked: true,
			}},
		},
	},
	{
		Strs: []string{"foo=([)"},
		mksh: &CallExpr{Assigns: []*Assign{{
			Name:  lit("foo"),
			Array: arrValues(litWord("[")),
		}}},
	},
	{
		Strs: []string{
			"a && b=(c)\nd",
			"a && b=(c); d",
		},
		bsmk: stmts(
			&BinaryCmd{
				Op: AndStmt,
				X:  litStmt("a"),
				Y: stmt(&CallExpr{Assigns: []*Assign{{
					Name:  lit("b"),
					Array: arrValues(litWord("c")),
				}}}),
			},
			litCall("d"),
		),
	},
	{
		Strs: []string{"declare -f $func >/dev/null"},
		bash: &Stmt{
			Cmd: &DeclClause{
				Variant: lit("declare"),
				Args: []*Assign{
					{Naked: true, Value: litWord("-f")},
					{
						Naked: true,
						Value: word(litParamExp("func")),
					},
				},
			},
			Redirs: []*Redirect{
				{Op: RdrOut, Word: litWord("/dev/null")},
			},
		},
	},
	{
		Strs: []string{"declare a\n{ x; }"},
		bash: stmts(
			&DeclClause{
				Variant: lit("declare"),
				Args: []*Assign{{
					Naked: true,
					Name:  lit("a"),
				}},
			},
			block(litStmt("x")),
		),
	},
	{
		Strs:   []string{"eval a=b foo"},
		common: litStmt("eval", "a=b", "foo"),
	},
	{
		Strs:  []string{"time", "time\n"},
		posix: litStmt("time"),
		bsmk:  &TimeClause{},
	},
	{
		Strs:  []string{"time -p"},
		posix: litStmt("time", "-p"),
		bsmk:  &TimeClause{PosixFormat: true},
	},
	{
		Strs:  []string{"time -a"},
		posix: litStmt("time", "-a"),
		bsmk:  &TimeClause{Stmt: litStmt("-a")},
	},
	{
		Strs:  []string{"time --"},
		posix: litStmt("time", "--"),
		bsmk:  &TimeClause{Stmt: litStmt("--")},
	},
	{
		Strs: []string{"time foo"},
		bsmk: &TimeClause{Stmt: litStmt("foo")},
	},
	{
		Strs: []string{"time { foo; }"},
		bsmk: &TimeClause{Stmt: stmt(block(litStmt("foo")))},
	},
	{
		Strs: []string{"time\nfoo"},
		bsmk: []*Stmt{
			stmt(&TimeClause{}),
			litStmt("foo"),
		},
	},
	{
		Strs:   []string{"coproc foo bar"},
		common: litStmt("coproc", "foo", "bar"),
		bash:   &CoprocClause{Stmt: litStmt("foo", "bar")},
	},
	{
		Strs: []string{"coproc name { foo; }"},
		bash: &CoprocClause{
			Name: litWord("name"),
			Stmt: stmt(block(litStmt("foo"))),
		},
	},
	{
		Strs: []string{"coproc $namevar { foo; }"},
		bash: &CoprocClause{
			Name: word(litParamExp("namevar")),
			Stmt: stmt(block(litStmt("foo"))),
		},
	},
	{
		Strs: []string{"coproc foo", "coproc foo;"},
		bash: &CoprocClause{Stmt: litStmt("foo")},
	},
	{
		Strs: []string{"coproc { foo; }"},
		bash: &CoprocClause{
			Stmt: stmt(block(litStmt("foo"))),
		},
	},
	{
		Strs: []string{"coproc (foo)"},
		bash: &CoprocClause{
			Stmt: stmt(subshell(litStmt("foo"))),
		},
	},
	{
		Strs: []string{"coproc name foo | bar"},
		bash: &CoprocClause{
			Name: litWord("name"),
			Stmt: stmt(&BinaryCmd{
				Op: Pipe,
				X:  litStmt("foo"),
				Y:  litStmt("bar"),
			}),
		},
	},
	{
		Strs: []string{"coproc $()", "coproc ``"},
		bash: &CoprocClause{Stmt: stmt(call(
			word(cmdSubst()),
		))},
	},
	{
		Strs: []string{`let i++`},
		bsmk: letClause(
			&UnaryArithm{Op: Inc, Post: true, X: litWord("i")},
		),
		posix: litStmt("let", "i++"),
	},
	{
		Strs: []string{`let a++ b++ c +d`},
		bsmk: letClause(
			&UnaryArithm{Op: Inc, Post: true, X: litWord("a")},
			&UnaryArithm{Op: Inc, Post: true, X: litWord("b")},
			litWord("c"),
			&UnaryArithm{Op: Plus, X: litWord("d")},
		),
	},
	{
		Strs: []string{`let ++i >/dev/null`},
		bsmk: &Stmt{
			Cmd:    letClause(&UnaryArithm{Op: Inc, X: litWord("i")}),
			Redirs: []*Redirect{{Op: RdrOut, Word: litWord("/dev/null")}},
		},
	},
	{
		Strs: []string{
			`let a=(1 + 2) b=3+4`,
			`let a=(1+2) b=3+4`,
		},
		bash: letClause(
			&BinaryArithm{
				Op: Assgn,
				X:  litWord("a"),
				Y: parenArit(&BinaryArithm{
					Op: Add,
					X:  litWord("1"),
					Y:  litWord("2"),
				}),
			},
			&BinaryArithm{
				Op: Assgn,
				X:  litWord("b"),
				Y: &BinaryArithm{
					Op: Add,
					X:  litWord("3"),
					Y:  litWord("4"),
				},
			},
		),
	},
	{
		Strs: []string{
			`let a=$(echo 3)`,
			"let a=`echo 3`",
		},
		bash: letClause(
			&BinaryArithm{
				Op: Assgn,
				X:  litWord("a"),
				Y:  word(cmdSubst(litStmt("echo", "3"))),
			},
		),
	},
	{
		Strs:   []string{"(foo-bar)"},
		common: subshell(litStmt("foo-bar")),
	},
	{
		Strs: []string{
			"let i++\nbar",
			"let i++ \nbar",
			"let i++; bar",
		},
		bsmk: stmts(
			letClause(&UnaryArithm{
				Op:   Inc,
				Post: true,
				X:    litWord("i"),
			}),
			litCall("bar"),
		),
	},
	{
		Strs: []string{
			"let i++\nfoo=(bar)",
			"let i++; foo=(bar)",
			"let i++; foo=(bar)\n",
		},
		bsmk: stmts(
			letClause(&UnaryArithm{
				Op:   Inc,
				Post: true,
				X:    litWord("i"),
			}),
			&CallExpr{Assigns: []*Assign{{
				Name:  lit("foo"),
				Array: arrValues(litWord("bar")),
			}}},
		),
	},
	{
		Strs: []string{
			"case a in b) let i++ ;; esac",
			"case a in b) let i++;; esac",
		},
		bsmk: &CaseClause{
			Word: word(lit("a")),
			Items: []*CaseItem{{
				Op:       Break,
				Patterns: litWords("b"),
				Stmts: stmts(letClause(&UnaryArithm{
					Op:   Inc,
					Post: true,
					X:    litWord("i"),
				})),
			}},
		},
	},
	{
		Strs: []string{"a+=1"},
		bsmk: &CallExpr{
			Assigns: []*Assign{{
				Append: true,
				Name:   lit("a"),
				Value:  litWord("1"),
			}},
		},
		posix: litStmt("a+=1"),
	},
	{
		Strs: []string{"b+=(2 3)"},
		bsmk: &CallExpr{Assigns: []*Assign{{
			Append: true,
			Name:   lit("b"),
			Array:  arrValues(litWords("2", "3")...),
		}}},
	},
	{
		Strs:  []string{"a[2]=b c[-3]= d[x]+=e"},
		posix: litStmt("a[2]=b", "c[-3]=", "d[x]+=e"),
		bsmk: &CallExpr{Assigns: []*Assign{
			{
				Name:  lit("a"),
				Index: litWord("2"),
				Value: litWord("b"),
			},
			{
				Name: lit("c"),
				Index: &UnaryArithm{
					Op: Minus,
					X:  litWord("3"),
				},
			},
			{
				Name:   lit("d"),
				Index:  litWord("x"),
				Append: true,
				Value:  litWord("e"),
			},
		}},
	},
	{
		Strs:   []string{"*[i]=x"},
		posix:  lit("*[i]=x"),
		common: word(lit("*"), lit("[i]=x")),
	},
	{
		Strs: []string{
			"b[i]+=2",
			"b[ i ]+=2",
		},
		bsmk: &CallExpr{Assigns: []*Assign{{
			Append: true,
			Name:   lit("b"),
			Index:  litWord("i"),
			Value:  litWord("2"),
		}}},
	},
	{
		Strs: []string{`$((a + "b + $c"))`},
		common: arithmExp(&BinaryArithm{
			Op: Add,
			X:  litWord("a"),
			Y: word(dblQuoted(
				lit("b + "),
				litParamExp("c"),
			)),
		}),
	},
	{
		Strs: []string{`let 'i++'`},
		bsmk: letClause(word(sglQuoted("i++"))),
	},
	{
		Strs: []string{`echo ${a["x y"]}`},
		bash: call(litWord("echo"), word(&ParamExp{
			Param: lit("a"),
			Index: word(dblQuoted(lit("x y"))),
		})),
	},
	{
		Strs: []string{
			`a[$"x y"]=b`,
			`a[ $"x y" ]=b`,
		},
		bash: &CallExpr{Assigns: []*Assign{{
			Name: lit("a"),
			Index: word(&DblQuoted{Dollar: true, Parts: []WordPart{
				lit("x y"),
			}}),
			Value: litWord("b"),
		}}},
	},
	{
		Strs: []string{`((a["x y"] = b))`, `((a["x y"]=b))`},
		bsmk: arithmCmd(&BinaryArithm{
			Op: Assgn,
			X: word(&ParamExp{
				Short: true,
				Param: lit("a"),
				Index: word(dblQuoted(lit("x y"))),
			}),
			Y: litWord("b"),
		}),
	},
	{
		Strs: []string{
			`a=(["x y"]=b)`,
			`a=( [ "x y" ]=b)`,
		},
		bash: &CallExpr{Assigns: []*Assign{{
			Name: lit("a"),
			Array: &ArrayExpr{Elems: []*ArrayElem{{
				Index: word(dblQuoted(lit("x y"))),
				Value: litWord("b"),
			}}},
		}}},
	},
	{
		Strs: []string{
			"a=([x]= [y]=)",
			"a=(\n[x]=\n[y]=\n)",
		},
		bash: &CallExpr{Assigns: []*Assign{{
			Name: lit("a"),
			Array: &ArrayExpr{Elems: []*ArrayElem{
				{Index: litWord("x")},
				{Index: litWord("y")},
			}},
		}}},
	},
	{
		Strs:   []string{"a]b"},
		common: litStmt("a]b"),
	},
	{
		Strs:  []string{"echo a[b c[de]f"},
		posix: litStmt("echo", "a[b", "c[de]f"),
		bsmk: call(litWord("echo"),
			word(lit("a"), lit("[b")),
			word(lit("c"), lit("[de]f")),
		),
	},
	{
		Strs: []string{"<<EOF | b\nfoo\nEOF"},
		common: &BinaryCmd{
			Op: Pipe,
			X: &Stmt{Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("foo\n"),
			}}},
			Y: litStmt("b"),
		},
	},
	{
		Strs: []string{"<<EOF1 <<EOF2 | c && d\nEOF1\nEOF2"},
		common: &BinaryCmd{
			Op: AndStmt,
			X: stmt(&BinaryCmd{
				Op: Pipe,
				X: &Stmt{Redirs: []*Redirect{
					{Op: Hdoc, Word: litWord("EOF1")},
					{Op: Hdoc, Word: litWord("EOF2")},
				}},
				Y: litStmt("c"),
			}),
			Y: litStmt("d"),
		},
	},
	{
		Strs: []string{
			"<<EOF && { bar; }\nhdoc\nEOF",
			"<<EOF &&\nhdoc\nEOF\n{ bar; }",
		},
		common: &BinaryCmd{
			Op: AndStmt,
			X: &Stmt{Redirs: []*Redirect{{
				Op:   Hdoc,
				Word: litWord("EOF"),
				Hdoc: litWord("hdoc\n"),
			}}},
			Y: stmt(block(litStmt("bar"))),
		},
	},
	{
		Strs: []string{"foo() {\n\t<<EOF && { bar; }\nhdoc\nEOF\n}"},
		common: &FuncDecl{
			Parens: true,
			Name:   lit("foo"),
			Body: stmt(block(stmt(&BinaryCmd{
				Op: AndStmt,
				X: &Stmt{Redirs: []*Redirect{{
					Op:   Hdoc,
					Word: litWord("EOF"),
					Hdoc: litWord("hdoc\n"),
				}}},
				Y: stmt(block(litStmt("bar"))),
			}))),
		},
	},
	{
		Strs: []string{`"a$("")"`, "\"a`\"\"`\""},
		common: dblQuoted(
			lit("a"),
			cmdSubst(stmt(call(
				word(dblQuoted()),
			))),
		),
	},
	{
		Strs: []string{"echo ?(b)*(c)+(d)@(e)!(f)"},
		bsmk: call(litWord("echo"), word(
			&ExtGlob{Op: GlobZeroOrOne, Pattern: lit("b")},
			&ExtGlob{Op: GlobZeroOrMore, Pattern: lit("c")},
			&ExtGlob{Op: GlobOneOrMore, Pattern: lit("d")},
			&ExtGlob{Op: GlobOne, Pattern: lit("e")},
			&ExtGlob{Op: GlobExcept, Pattern: lit("f")},
		)),
	},
	{
		Strs: []string{"echo foo@(b*(c|d))bar"},
		bsmk: call(litWord("echo"), word(
			lit("foo"),
			&ExtGlob{Op: GlobOne, Pattern: lit("b*(c|d)")},
			lit("bar"),
		)),
	},
	{
		Strs: []string{"echo $a@(b)$c?(d)$e*(f)$g+(h)$i!(j)$k"},
		bsmk: call(litWord("echo"), word(
			litParamExp("a"),
			&ExtGlob{Op: GlobOne, Pattern: lit("b")},
			litParamExp("c"),
			&ExtGlob{Op: GlobZeroOrOne, Pattern: lit("d")},
			litParamExp("e"),
			&ExtGlob{Op: GlobZeroOrMore, Pattern: lit("f")},
			litParamExp("g"),
			&ExtGlob{Op: GlobOneOrMore, Pattern: lit("h")},
			litParamExp("i"),
			&ExtGlob{Op: GlobExcept, Pattern: lit("j")},
			litParamExp("k"),
		)),
	},
	{
		Strs: []string{"@test \"desc\" { body; }"},
		bats: &TestDecl{
			Description: word(dblQuoted(lit("desc"))),
			Body:        stmt(block(litStmt("body"))),
		},
	},
	{
		Strs: []string{"@test 'desc' {\n\tmultiple\n\tstatements\n}"},
		bats: &TestDecl{
			Description: word(sglQuoted("desc")),
			Body:        stmt(block(litStmts("multiple", "statements")...)),
		},
	},
}

// these don't have a canonical format with the same syntax tree
var fileTestsNoPrint = []testCase{
	{
		Strs:  []string{`$[foo]`},
		posix: word(lit("$"), lit("[foo]")),
	},
	{
		Strs:  []string{`"$[foo]"`},
		posix: dblQuoted(lit("$"), lit("[foo]")),
	},
	{
		Strs: []string{`"$[1 + 3]"`},
		bash: dblQuoted(arithmExpBr(&BinaryArithm{
			Op: Add,
			X:  litWord("1"),
			Y:  litWord("3"),
		})),
	},
}

// these parse with comments
var fileTestsKeepComments = []testCase{
	{
		Strs: []string{"# foo\ncmd\n# bar"},
		common: &File{
			Stmts: []*Stmt{{
				Comments: []Comment{{Text: " foo"}},
				Cmd:      litCall("cmd"),
			}},
			Last: []Comment{{Text: " bar"}},
		},
	},
	{
		Strs: []string{"foo # bar # baz"},
		common: &File{
			Stmts: []*Stmt{{
				Comments: []Comment{{Text: " bar # baz"}},
				Cmd:      litCall("foo"),
			}},
		},
	},
	{
		Strs: []string{
			"$(\n\t# foo\n)",
			"`\n\t# foo\n`",
			"`# foo\n`",
		},
		common: &CmdSubst{
			Last: []Comment{{Text: " foo"}},
		},
	},
	{
		Strs: []string{
			"`# foo`",
			"` # foo`",
		},
		common: &CmdSubst{
			Last: []Comment{{Text: " foo"}},
		},
	},
}

func fullProg(v any) *File {
	f := &File{}
	switch v := v.(type) {
	case *File:
		return v
	case []*Stmt:
		f.Stmts = v
		return f
	case *Stmt:
		f.Stmts = append(f.Stmts, v)
		return f
	case []Command:
		for _, cmd := range v {
			f.Stmts = append(f.Stmts, stmt(cmd))
		}
		return f
	case *Word:
		return fullProg(call(v))
	case WordPart:
		return fullProg(word(v))
	case Command:
		return fullProg(stmt(v))
	case nil:
	default:
		panic(reflect.TypeOf(v))
	}
	return nil
}

func recursiveSanityCheck(tb testing.TB, src string, v any) {
	checkPos := func(pos Pos, strs ...string) {
		if !pos.IsValid() {
			tb.Fatalf("invalid Pos in %T", v)
		}
		if src == "" {
			return
		}
		offs := pos.Offset()
		if offs > uint(len(src)) {
			tb.Errorf("Pos offset %d in %T is out of bounds in %q",
				offs, v, src)
			return
		}
		if len(strs) == 0 {
			return
		}
		if strings.Contains(src, "<<-") {
			// since the tab indentation in <<- heredoc bodies
			// aren't part of the final literals
			return
		}
		var gotErr string
		for i, want := range strs {
			got := src[offs:]
			if i == 0 {
				gotErr = got
			}
			got = strings.ReplaceAll(got, "\x00", "")
			got = strings.ReplaceAll(got, "\r\n", "\n")
			if !strings.Contains(want, "\\\n") {
				// Hack to let "foobar" match the input "foo\\\nbar".
				got = strings.ReplaceAll(got, "\\\n", "")
			}
			if strings.HasPrefix(got, want) {
				return
			}
		}
		tb.Errorf("Expected one of %q at %s in %q, found %q",
			strs, pos, src, gotErr)
	}
	checkNodePosEnd := func(n Node) {
		if n.Pos().After(n.End()) {
			tb.Errorf("Found End() before Pos() in %T", n)
		}
	}
	recurse := func(v any) {
		recursiveSanityCheck(tb, src, v)
		if n, ok := v.(Node); ok {
			checkNodePosEnd(n)
		}
	}
	switch v := v.(type) {
	case *File:
		recurse(v.Stmts)
		recurse(v.Last)
		checkNodePosEnd(v)
	case []*Stmt:
		for _, s := range v {
			recurse(s)
		}
	case []Comment:
		for i := range v {
			recurse(&v[i])
		}
	case *Comment:
		checkPos(v.Hash, "#"+v.Text)
	case *Stmt:
		endOff := int(v.End().Offset())
		if endOff < len(src) {
			end := src[endOff]
			switch {
			case end == ' ', end == '\n', end == '\t', end == '\r':
				// ended by whitespace
			case regOps(rune(end)):
				// ended by end character
			case endOff > 0 && src[endOff-1] == ';':
				// ended by semicolon
			case endOff > 0 && src[endOff-1] == '&':
				// ended by & or |&
			case end == '\\' && src[endOff+1] == '`':
				// ended by an escaped backquote
			default:
				tb.Errorf("Unexpected Stmt.End() %d %q in %q",
					endOff, end, src)
			}
		}
		recurse(v.Comments)
		if src[v.Position.Offset()] == '#' {
			tb.Errorf("Stmt.Pos() should not be a comment")
		}
		checkPos(v.Position)
		if v.Semicolon.IsValid() {
			checkPos(v.Semicolon, ";", "&", "|&")
		}
		if v.Cmd != nil {
			recurse(v.Cmd)
		}
		for _, r := range v.Redirs {
			checkPos(r.OpPos, r.Op.String())
			if r.N != nil {
				recurse(r.N)
			}
			recurse(r.Word)
			if r.Hdoc != nil {
				recurse(r.Hdoc)
			}
		}
	case []*Assign:
		for _, a := range v {
			if a.Name != nil {
				recurse(a.Name)
			}
			if a.Index != nil {
				recurse(a.Index)
			}
			if a.Value != nil {
				recurse(a.Value)
			}
			if a.Array != nil {
				recurse(a.Array)
			}
			checkNodePosEnd(a)
		}
	case *CallExpr:
		recurse(v.Assigns)
		recurse(v.Args)
	case []*Word:
		for _, w := range v {
			recurse(w)
		}
	case *Word:
		recurse(v.Parts)
	case []WordPart:
		for _, wp := range v {
			recurse(wp)
		}
	case *Lit:
		pos, end := int(v.Pos().Offset()), int(v.End().Offset())
		want := pos + len(v.Value)
		val := v.Value
		posLine := v.Pos().Line()
		endLine := v.End().Line()
		switch {
		case src == "":
		case strings.Contains(src, "\\\n"), strings.Contains(src, "\\\r\n"):
		case !strings.Contains(v.Value, "\n") && posLine != endLine:
			tb.Errorf("Lit without newlines has Pos/End lines %d and %d",
				posLine, endLine)
		case strings.Contains(src, "`") && strings.Contains(src, "\\"):
			// removed backslashes inside backquote cmd substs
			val = ""
		case end < len(src) && (src[end] == '\n' || src[end] == '`'):
			// heredoc literals that end with the
			// stop word and a newline or closing backquote
		case end == len(src):
			// same as above, but with word and EOF
		case end != want:
			tb.Errorf("Unexpected Lit %q End() %d (wanted %d for pos %d) in %q",
				val, end, want, pos, src)
		}
		checkPos(v.ValuePos, val)
		checkPos(v.ValueEnd)
	case *Subshell:
		checkPos(v.Lparen, "(")
		checkPos(v.Rparen, ")")
		recurse(v.Stmts)
		recurse(v.Last)
	case *Block:
		checkPos(v.Lbrace, "{")
		checkPos(v.Rbrace, "}")
		recurse(v.Stmts)
		recurse(v.Last)
	case *IfClause:
		if v.ThenPos.IsValid() {
			checkPos(v.Position, "if", "elif")
			checkPos(v.ThenPos, "then")
		} else {
			checkPos(v.Position, "else")
		}
		checkPos(v.FiPos, "fi")
		recurse(v.Cond)
		recurse(v.CondLast)
		recurse(v.Then)
		recurse(v.ThenLast)
		if v.Else != nil {
			recurse(v.Else)
		}
	case *WhileClause:
		rsrv := "while"
		if v.Until {
			rsrv = "until"
		}
		checkPos(v.WhilePos, rsrv)
		checkPos(v.DoPos, "do")
		checkPos(v.DonePos, "done")
		recurse(v.Cond)
		recurse(v.CondLast)
		recurse(v.Do)
		recurse(v.DoLast)
	case *ForClause:
		if v.Select {
			checkPos(v.ForPos, "select")
		} else {
			checkPos(v.ForPos, "for")
		}
		if v.Braces {
			checkPos(v.DoPos, "{")
			checkPos(v.DonePos, "}")
			// Zero out Braces, to not duplicate all the test cases.
			// The printer ignores the field anyway.
			v.Braces = false
		} else {
			checkPos(v.DoPos, "do")
			checkPos(v.DonePos, "done")
		}
		recurse(v.Loop)
		recurse(v.Do)
		recurse(v.DoLast)
	case *WordIter:
		recurse(v.Name)
		if v.InPos.IsValid() {
			checkPos(v.InPos, "in")
		}
		recurse(v.Items)
	case *CStyleLoop:
		checkPos(v.Lparen, "((")
		checkPos(v.Rparen, "))")
		if v.Init != nil {
			recurse(v.Init)
		}
		if v.Cond != nil {
			recurse(v.Cond)
		}
		if v.Post != nil {
			recurse(v.Post)
		}
	case *SglQuoted:
		checkPos(posAddCol(v.End(), -1), "'")
		valuePos := posAddCol(v.Left, 1)
		if v.Dollar {
			valuePos = posAddCol(valuePos, 1)
		}
		val := v.Value
		if strings.Contains(src, "`") && strings.Contains(src, "\\") {
			// removed backslashes inside backquote cmd substs
			val = ""
		}
		checkPos(valuePos, val)
		if v.Dollar {
			checkPos(v.Left, "$'")
		} else {
			checkPos(v.Left, "'")
		}
		checkPos(v.Right, "'")
	case *DblQuoted:
		checkPos(posAddCol(v.End(), -1), `"`)
		if v.Dollar {
			checkPos(v.Left, `$"`)
		} else {
			checkPos(v.Left, `"`)
		}
		checkPos(v.Right, `"`)
		recurse(v.Parts)
	case *UnaryArithm:
		checkPos(v.OpPos, v.Op.String())
		recurse(v.X)
	case *UnaryTest:
		strs := []string{v.Op.String()}
		switch v.Op {
		case TsExists:
			strs = append(strs, "-a")
		case TsSmbLink:
			strs = append(strs, "-h")
		}
		checkPos(v.OpPos, strs...)
		recurse(v.X)
	case *BinaryCmd:
		checkPos(v.OpPos, v.Op.String())
		recurse(v.X)
		recurse(v.Y)
	case *BinaryArithm:
		checkPos(v.OpPos, v.Op.String())
		recurse(v.X)
		recurse(v.Y)
	case *BinaryTest:
		strs := []string{v.Op.String()}
		switch v.Op {
		case TsMatch:
			strs = append(strs, "=")
		}
		checkPos(v.OpPos, strs...)
		recurse(v.X)
		recurse(v.Y)
	case *ParenArithm:
		checkPos(v.Lparen, "(")
		checkPos(v.Rparen, ")")
		recurse(v.X)
	case *ParenTest:
		checkPos(v.Lparen, "(")
		checkPos(v.Rparen, ")")
		recurse(v.X)
	case *FuncDecl:
		if v.RsrvWord {
			checkPos(v.Position, "function")
		} else {
			checkPos(v.Position)
		}
		recurse(v.Name)
		recurse(v.Body)
	case *ParamExp:
		doll := "$"
		if v.nakedIndex() {
			doll = ""
		}
		checkPos(v.Dollar, doll)
		if !v.Short {
			checkPos(v.Rbrace, "}")
		} else if v.nakedIndex() {
			checkPos(posAddCol(v.End(), -1), "]")
		}
		recurse(v.Param)
		if v.Index != nil {
			recurse(v.Index)
		}
		if v.Slice != nil {
			if v.Slice.Offset != nil {
				recurse(v.Slice.Offset)
			}
			if v.Slice.Length != nil {
				recurse(v.Slice.Length)
			}
		}
		if v.Repl != nil {
			if v.Repl.Orig != nil {
				recurse(v.Repl.Orig)
			}
			if v.Repl.With != nil {
				recurse(v.Repl.With)
			}
		}
		if v.Exp != nil && v.Exp.Word != nil {
			recurse(v.Exp.Word)
		}
	case *ArithmExp:
		if v.Bracket {
			// deprecated $(( form
			checkPos(v.Left, "$[")
			checkPos(v.Right, "]")
		} else {
			checkPos(v.Left, "$((")
			checkPos(v.Right, "))")
		}
		recurse(v.X)
	case *ArithmCmd:
		checkPos(v.Left, "((")
		checkPos(v.Right, "))")
		recurse(v.X)
	case *CmdSubst:
		switch {
		case v.TempFile:
			checkPos(v.Left, "${ ", "${\t", "${\n")
			checkPos(v.Right, "}")
		case v.ReplyVar:
			checkPos(v.Left, "${|")
			checkPos(v.Right, "}")
		case v.Backquotes:
			checkPos(v.Left, "`", "\\`")
			checkPos(v.Right, "`", "\\`")
			// Zero out Backquotes, to not duplicate all the test
			// cases. The printer ignores the field anyway.
			v.Backquotes = false
		default:
			checkPos(v.Left, "$(")
			checkPos(v.Right, ")")
		}
		recurse(v.Stmts)
		recurse(v.Last)
	case *CaseClause:
		checkPos(v.Case, "case")
		if v.Braces {
			checkPos(v.In, "{")
			checkPos(v.Esac, "}")
			// Zero out Braces, to not duplicate all the test cases.
			// The printer ignores the field anyway.
			v.Braces = false
		} else {
			checkPos(v.In, "in")
			checkPos(v.Esac, "esac")
		}
		recurse(v.Word)
		for _, ci := range v.Items {
			recurse(ci)
		}
	case *CaseItem:
		if v.OpPos.IsValid() {
			checkPos(v.OpPos, v.Op.String(), "esac")
		}
		recurse(v.Patterns)
		recurse(v.Stmts)
		recurse(v.Last)
	case *TestClause:
		checkPos(v.Left, "[[")
		checkPos(v.Right, "]]")
		recurse(v.X)
	case *DeclClause:
		recurse(v.Variant)
		recurse(v.Args)
	case *TimeClause:
		checkPos(v.Time, "time")
		if v.Stmt != nil {
			recurse(v.Stmt)
		}
	case *CoprocClause:
		checkPos(v.Coproc, "coproc")
		if v.Name != nil {
			recurse(v.Name)
		}
		recurse(v.Stmt)
	case *LetClause:
		checkPos(v.Let, "let")
		for _, expr := range v.Exprs {
			recurse(expr)
		}
	case *TestDecl:
		checkPos(v.Position, "@test")
		recurse(v.Description)
		recurse(v.Body)
	case *ArrayExpr:
		checkPos(v.Lparen, "(")
		checkPos(v.Rparen, ")")
		for _, elem := range v.Elems {
			recurse(elem)
		}
	case *ArrayElem:
		if v.Index != nil {
			recurse(v.Index)
		}
		if v.Value != nil {
			recurse(v.Value)
		}
	case *ExtGlob:
		checkPos(v.OpPos, v.Op.String())
		checkPos(posAddCol(v.End(), -1), ")")
		recurse(v.Pattern)
	case *ProcSubst:
		checkPos(v.OpPos, v.Op.String())
		checkPos(v.Rparen, ")")
		recurse(v.Stmts)
		recurse(v.Last)
	default:
		panic(reflect.TypeOf(v))
	}
}
07070100000050000081A4000000000000000000000001686AE5C0000011E1000000000000000000000000000000000000001E00000000sh-3.12.0/syntax/fuzz_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"fmt"
	"io"
	"os/exec"
	"strings"
	"testing"

	"github.com/go-quicktest/qt"
)

func FuzzQuote(f *testing.F) {
	if _, err := exec.LookPath("bash"); err != nil {
		f.Skipf("requires bash to verify quoted strings")
	}

	// Keep in sync with ExampleQuote.
	f.Add("foo", uint8(LangBash))
	f.Add("bar $baz", uint8(LangBash))
	f.Add(`"won't"`, uint8(LangBash))
	f.Add(`~/home`, uint8(LangBash))
	f.Add("#1304", uint8(LangBash))
	f.Add("name=value", uint8(LangBash))
	f.Add(`glob-*`, uint8(LangBash))
	f.Add("invalid-\xe2'", uint8(LangBash))
	f.Add("nonprint-\x0b\x1b", uint8(LangBash))
	f.Fuzz(func(t *testing.T, s string, langVariant uint8) {
		if langVariant > 3 {
			t.Skip() // lang variants are 0-3
		}
		lang := LangVariant(langVariant)
		quoted, err := Quote(s, lang)
		if err != nil {
			// Cannot be quoted; not interesting.
			t.Skip()
		}

		var shellProgram string
		switch lang {
		case LangBash:
			requireBash52(t)
			shellProgram = "bash"
		case LangPOSIX:
			requireDash059(t)
			shellProgram = "dash"
		case LangMirBSDKorn:
			requireMksh59(t)
			shellProgram = "mksh"
		case LangBats:
			t.Skip() // bats has no shell and its syntax is just bash
		default:
			panic(fmt.Sprintf("unknown lang variant: %d", lang))
		}

		// Verify that our parser ends up with a simple command with one word.
		f, err := NewParser(Variant(lang)).Parse(strings.NewReader(quoted), "")
		if err != nil {
			t.Fatalf("parse error on %q quoted as %s: %v", s, quoted, err)
		}
		qt.Assert(t, qt.Equals(len(f.Stmts), 1), qt.Commentf("in: %q, quoted: %s", s, quoted))
		call, ok := f.Stmts[0].Cmd.(*CallExpr)
		qt.Assert(t, qt.IsTrue(ok), qt.Commentf("in: %q, quoted: %s", s, quoted))
		qt.Assert(t, qt.Equals(len(call.Args), 1), qt.Commentf("in: %q, quoted: %s", s, quoted))

		// Also check that the single word only uses literals or quoted strings.
		Walk(call.Args[0], func(node Node) bool {
			switch node.(type) {
			case nil, *Word, *Lit, *SglQuoted, *DblQuoted:
			default:
				t.Fatalf("unexpected node type: %T", node)
			}
			return true
		})

		// The process below shouldn't run arbitrary code,
		// since our parser checks above should catch the use of ';' or '$',
		// in the case that Quote were too naive to quote them.
		out, err := exec.Command(shellProgram, "-c", "printf %s "+quoted).CombinedOutput()
		if err != nil {
			t.Fatalf("%s error on %q quoted as %s: %v: %s", shellProgram, s, quoted, err, out)
		}
		want, got := s, string(out)
		if want != got {
			t.Fatalf("%s output mismatch on %q quoted as %s: got %q (len=%d)",
				shellProgram, want, quoted, got, len(got))
		}
	})
}

func FuzzParsePrint(f *testing.F) {
	add := func(src string, variant LangVariant) {
		// For now, default to just KeepComments.
		f.Add(src, uint8(variant), true, false,
			uint8(0), false, false, false, false, false, false, false)
	}

	for _, test := range shellTests {
		add(test.in, LangBash)
	}
	for _, test := range printTests {
		add(test.in, LangBash)
	}
	for _, test := range fileTests {
		for _, in := range test.Strs {
			if test.Bash != nil {
				add(in, LangBash)
			}
			if test.Posix != nil {
				add(in, LangPOSIX)
			}
			if test.MirBSDKorn != nil {
				add(in, LangMirBSDKorn)
			}
			if test.Bats != nil {
				add(in, LangBats)
			}
		}
	}

	f.Fuzz(func(t *testing.T,
		src string,

		// parser options
		// TODO: also fuzz StopAt
		langVariant uint8, // 0-3
		keepComments bool,

		simplify bool,

		// printer options
		indent uint8, // 0-255
		binaryNextLine bool,
		switchCaseIndent bool,
		spaceRedirects bool,
		keepPadding bool,
		minify bool,
		singleLine bool,
		functionNextLine bool,
	) {
		if langVariant > 3 {
			t.Skip() // lang variants are 0-3
		}
		if indent > 16 {
			t.Skip() // more indentation won't really be interesting
		}

		parser := NewParser()
		Variant(LangVariant(langVariant))(parser)
		KeepComments(keepComments)(parser)

		prog, err := parser.Parse(strings.NewReader(src), "")
		if err != nil {
			t.Skip() // not valid shell syntax
		}

		if simplify {
			Simplify(prog)
		}

		printer := NewPrinter()
		Indent(uint(indent))(printer)
		BinaryNextLine(binaryNextLine)(printer)
		SwitchCaseIndent(switchCaseIndent)(printer)
		SpaceRedirects(spaceRedirects)(printer)
		KeepPadding(keepPadding)(printer)
		Minify(minify)(printer)
		SingleLine(singleLine)(printer)
		FunctionNextLine(functionNextLine)(printer)

		if err := printer.Print(io.Discard, prog); err != nil {
			t.Skip() // e.g. invalid option
		}
	})
}
07070100000051000081A4000000000000000000000001686AE5C000005748000000000000000000000000000000000000001A00000000sh-3.12.0/syntax/lexer.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"bytes"
	"io"
	"unicode/utf8"
)

// bytes that form or start a token
func regOps(r rune) bool {
	switch r {
	case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`':
		return true
	}
	return false
}

// tokenize these inside parameter expansions
func paramOps(r rune) bool {
	switch r {
	case '}', '#', '!', ':', '-', '+', '=', '?', '%', '[', ']', '/', '^',
		',', '@', '*':
		return true
	}
	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
}

func bquoteEscaped(b byte) bool {
	switch b {
	case '$', '`', '\\':
		return true
	}
	return false
}

const escNewl rune = utf8.RuneSelf + 1

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.
		p.line++
		p.col = 0
	}
	p.col += int64(p.w)
	bquotes := 0
retry:
	if p.bsp < uint(len(p.bs)) {
		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.peekByte('\n') { // \r\n turns into \n
					p.col++
					goto retry
				}
			case '\\':
				if p.r == '\\' {
				} else if p.peekByte('\n') {
					p.bsp++
					p.w, p.r = 1, escNewl
					return escNewl
				} else if p.peekBytes("\r\n") { // \\\r\n turns into \\\n
					p.col++
					p.bsp += 2
					p.w, p.r = 2, escNewl
					return escNewl
				}
				if p.openBquotes > 0 && bquotes < p.openBquotes &&
					p.bsp < 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
				}
			}
			if b == '`' {
				p.lastBquoteEsc = bquotes
			}
			if p.litBs != nil {
				p.litBs = append(p.litBs, b)
			}
			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()
		}
		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+uint(w)]...)
		}
		p.bsp += uint(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
		}
	}
	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 += 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 n == 0 {
		if err == nil {
			goto readAgain
		}
		// don't use p.errPass as we don't want to overwrite p.tok
		if err != io.EOF {
			p.err = err
		}
		if left > 0 {
			p.bs = p.readBuf[:left]
		} else {
			p.bs = nil
		}
	} else {
		p.bs = p.readBuf[:left+n]
	}
	p.bsp = 0
}

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.
		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 dblQuotes:
		switch r {
		case '`', '"', '$':
			p.tok = p.dqToken(r)
		default:
			p.advanceLitDquote(r)
		}
	case hdocBody, hdocBodyTabs:
		switch r {
		case '`', '$':
			p.tok = p.dqToken(r)
		default:
			p.advanceLitHdoc(r)
		}
	default: // paramExpExp:
		switch r {
		case '}':
			p.tok = p.paramToken(r)
		case '`', '"', '$', '\'':
			p.tok = p.regToken(r)
		default:
			p.advanceLitOther(r)
		}
	}
	if p.err != nil && p.tok != _EOF {
		p.tok = _EOF
	}
}

func (p *Parser) next() {
	if p.r == utf8.RuneSelf {
		p.tok = _EOF
		return
	}
	p.spaced = false
	if p.quote&allKeepSpaces != 0 {
		p.nextKeepSpaces()
		return
	}
	r := p.r
	for r == escNewl {
		r = p.rune()
	}
skipSpace:
	for {
		switch r {
		case utf8.RuneSelf:
			p.tok = _EOF
			return
		case escNewl:
			r = p.rune()
		case ' ', '\t', '\r':
			p.spaced = true
			r = p.rune()
		case '\n':
			if p.tok == _Newl {
				// merge consecutive newline tokens
				r = p.rune()
				continue
			}
			p.spaced = true
			p.tok = _Newl
			if p.quote != hdocWord && len(p.heredocs) > p.buriedHdocs {
				p.doHeredocs()
			}
			return
		default:
			break skipSpace
		}
	}
	if p.stopAt != nil && (p.spaced || p.tok == illegalTok || p.stopToken()) {
		w := utf8.RuneLen(r)
		if bytes.HasPrefix(p.bs[p.bsp-uint(w):], p.stopAt) {
			p.r = utf8.RuneSelf
			p.w = 1
			p.tok = _EOF
			return
		}
	}
	p.pos = p.nextPos()
	switch {
	case p.quote&allRegTokens != 0:
		switch r {
		case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`':
			p.tok = p.regToken(r)
		case '#':
			// If we're parsing $foo#bar, ${foo}#bar, 'foo'#bar, or "foo"#bar,
			// #bar is a continuation of the same word, not a comment.
			// TODO: support $(foo)#bar and `foo`#bar as well, which is slightly tricky,
			// as we can't easily tell them apart from (foo)#bar and `#bar`,
			// where #bar should remain a comment.
			if !p.spaced {
				switch p.tok {
				case _LitWord, rightBrace, sglQuote, dblQuote:
					p.advanceLitNone(r)
					return
				}
			}
			r = p.rune()
			p.newLit(r)
		runeLoop:
			for {
				switch r {
				case '\n', utf8.RuneSelf:
					break runeLoop
				case escNewl:
					p.litBs = append(p.litBs, '\\', '\n')
					break runeLoop
				case '`':
					if p.backquoteEnd() {
						break runeLoop
					}
				}
				r = p.rune()
			}
			if p.keepComments {
				*p.curComs = append(*p.curComs, Comment{
					Hash: p.pos,
					Text: p.endLit(),
				})
			} else {
				p.litBs = nil
			}
			p.next()
		case '[', '=':
			if p.quote == arrayElems {
				p.tok = p.paramToken(r)
			} else {
				p.advanceLitNone(r)
			}
		case '?', '*', '+', '@', '!':
			if p.extendedGlob() {
				switch r {
				case '?':
					p.tok = globQuest
				case '*':
					p.tok = globStar
				case '+':
					p.tok = globPlus
				case '@':
					p.tok = globAt
				default: // '!'
					p.tok = globExcl
				}
				p.rune()
				p.rune()
			} else {
				p.advanceLitNone(r)
			}
		default:
			p.advanceLitNone(r)
		}
	case p.quote&allArithmExpr != 0 && arithmOps(r):
		p.tok = p.arithmToken(r)
	case p.quote&allParamExp != 0 && paramOps(r):
		p.tok = p.paramToken(r)
	case p.quote == testExprRegexp:
		if !p.rxFirstPart && p.spaced {
			p.quote = noState
			goto skipSpace
		}
		p.rxFirstPart = false
		switch r {
		case ';', '"', '\'', '$', '&', '>', '<', '`':
			p.tok = p.regToken(r)
		case ')':
			if p.rxOpenParens > 0 {
				// continuation of open paren
				p.advanceLitRe(r)
			} else {
				p.tok = rightParen
				p.quote = noState
				p.rune() // we are tokenizing manually
			}
		default: // including '(', '|'
			p.advanceLitRe(r)
		}
	case regOps(r):
		p.tok = p.regToken(r)
	default:
		p.advanceLitOther(r)
	}
	if p.err != nil && p.tok != _EOF {
		p.tok = _EOF
	}
}

// extendedGlob determines whether we're parsing a Bash extended globbing expression.
// For example, whether `*` or `@` are followed by `(` to form `@(foo)`.
func (p *Parser) extendedGlob() bool {
	if p.val == "function" {
		return false
	}
	if p.peekByte('(') {
		// NOTE: empty pattern list is a valid globbing syntax like `@()`,
		// but we'll operate on the "likelihood" that it is a function;
		// only tokenize if its a non-empty pattern list.
		// We do this after peeking for just one byte, so that the input `echo *`
		// followed by a newline does not hang an interactive shell parser until
		// another byte is input.
		return !p.peekBytes("()")
	}
	return false
}

func (p *Parser) peekBytes(s string) bool {
	peekEnd := int(p.bsp) + len(s)
	// TODO: This should loop for slow readers, e.g. those providing one byte at
	// a time. Use a loop and test it with [testing/iotest.OneByteReader].
	if peekEnd > len(p.bs) {
		p.fill()
	}
	return peekEnd <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:peekEnd], []byte(s))
}

func (p *Parser) peekByte(b byte) bool {
	if p.bsp == uint(len(p.bs)) {
		p.fill()
	}
	return p.bsp < uint(len(p.bs)) && p.bs[p.bsp] == b
}

func (p *Parser) regToken(r rune) token {
	switch r {
	case '\'':
		p.rune()
		return sglQuote
	case '"':
		p.rune()
		return dblQuote
	case '`':
		// Don't call p.rune, as we need to work out p.openBquotes to
		// properly handle backslashes in the lexer.
		return bckQuote
	case '&':
		switch p.rune() {
		case '&':
			p.rune()
			return andAnd
		case '>':
			if p.rune() == '>' {
				p.rune()
				return appAll
			}
			return rdrAll
		}
		return and
	case '|':
		switch p.rune() {
		case '|':
			p.rune()
			return orOr
		case '&':
			if p.lang == LangPOSIX {
				break
			}
			p.rune()
			return orAnd
		}
		return or
	case '$':
		switch p.rune() {
		case '\'':
			if p.lang == LangPOSIX {
				break
			}
			p.rune()
			return dollSglQuote
		case '"':
			if p.lang == LangPOSIX {
				break
			}
			p.rune()
			return dollDblQuote
		case '{':
			p.rune()
			return dollBrace
		case '[':
			if !p.lang.isBash() || p.quote == paramExpName {
				// latter to not tokenise ${$[@]} as $[
				break
			}
			p.rune()
			return dollBrack
		case '(':
			if p.rune() == '(' {
				p.rune()
				return dollDblParen
			}
			return dollParen
		}
		return dollar
	case '(':
		if p.rune() == '(' && p.lang != LangPOSIX && p.quote != testExpr {
			p.rune()
			return dblLeftParen
		}
		return leftParen
	case ')':
		p.rune()
		return rightParen
	case ';':
		switch p.rune() {
		case ';':
			if p.rune() == '&' && p.lang.isBash() {
				p.rune()
				return dblSemiAnd
			}
			return dblSemicolon
		case '&':
			if p.lang == LangPOSIX {
				break
			}
			p.rune()
			return semiAnd
		case '|':
			if p.lang != LangMirBSDKorn {
				break
			}
			p.rune()
			return semiOr
		}
		return semicolon
	case '<':
		switch p.rune() {
		case '<':
			if r = p.rune(); r == '-' {
				p.rune()
				return dashHdoc
			} else if r == '<' {
				p.rune()
				return wordHdoc
			}
			return hdoc
		case '>':
			p.rune()
			return rdrInOut
		case '&':
			p.rune()
			return dplIn
		case '(':
			if !p.lang.isBash() {
				break
			}
			p.rune()
			return cmdIn
		}
		return rdrIn
	default: // '>'
		switch p.rune() {
		case '>':
			p.rune()
			return appOut
		case '&':
			p.rune()
			return dplOut
		case '|':
			p.rune()
			return clbOut
		case '(':
			if !p.lang.isBash() {
				break
			}
			p.rune()
			return cmdOut
		}
		return rdrOut
	}
}

func (p *Parser) dqToken(r rune) token {
	switch r {
	case '"':
		p.rune()
		return dblQuote
	case '`':
		// Don't call p.rune, as we need to work out p.openBquotes to
		// properly handle backslashes in the lexer.
		return bckQuote
	default: // '$'
		switch p.rune() {
		case '{':
			p.rune()
			return dollBrace
		case '[':
			if !p.lang.isBash() {
				break
			}
			p.rune()
			return dollBrack
		case '(':
			if p.rune() == '(' {
				p.rune()
				return dollDblParen
			}
			return dollParen
		}
		return dollar
	}
}

func (p *Parser) paramToken(r rune) token {
	switch r {
	case '}':
		p.rune()
		return rightBrace
	case ':':
		switch p.rune() {
		case '+':
			p.rune()
			return colPlus
		case '-':
			p.rune()
			return colMinus
		case '?':
			p.rune()
			return colQuest
		case '=':
			p.rune()
			return colAssgn
		}
		return colon
	case '+':
		p.rune()
		return plus
	case '-':
		p.rune()
		return minus
	case '?':
		p.rune()
		return quest
	case '=':
		p.rune()
		return assgn
	case '%':
		if p.rune() == '%' {
			p.rune()
			return dblPerc
		}
		return perc
	case '#':
		if p.rune() == '#' {
			p.rune()
			return dblHash
		}
		return hash
	case '!':
		p.rune()
		return exclMark
	case '[':
		p.rune()
		return leftBrack
	case ']':
		p.rune()
		return rightBrack
	case '/':
		if p.rune() == '/' && p.quote != paramExpRepl {
			p.rune()
			return dblSlash
		}
		return slash
	case '^':
		if p.rune() == '^' {
			p.rune()
			return dblCaret
		}
		return caret
	case ',':
		if p.rune() == ',' {
			p.rune()
			return dblComma
		}
		return comma
	case '@':
		p.rune()
		return at
	default: // '*'
		p.rune()
		return star
	}
}

func (p *Parser) arithmToken(r rune) token {
	switch r {
	case '!':
		if p.rune() == '=' {
			p.rune()
			return nequal
		}
		return exclMark
	case '=':
		if p.rune() == '=' {
			p.rune()
			return equal
		}
		return assgn
	case '~':
		p.rune()
		return tilde
	case '(':
		p.rune()
		return leftParen
	case ')':
		p.rune()
		return rightParen
	case '&':
		switch p.rune() {
		case '&':
			p.rune()
			return andAnd
		case '=':
			p.rune()
			return andAssgn
		}
		return and
	case '|':
		switch p.rune() {
		case '|':
			p.rune()
			return orOr
		case '=':
			p.rune()
			return orAssgn
		}
		return or
	case '<':
		switch p.rune() {
		case '<':
			if p.rune() == '=' {
				p.rune()
				return shlAssgn
			}
			return hdoc
		case '=':
			p.rune()
			return lequal
		}
		return rdrIn
	case '>':
		switch p.rune() {
		case '>':
			if p.rune() == '=' {
				p.rune()
				return shrAssgn
			}
			return appOut
		case '=':
			p.rune()
			return gequal
		}
		return rdrOut
	case '+':
		switch p.rune() {
		case '+':
			p.rune()
			return addAdd
		case '=':
			p.rune()
			return addAssgn
		}
		return plus
	case '-':
		switch p.rune() {
		case '-':
			p.rune()
			return subSub
		case '=':
			p.rune()
			return subAssgn
		}
		return minus
	case '%':
		if p.rune() == '=' {
			p.rune()
			return remAssgn
		}
		return perc
	case '*':
		switch p.rune() {
		case '*':
			p.rune()
			return power
		case '=':
			p.rune()
			return mulAssgn
		}
		return star
	case '/':
		if p.rune() == '=' {
			p.rune()
			return quoAssgn
		}
		return slash
	case '^':
		if p.rune() == '=' {
			p.rune()
			return xorAssgn
		}
		return caret
	case '[':
		p.rune()
		return leftBrack
	case ']':
		p.rune()
		return rightBrack
	case ',':
		p.rune()
		return comma
	case '?':
		p.rune()
		return quest
	case ':':
		p.rune()
		return colon
	default: // '#'
		p.rune()
		return hash
	}
}

func (p *Parser) newLit(r rune) {
	switch {
	case r < utf8.RuneSelf:
		p.litBs = p.litBuf[:1]
		p.litBs[0] = byte(r)
	case r > escNewl:
		w := utf8.RuneLen(r)
		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 [utf8.RuneLen]
		// would return -1
		p.litBs = p.litBuf[:0]
	}
}

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)-p.w])
	}
	p.litBs = nil
	return
}

func (p *Parser) isLitRedir() bool {
	lit := p.litBs[:len(p.litBs)-1]
	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 true
}

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
		}
	}
	p.tok, p.val = _LitWord, p.endLit()
}

func (p *Parser) advanceLitOther(r rune) {
	tok := _LitWord
loop:
	for p.newLit(r); r != utf8.RuneSelf; r = p.rune() {
		switch r {
		case '\\': // escaped byte follows
			p.rune()
		case '\'', '"', '`', '$':
			tok = _Lit
			break loop
		case '}':
			if p.quote&allParamExp != 0 {
				break loop
			}
		case '/':
			if p.quote != paramExpExp {
				break loop
			}
		case ':', '=', '%', '^', ',', '?', '!', '~', '*':
			if p.quote&allArithmExpr != 0 || p.quote == paramExpName {
				break loop
			}
		case '[', ']':
			if p.lang != LangPOSIX && p.quote&allArithmExpr != 0 {
				break loop
			}
			fallthrough
		case '#', '@':
			if p.quote&allParamReg != 0 {
				break loop
			}
		case '+', '-', ' ', '\t', ';', '&', '>', '<', '|', '(', ')', '\n', '\r':
			if p.quote&allKeepSpaces == 0 {
				break loop
			}
		}
	}
	p.tok, p.val = tok, p.endLit()
}

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', '&', '|', ';', '(', ')':
			break loop
		case '\\': // escaped byte follows
			p.rune()
		case '>', '<':
			if p.peekByte('(') {
				tok = _Lit
			} else if p.isLitRedir() {
				tok = _LitRedir
			}
			break loop
		case '`':
			if p.quote != subCmdBckquo {
				tok = _Lit
			}
			break loop
		case '"', '\'', '$':
			tok = _Lit
			break loop
		case '?', '*', '+', '@', '!':
			if p.extendedGlob() {
				tok = _Lit
				break loop
			}
		case '=':
			if p.eqlOffs < 0 {
				p.eqlOffs = len(p.litBs) - 1
			}
		case '[':
			if p.lang != LangPOSIX && len(p.litBs) > 1 && p.litBs[0] != '[' {
				tok = _Lit
				break loop
			}
		}
	}
	p.tok, p.val = tok, p.endLit()
}

func (p *Parser) advanceLitDquote(r rune) {
	tok := _LitWord
loop:
	for p.newLit(r); r != utf8.RuneSelf; r = p.rune() {
		switch r {
		case '"':
			break loop
		case '\\': // escaped byte follows
			p.rune()
		case escNewl, '`', '$':
			tok = _Lit
			break loop
		}
	}
	p.tok, p.val = tok, p.endLit()
}

func (p *Parser) advanceLitHdoc(r rune) {
	// Unlike the rest of nextKeepSpaces quote states, we handle escaped
	// newlines here. If lastTok==_Lit, then we know we're following an
	// escaped newline, so the first line can't end the heredoc.
	lastTok := p.tok
	for r == escNewl {
		r = p.rune()
		lastTok = _Lit
	}
	p.pos = p.nextPos()

	p.tok = _Lit
	p.newLit(r)
	if p.quote == hdocBodyTabs {
		for r == '\t' {
			r = p.rune()
		}
	}
	lStart := len(p.litBs) - 1
	stop := p.hdocStops[len(p.hdocStops)-1]
	for ; ; r = p.rune() {
		switch r {
		case escNewl, '$':
			p.val = p.endLit()
			return
		case '\\': // escaped byte follows
			p.rune()
		case '`':
			if !p.backquoteEnd() {
				p.val = p.endLit()
				return
			}
			fallthrough
		case '\n', utf8.RuneSelf:
			if p.parsingDoc {
				if r == utf8.RuneSelf {
					p.tok = _LitWord
					p.val = p.endLit()
					return
				}
			} else if lStart == 0 && lastTok == _Lit {
				// This line starts right after an escaped
				// newline, so it should never end the heredoc.
			} else if lStart >= 0 {
				// Compare the current line with the stop word.
				line := p.litBs[lStart:]
				if r != utf8.RuneSelf && len(line) > 0 {
					line = line[:len(line)-1] // minus trailing character
				}
				if bytes.Equal(line, stop) {
					p.tok = _LitWord
					p.val = p.endLit()[:lStart]
					if p.val == "" {
						p.tok = _Newl
					}
					p.hdocStops[len(p.hdocStops)-1] = nil
					return
				}
			}
			if r != '\n' {
				return // hit an unexpected EOF or closing backquote
			}
			if p.quote == hdocBodyTabs {
				for p.peekByte('\t') {
					p.rune()
				}
			}
			lStart = len(p.litBs)
		}
	}
}

func (p *Parser) quotedHdocWord() *Word {
	r := p.r
	p.newLit(r)
	pos := p.nextPos()
	stop := p.hdocStops[len(p.hdocStops)-1]
	for ; ; r = p.rune() {
		if r == utf8.RuneSelf {
			return nil
		}
		if p.quote == hdocBodyTabs {
			for r == '\t' {
				r = p.rune()
			}
		}
		lStart := len(p.litBs) - 1
	runeLoop:
		for {
			switch r {
			case utf8.RuneSelf, '\n':
				break runeLoop
			case '`':
				if p.backquoteEnd() {
					break runeLoop
				}
			case escNewl:
				p.litBs = append(p.litBs, '\\', '\n')
				break runeLoop
			}
			r = p.rune()
		}
		if lStart < 0 {
			continue
		}
		// Compare the current line with the stop word.
		line := p.litBs[lStart:]
		if r != utf8.RuneSelf && len(line) > 0 {
			line = line[:len(line)-1] // minus \n
		}
		if bytes.Equal(line, stop) {
			p.hdocStops[len(p.hdocStops)-1] = nil
			val := p.endLit()[:lStart]
			if val == "" {
				return nil
			}
			return p.wordOne(p.lit(pos, val))
		}
	}
}

func (p *Parser) advanceLitRe(r rune) {
	for p.newLit(r); ; r = p.rune() {
		switch r {
		case '\\':
			p.rune()
		case '(':
			p.rxOpenParens++
		case ')':
			if p.rxOpenParens--; p.rxOpenParens < 0 {
				p.tok, p.val = _LitWord, p.endLit()
				p.quote = noState
				return
			}
		case ' ', '\t', '\r', '\n', ';', '&', '>', '<':
			if p.rxOpenParens <= 0 {
				p.tok, p.val = _LitWord, p.endLit()
				p.quote = noState
				return
			}
		case '"', '\'', '$', '`':
			p.tok, p.val = _Lit, p.endLit()
			return
		case utf8.RuneSelf:
			p.tok, p.val = _LitWord, p.endLit()
			p.quote = noState
			return
		}
	}
}

func testUnaryOp(val string) UnTestOperator {
	switch val {
	case "!":
		return TsNot
	case "-e", "-a":
		return TsExists
	case "-f":
		return TsRegFile
	case "-d":
		return TsDirect
	case "-c":
		return TsCharSp
	case "-b":
		return TsBlckSp
	case "-p":
		return TsNmPipe
	case "-S":
		return TsSocket
	case "-L", "-h":
		return TsSmbLink
	case "-k":
		return TsSticky
	case "-g":
		return TsGIDSet
	case "-u":
		return TsUIDSet
	case "-G":
		return TsGrpOwn
	case "-O":
		return TsUsrOwn
	case "-N":
		return TsModif
	case "-r":
		return TsRead
	case "-w":
		return TsWrite
	case "-x":
		return TsExec
	case "-s":
		return TsNoEmpty
	case "-t":
		return TsFdTerm
	case "-z":
		return TsEmpStr
	case "-n":
		return TsNempStr
	case "-o":
		return TsOptSet
	case "-v":
		return TsVarSet
	case "-R":
		return TsRefVar
	default:
		return 0
	}
}

func testBinaryOp(val string) BinTestOperator {
	switch val {
	case "=":
		return TsMatchShort
	case "==":
		return TsMatch
	case "!=":
		return TsNoMatch
	case "=~":
		return TsReMatch
	case "-nt":
		return TsNewer
	case "-ot":
		return TsOlder
	case "-ef":
		return TsDevIno
	case "-eq":
		return TsEql
	case "-ne":
		return TsNeq
	case "-le":
		return TsLeq
	case "-ge":
		return TsGeq
	case "-lt":
		return TsLss
	case "-gt":
		return TsGtr
	default:
		return 0
	}
}
07070100000052000081A4000000000000000000000001686AE5C00000669B000000000000000000000000000000000000001A00000000sh-3.12.0/syntax/nodes.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"math"
	"strconv"
	"strings"
)

// 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].
	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].
	End() Pos
}

// File represents a shell source file.
type File struct {
	Name string

	Stmts []*Stmt
	Last  []Comment
}

func (f *File) Pos() Pos { return stmtsPos(f.Stmts, f.Last) }
func (f *File) End() Pos { return stmtsEnd(f.Stmts, f.Last) }

func stmtsPos(stmts []*Stmt, last []Comment) Pos {
	if len(stmts) > 0 {
		s := stmts[0]
		sPos := s.Pos()
		if len(s.Comments) > 0 {
			if cPos := s.Comments[0].Pos(); sPos.After(cPos) {
				return cPos
			}
		}
		return sPos
	}
	if len(last) > 0 {
		return last[0].Pos()
	}
	return Pos{}
}

func stmtsEnd(stmts []*Stmt, last []Comment) Pos {
	if len(last) > 0 {
		return last[len(last)-1].End()
	}
	if len(stmts) > 0 {
		s := stmts[len(stmts)-1]
		sEnd := s.End()
		if len(s.Comments) > 0 {
			if cEnd := s.Comments[0].End(); cEnd.After(sEnd) {
				return cEnd
			}
		}
		return sEnd
	}
	return Pos{}
}

// Pos is a position within a shell source file.
type Pos struct {
	offs, lineCol uint32
}

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

	colBitSize = 32 - lineBitSize
	colMax     = (1 << colBitSize) - 1
	colBitMask = colMax
)

// TODO(v4): consider using uint32 for Offset/Line/Col to better represent bit sizes.
// Or go with int64, which more closely resembles portable "sizes" elsewhere.
// The latter is probably nicest, as then we can change the number of internal
// bits later, and we can also do overflow checks for the user in NewPos.

// NewPos creates a position with the given offset, line, and column.
//
// 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 "?"
	}
	if column > colMax {
		column = 0 // protect against overflows; rendered as "?"
	}
	return Pos{
		offs:    uint32(offset),
		lineCol: (uint32(line) << colBitSize) | uint32(column),
	}
}

// Offset returns the byte offset of the position in the original source file.
// Byte offsets start at 0. Invalid positions always report the offset 0.
//
// 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].
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. 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].
func (p Pos) Col() uint { return uint(p.lineCol & colBitMask) }

func (p Pos) String() string {
	var b strings.Builder
	if line := p.Line(); line > 0 {
		b.WriteString(strconv.FormatUint(uint64(line), 10))
	} else {
		b.WriteByte('?')
	}
	b.WriteByte(':')
	if col := p.Col(); col > 0 {
		b.WriteString(strconv.FormatUint(uint64(col), 10))
	} else {
		b.WriteByte('?')
	}
	return b.String()
}

// 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().
// 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)
	return p
}

func posMax(p1, p2 Pos) Pos {
	if p2.After(p1) {
		return p2
	}
	return p1
}

// Comment represents a single comment on a single line.
type Comment struct {
	Hash Pos
	Text string
}

func (c *Comment) Pos() Pos { return c.Hash }
func (c *Comment) End() Pos { return posAddCol(c.Hash, 1+len(c.Text)) }

// Stmt represents a statement, also known as a "complete command". It is
// compromised of a command and other components that may come before or after
// it.
type Stmt struct {
	Comments   []Comment
	Cmd        Command
	Position   Pos
	Semicolon  Pos  // position of ';', '&', or '|&', if any
	Negated    bool // ! stmt
	Background bool // stmt &
	Coprocess  bool // mksh's |&

	Redirs []*Redirect // stmt >a <b
}

func (s *Stmt) Pos() Pos { return s.Position }
func (s *Stmt) End() Pos {
	if s.Semicolon.IsValid() {
		end := posAddCol(s.Semicolon, 1) // ';' or '&'
		if s.Coprocess {
			end = posAddCol(end, 1) // '|&'
		}
		return end
	}
	end := s.Position
	if s.Negated {
		end = posAddCol(end, 1)
	}
	if s.Cmd != nil {
		end = s.Cmd.End()
	}
	if len(s.Redirs) > 0 {
		end = posMax(end, s.Redirs[len(s.Redirs)-1].End())
	}
	return end
}

// Command represents all nodes that are simple or compound commands, including
// function declarations.
//
// These are [*CallExpr], [*IfClause], [*WhileClause], [*ForClause], [*CaseClause],
// [*Block], [*Subshell], [*BinaryCmd], [*FuncDecl], [*ArithmCmd], [*TestClause],
// [*DeclClause], [*LetClause], [*TimeClause], and [*CoprocClause].
type Command interface {
	Node
	commandNode()
}

func (*CallExpr) commandNode()     {}
func (*IfClause) commandNode()     {}
func (*WhileClause) commandNode()  {}
func (*ForClause) commandNode()    {}
func (*CaseClause) commandNode()   {}
func (*Block) commandNode()        {}
func (*Subshell) commandNode()     {}
func (*BinaryCmd) commandNode()    {}
func (*FuncDecl) commandNode()     {}
func (*ArithmCmd) commandNode()    {}
func (*TestClause) commandNode()   {}
func (*DeclClause) commandNode()   {}
func (*LetClause) commandNode()    {}
func (*TimeClause) commandNode()   {}
func (*CoprocClause) commandNode() {}
func (*TestDecl) commandNode()     {}

// Assign represents an assignment to a variable.
//
// Here and elsewhere, Index can mean either an index expression into an indexed
// array, or a string key into an associative array.
//
// If Index is non-nil, the value will be a word and not an array as nested
// arrays are not allowed.
//
// If Naked is true and Name is nil, the assignment is part of a [DeclClause] and
// the argument (in the Value field) will be evaluated at run-time. This
// includes parameter expansions, which may expand to assignments or options.
type Assign struct {
	Append bool       // +=
	Naked  bool       // without '='
	Name   *Lit       // must be a valid name
	Index  ArithmExpr // [i], ["k"]
	Value  *Word      // =val
	Array  *ArrayExpr // =(arr)
}

func (a *Assign) Pos() Pos {
	if a.Name == nil {
		return a.Value.Pos()
	}
	return a.Name.Pos()
}

func (a *Assign) End() Pos {
	if a.Value != nil {
		return a.Value.End()
	}
	if a.Array != nil {
		return a.Array.End()
	}
	if a.Index != nil {
		return posAddCol(a.Index.End(), 2)
	}
	if a.Naked {
		return a.Name.End()
	}
	return posAddCol(a.Name.End(), 1)
}

// Redirect represents an input/output redirection.
type Redirect struct {
	OpPos Pos
	Op    RedirOperator
	N     *Lit  // fd>, or {varname}> in Bash
	Word  *Word // >word
	Hdoc  *Word // here-document body
}

func (r *Redirect) Pos() Pos {
	if r.N != nil {
		return r.N.Pos()
	}
	return r.OpPos
}

func (r *Redirect) End() Pos {
	if r.Hdoc != nil {
		return r.Hdoc.End()
	}
	return r.Word.End()
}

// CallExpr represents a command execution or function call, otherwise known as
// a "simple command".
//
// If Args is empty, Assigns apply to the shell environment. Otherwise, they are
// variables that cannot be arrays and which only apply to the call.
type CallExpr struct {
	Assigns []*Assign // a=x b=y args
	Args    []*Word
}

func (c *CallExpr) Pos() Pos {
	if len(c.Assigns) > 0 {
		return c.Assigns[0].Pos()
	}
	return c.Args[0].Pos()
}

func (c *CallExpr) End() Pos {
	if len(c.Args) == 0 {
		return c.Assigns[len(c.Assigns)-1].End()
	}
	return c.Args[len(c.Args)-1].End()
}

// Subshell represents a series of commands that should be executed in a nested
// shell environment.
type Subshell struct {
	Lparen, Rparen Pos

	Stmts []*Stmt
	Last  []Comment
}

func (s *Subshell) Pos() Pos { return s.Lparen }
func (s *Subshell) End() Pos { return posAddCol(s.Rparen, 1) }

// Block represents a series of commands that should be executed in a nested
// scope. It is essentially a list of statements within curly braces.
type Block struct {
	Lbrace, Rbrace Pos

	Stmts []*Stmt
	Last  []Comment
}

func (b *Block) Pos() Pos { return b.Lbrace }
func (b *Block) End() Pos { return posAddCol(b.Rbrace, 1) }

// IfClause represents an if statement.
type IfClause struct {
	Position Pos // position of the starting "if", "elif", or "else" token
	ThenPos  Pos // position of "then", empty if this is an "else"
	FiPos    Pos // position of "fi", shared with .Else if non-nil

	Cond     []*Stmt
	CondLast []Comment
	Then     []*Stmt
	ThenLast []Comment

	Else *IfClause // if non-nil, an "elif" or an "else"

	Last []Comment // comments on the first "elif", "else", or "fi"
}

func (c *IfClause) Pos() Pos { return c.Position }
func (c *IfClause) End() Pos { return posAddCol(c.FiPos, 2) }

// WhileClause represents a while or an until clause.
type WhileClause struct {
	WhilePos, DoPos, DonePos Pos
	Until                    bool

	Cond     []*Stmt
	CondLast []Comment
	Do       []*Stmt
	DoLast   []Comment
}

func (w *WhileClause) Pos() Pos { return w.WhilePos }
func (w *WhileClause) End() Pos { return posAddCol(w.DonePos, 4) }

// ForClause represents a for or a select clause. The latter is only present in
// Bash.
type ForClause struct {
	ForPos, DoPos, DonePos Pos
	Select                 bool
	Braces                 bool // deprecated form with { } instead of do/done
	Loop                   Loop

	Do     []*Stmt
	DoLast []Comment
}

func (f *ForClause) Pos() Pos { return f.ForPos }
func (f *ForClause) End() Pos { return posAddCol(f.DonePos, 4) }

// Loop holds either [*WordIter] or [*CStyleLoop].
type Loop interface {
	Node
	loopNode()
}

func (*WordIter) loopNode()   {}
func (*CStyleLoop) loopNode() {}

// WordIter represents the iteration of a variable over a series of words in a
// for clause. If InPos is an invalid position, the "in" token was missing, so
// the iteration is over the shell's positional parameters.
type WordIter struct {
	Name  *Lit
	InPos Pos // position of "in"
	Items []*Word
}

func (w *WordIter) Pos() Pos { return w.Name.Pos() }
func (w *WordIter) End() Pos {
	if len(w.Items) > 0 {
		return wordLastEnd(w.Items)
	}
	return posMax(w.Name.End(), posAddCol(w.InPos, 2))
}

// CStyleLoop represents the behavior of a for clause similar to the C
// language.
//
// This node will only appear with [LangBash].
type CStyleLoop struct {
	Lparen, Rparen Pos
	// Init, Cond, Post can each be nil, if the for loop construct omits it.
	Init, Cond, Post ArithmExpr
}

func (c *CStyleLoop) Pos() Pos { return c.Lparen }
func (c *CStyleLoop) End() Pos { return posAddCol(c.Rparen, 2) }

// BinaryCmd represents a binary expression between two statements.
type BinaryCmd struct {
	OpPos Pos
	Op    BinCmdOperator
	X, Y  *Stmt
}

func (b *BinaryCmd) Pos() Pos { return b.X.Pos() }
func (b *BinaryCmd) End() Pos { return b.Y.End() }

// FuncDecl represents the declaration of a function.
type FuncDecl struct {
	Position Pos
	RsrvWord bool // non-posix "function f" style
	Parens   bool // with () parentheses, only meaningful with RsrvWord=true
	Name     *Lit
	Body     *Stmt
}

func (f *FuncDecl) Pos() Pos { return f.Position }
func (f *FuncDecl) End() Pos { return f.Body.End() }

// Word represents a shell word, containing one or more word parts contiguous to
// each other. The word is delimited by word boundaries, such as spaces,
// newlines, semicolons, or parentheses.
type Word struct {
	Parts []WordPart
}

func (w *Word) Pos() Pos { return w.Parts[0].Pos() }
func (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() }

// Lit returns the word as a literal value, if the word consists of [*Lit] nodes
// only. An empty string is returned otherwise. Words with multiple literals,
// which can appear in some edge cases, are handled properly.
//
// For example, the word "foo" will return "foo", but the word "foo${bar}" will
// return "".
func (w *Word) Lit() string {
	// In the usual case, we'll have either a single part that's a literal,
	// or one of the parts being a non-literal. Using strings.Join instead
	// of a strings.Builder avoids extra work in these cases, since a single
	// part is a shortcut, and many parts don't incur string copies.
	lits := make([]string, 0, 1)
	for _, part := range w.Parts {
		lit, ok := part.(*Lit)
		if !ok {
			return ""
		}
		lits = append(lits, lit.Value)
	}
	return strings.Join(lits, "")
}

// WordPart represents all nodes that can form part of a word.
//
// These are [*Lit], [*SglQuoted], [*DblQuoted], [*ParamExp], [*CmdSubst], [*ArithmExp],
// [*ProcSubst], and [*ExtGlob].
type WordPart interface {
	Node
	wordPartNode()
}

func (*Lit) wordPartNode()       {}
func (*SglQuoted) wordPartNode() {}
func (*DblQuoted) wordPartNode() {}
func (*ParamExp) wordPartNode()  {}
func (*CmdSubst) wordPartNode()  {}
func (*ArithmExp) wordPartNode() {}
func (*ProcSubst) wordPartNode() {}
func (*ExtGlob) wordPartNode()   {}
func (*BraceExp) wordPartNode()  {}

// Lit represents a string literal.
//
// Note that a parsed string literal may not appear as-is in the original source
// code, as it is possible to split literals by escaping newlines. The splitting
// is lost, but the end position is not.
type Lit struct {
	ValuePos, ValueEnd Pos
	Value              string
}

func (l *Lit) Pos() Pos { return l.ValuePos }
func (l *Lit) End() Pos { return l.ValueEnd }

// SglQuoted represents a string within single quotes.
type SglQuoted struct {
	Left, Right Pos
	Dollar      bool // $''
	Value       string
}

func (q *SglQuoted) Pos() Pos { return q.Left }
func (q *SglQuoted) End() Pos { return posAddCol(q.Right, 1) }

// DblQuoted represents a list of nodes within double quotes.
type DblQuoted struct {
	Left, Right Pos
	Dollar      bool // $""
	Parts       []WordPart
}

func (q *DblQuoted) Pos() Pos { return q.Left }
func (q *DblQuoted) End() Pos { return posAddCol(q.Right, 1) }

// CmdSubst represents a command substitution.
type CmdSubst struct {
	Left, Right Pos

	Stmts []*Stmt
	Last  []Comment

	Backquotes bool // deprecated `foo`
	TempFile   bool // mksh's ${ foo;}
	ReplyVar   bool // mksh's ${|foo;}
}

func (c *CmdSubst) Pos() Pos { return c.Left }
func (c *CmdSubst) End() Pos { return posAddCol(c.Right, 1) }

// ParamExp represents a parameter expansion.
type ParamExp struct {
	Dollar, Rbrace Pos

	Short  bool // $a instead of ${a}
	Excl   bool // ${!a}
	Length bool // ${#a}
	Width  bool // ${%a}
	Param  *Lit
	Index  ArithmExpr       // ${a[i]}, ${a["k"]}
	Slice  *Slice           // ${a:x:y}
	Repl   *Replace         // ${a/x/y}
	Names  ParNamesOperator // ${!prefix*} or ${!prefix@}
	Exp    *Expansion       // ${a:-b}, ${a#b}, etc
}

func (p *ParamExp) Pos() Pos { return p.Dollar }
func (p *ParamExp) End() Pos {
	if !p.Short {
		return posAddCol(p.Rbrace, 1)
	}
	if p.Index != nil {
		return posAddCol(p.Index.End(), 1)
	}
	return p.Param.End()
}

func (p *ParamExp) nakedIndex() bool {
	return p.Short && p.Index != nil
}

// Slice represents a character slicing expression inside a [ParamExp].
//
// This node will only appear with [LangBash] and [LangMirBSDKorn].
type Slice struct {
	Offset, Length ArithmExpr
}

// Replace represents a search and replace expression inside a [ParamExp].
type Replace struct {
	All        bool
	Orig, With *Word
}

// Expansion represents string manipulation in a [ParamExp] other than those
// covered by [Replace].
type Expansion struct {
	Op   ParExpOperator
	Word *Word
}

// ArithmExp represents an arithmetic expansion.
type ArithmExp struct {
	Left, Right Pos
	Bracket     bool // deprecated $[expr] form
	Unsigned    bool // mksh's $((# expr))

	X ArithmExpr
}

func (a *ArithmExp) Pos() Pos { return a.Left }
func (a *ArithmExp) End() Pos {
	if a.Bracket {
		return posAddCol(a.Right, 1)
	}
	return posAddCol(a.Right, 2)
}

// ArithmCmd represents an arithmetic command.
//
// This node will only appear with [LangBash] and [LangMirBSDKorn].
type ArithmCmd struct {
	Left, Right Pos
	Unsigned    bool // mksh's ((# expr))

	X ArithmExpr
}

func (a *ArithmCmd) Pos() Pos { return a.Left }
func (a *ArithmCmd) End() Pos { return posAddCol(a.Right, 2) }

// ArithmExpr represents all nodes that form arithmetic expressions.
//
// These are [*BinaryArithm], [*UnaryArithm], [*ParenArithm], and [*Word].
type ArithmExpr interface {
	Node
	arithmExprNode()
}

func (*BinaryArithm) arithmExprNode() {}
func (*UnaryArithm) arithmExprNode()  {}
func (*ParenArithm) arithmExprNode()  {}
func (*Word) arithmExprNode()         {}

// BinaryArithm represents a binary arithmetic expression.
//
// If Op is any assign operator, X will be a word with a single [*Lit] whose value
// is a valid name.
//
// Ternary operators like "a ? b : c" are fit into this structure. Thus, if
// Op==[TernQuest], Y will be a [*BinaryArithm] with Op==[TernColon].
// [TernColon] does not appear in any other scenario.
type BinaryArithm struct {
	OpPos Pos
	Op    BinAritOperator
	X, Y  ArithmExpr
}

func (b *BinaryArithm) Pos() Pos { return b.X.Pos() }
func (b *BinaryArithm) End() Pos { return b.Y.End() }

// UnaryArithm represents an unary arithmetic expression. The unary operator
// may come before or after the sub-expression.
//
// If Op is [Inc] or [Dec], X will be a word with a single [*Lit] whose value is a
// valid name.
type UnaryArithm struct {
	OpPos Pos
	Op    UnAritOperator
	Post  bool
	X     ArithmExpr
}

func (u *UnaryArithm) Pos() Pos {
	if u.Post {
		return u.X.Pos()
	}
	return u.OpPos
}

func (u *UnaryArithm) End() Pos {
	if u.Post {
		return posAddCol(u.OpPos, 2)
	}
	return u.X.End()
}

// ParenArithm represents an arithmetic expression within parentheses.
type ParenArithm struct {
	Lparen, Rparen Pos

	X ArithmExpr
}

func (p *ParenArithm) Pos() Pos { return p.Lparen }
func (p *ParenArithm) End() Pos { return posAddCol(p.Rparen, 1) }

// CaseClause represents a case (switch) clause.
type CaseClause struct {
	Case, In, Esac Pos
	Braces         bool // deprecated mksh form with braces instead of in/esac

	Word  *Word
	Items []*CaseItem
	Last  []Comment
}

func (c *CaseClause) Pos() Pos { return c.Case }
func (c *CaseClause) End() Pos { return posAddCol(c.Esac, 4) }

// CaseItem represents a pattern list (case) within a [CaseClause].
type CaseItem struct {
	Op       CaseOperator
	OpPos    Pos // unset if it was finished by "esac"
	Comments []Comment
	Patterns []*Word

	Stmts []*Stmt
	Last  []Comment
}

func (c *CaseItem) Pos() Pos { return c.Patterns[0].Pos() }
func (c *CaseItem) End() Pos {
	if c.OpPos.IsValid() {
		return posAddCol(c.OpPos, len(c.Op.String()))
	}
	return stmtsEnd(c.Stmts, c.Last)
}

// TestClause represents a Bash extended test clause.
//
// This node will only appear with [LangBash] and [LangMirBSDKorn].
type TestClause struct {
	Left, Right Pos

	X TestExpr
}

func (t *TestClause) Pos() Pos { return t.Left }
func (t *TestClause) End() Pos { return posAddCol(t.Right, 2) }

// TestExpr represents all nodes that form test expressions.
//
// These are [*BinaryTest], [*UnaryTest], [*ParenTest], and [*Word].
type TestExpr interface {
	Node
	testExprNode()
}

func (*BinaryTest) testExprNode() {}
func (*UnaryTest) testExprNode()  {}
func (*ParenTest) testExprNode()  {}
func (*Word) testExprNode()       {}

// BinaryTest represents a binary test expression.
type BinaryTest struct {
	OpPos Pos
	Op    BinTestOperator
	X, Y  TestExpr
}

func (b *BinaryTest) Pos() Pos { return b.X.Pos() }
func (b *BinaryTest) End() Pos { return b.Y.End() }

// UnaryTest represents a unary test expression. The unary operator may come
// before or after the sub-expression.
type UnaryTest struct {
	OpPos Pos
	Op    UnTestOperator
	X     TestExpr
}

func (u *UnaryTest) Pos() Pos { return u.OpPos }
func (u *UnaryTest) End() Pos { return u.X.End() }

// ParenTest represents a test expression within parentheses.
type ParenTest struct {
	Lparen, Rparen Pos

	X TestExpr
}

func (p *ParenTest) Pos() Pos { return p.Lparen }
func (p *ParenTest) End() Pos { return posAddCol(p.Rparen, 1) }

// DeclClause represents a Bash declare clause.
//
// Args can contain a mix of regular and naked assignments. The naked
// assignments can represent either options or variable names.
//
// This node will only appear with [LangBash].
type DeclClause struct {
	// Variant is one of "declare", "local", "export", "readonly",
	// "typeset", or "nameref".
	Variant *Lit
	Args    []*Assign
}

func (d *DeclClause) Pos() Pos { return d.Variant.Pos() }
func (d *DeclClause) End() Pos {
	if len(d.Args) > 0 {
		return d.Args[len(d.Args)-1].End()
	}
	return d.Variant.End()
}

// ArrayExpr represents a Bash array expression.
//
// This node will only appear with [LangBash].
type ArrayExpr struct {
	Lparen, Rparen Pos

	Elems []*ArrayElem
	Last  []Comment
}

func (a *ArrayExpr) Pos() Pos { return a.Lparen }
func (a *ArrayExpr) End() Pos { return posAddCol(a.Rparen, 1) }

// ArrayElem represents a Bash array element.
//
// Index can be nil; for example, declare -a x=(value).
// Value can be nil; for example, declare -A x=([index]=).
// Finally, neither can be nil; for example, declare -A x=([index]=value)
type ArrayElem struct {
	Index    ArithmExpr
	Value    *Word
	Comments []Comment
}

func (a *ArrayElem) Pos() Pos {
	if a.Index != nil {
		return a.Index.Pos()
	}
	return a.Value.Pos()
}

func (a *ArrayElem) End() Pos {
	if a.Value != nil {
		return a.Value.End()
	}
	return posAddCol(a.Index.Pos(), 1)
}

// ExtGlob represents a Bash extended globbing expression. Note that these are
// parsed independently of whether shopt has been called or not.
//
// This node will only appear with [LangBash] and [LangMirBSDKorn].
type ExtGlob struct {
	OpPos   Pos
	Op      GlobOperator
	Pattern *Lit
}

func (e *ExtGlob) Pos() Pos { return e.OpPos }
func (e *ExtGlob) End() Pos { return posAddCol(e.Pattern.End(), 1) }

// ProcSubst represents a Bash process substitution.
//
// This node will only appear with [LangBash].
type ProcSubst struct {
	OpPos, Rparen Pos
	Op            ProcOperator

	Stmts []*Stmt
	Last  []Comment
}

func (s *ProcSubst) Pos() Pos { return s.OpPos }
func (s *ProcSubst) End() Pos { return posAddCol(s.Rparen, 1) }

// TimeClause represents a Bash time clause. PosixFormat corresponds to the -p
// flag.
//
// This node will only appear with [LangBash] and [LangMirBSDKorn].
type TimeClause struct {
	Time        Pos
	PosixFormat bool
	Stmt        *Stmt
}

func (c *TimeClause) Pos() Pos { return c.Time }
func (c *TimeClause) End() Pos {
	if c.Stmt == nil {
		return posAddCol(c.Time, 4)
	}
	return c.Stmt.End()
}

// CoprocClause represents a Bash coproc clause.
//
// This node will only appear with [LangBash].
type CoprocClause struct {
	Coproc Pos
	Name   *Word
	Stmt   *Stmt
}

func (c *CoprocClause) Pos() Pos { return c.Coproc }
func (c *CoprocClause) End() Pos { return c.Stmt.End() }

// LetClause represents a Bash let clause.
//
// This node will only appear with [LangBash] and [LangMirBSDKorn].
type LetClause struct {
	Let   Pos
	Exprs []ArithmExpr
}

func (l *LetClause) Pos() Pos { return l.Let }
func (l *LetClause) End() Pos { return l.Exprs[len(l.Exprs)-1].End() }

// BraceExp represents a Bash brace expression, such as "{a,f}" or "{1..10}".
//
// This node will only appear as a result of [SplitBraces].
type BraceExp struct {
	Sequence bool // {x..y[..incr]} instead of {x,y[,...]}
	Elems    []*Word
}

func (b *BraceExp) Pos() Pos {
	return posAddCol(b.Elems[0].Pos(), -1)
}

func (b *BraceExp) End() Pos {
	return posAddCol(wordLastEnd(b.Elems), 1)
}

// TestDecl represents the declaration of a Bats test function.
type TestDecl struct {
	Position    Pos
	Description *Word
	Body        *Stmt
}

func (f *TestDecl) Pos() Pos { return f.Position }
func (f *TestDecl) End() Pos { return f.Body.End() }

func wordLastEnd(ws []*Word) Pos {
	if len(ws) == 0 {
		return Pos{}
	}
	return ws[len(ws)-1].End()
}
07070100000053000081A4000000000000000000000001686AE5C0000006A9000000000000000000000000000000000000001F00000000sh-3.12.0/syntax/nodes_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"fmt"
	"strings"
	"testing"
)

func TestPosition(t *testing.T) {
	t.Parallel()
	parserBash := NewParser(KeepComments(true))
	parserPosix := NewParser(KeepComments(true), Variant(LangPOSIX))
	parserMirBSD := NewParser(KeepComments(true), Variant(LangMirBSDKorn))
	parserBats := NewParser(KeepComments(true), Variant(LangBats))
	for i, c := range fileTests {
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("%03d-%d", i, j), func(t *testing.T) {
				parser := parserPosix
				if c.Bats != nil {
					parser = parserBats
				} else if c.Bash != nil {
					parser = parserBash
				} else if c.MirBSDKorn != nil {
					parser = parserMirBSD
				}
				prog, err := parser.Parse(strings.NewReader(in), "")
				if err != nil {
					t.Fatalf("Unexpected error in %q: %v", in, err)
				}
				v := &posWalker{
					t:     t,
					f:     prog,
					lines: strings.Split(in, "\n"),
				}
				Walk(prog, v.Visit)
			})
		}
	}
}

type posWalker struct {
	t     *testing.T
	f     *File
	lines []string
}

func (v *posWalker) Visit(n Node) bool {
	if n == nil {
		return true
	}
	p := n.Pos()
	if !p.IsValid() && len(v.f.Stmts) > 0 {
		v.t.Fatalf("Invalid Pos")
	}
	if c, ok := n.(*Comment); ok {
		if v.f.Pos().After(c.Pos()) {
			v.t.Fatalf("A Comment is before its File")
		}
		if c.End().After(v.f.End()) {
			v.t.Fatalf("A Comment is after its File")
		}
	}
	return true
}

func TestWeirdOperatorString(t *testing.T) {
	t.Parallel()
	op := RedirOperator(1000)
	want := "token(1000)"
	if got := op.String(); got != want {
		t.Fatalf("token.String() mismatch: want %s, got %s", want, got)
	}
}
07070100000054000081A4000000000000000000000001686AE5C00000F1D1000000000000000000000000000000000000001B00000000sh-3.12.0/syntax/parser.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"fmt"
	"io"
	"iter"
	"slices"
	"strconv"
	"strings"
	"unicode/utf8"
)

// ParserOption is a function which can be passed to NewParser
// to alter its behavior. To apply option to existing Parser
// call it directly, for example KeepComments(true)(parser).
type ParserOption func(*Parser)

// KeepComments makes the parser parse comments and attach them to
// nodes, as opposed to discarding them.
func KeepComments(enabled bool) ParserOption {
	return func(p *Parser) { p.keepComments = enabled }
}

// LangVariant describes a shell language variant to use when tokenizing and
// parsing shell code. The zero value is [LangBash].
type LangVariant int

const (
	// LangBash corresponds to the GNU Bash language, as described in its
	// manual at https://www.gnu.org/software/bash/manual/bash.html.
	//
	// We currently follow Bash version 5.2.
	//
	// Its string representation is "bash".
	LangBash LangVariant = iota

	// LangPOSIX corresponds to the POSIX Shell language, as described at
	// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html.
	//
	// Its string representation is "posix" or "sh".
	LangPOSIX

	// LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as
	// mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm.
	// Note that it shares some features with Bash, due to the shared
	// ancestry that is ksh.
	//
	// We currently follow mksh version 59.
	//
	// Its string representation is "mksh".
	LangMirBSDKorn

	// LangBats corresponds to the Bash Automated Testing System language,
	// as described at https://github.com/bats-core/bats-core. Note that
	// it's just a small extension of the Bash language.
	//
	// Its string representation is "bats".
	LangBats

	// LangAuto corresponds to automatic language detection,
	// commonly used by end-user applications like shfmt,
	// which can guess a file's language variant given its filename or shebang.
	//
	// At this time, [Variant] does not support LangAuto.
	LangAuto
)

// Variant changes the shell language variant that the parser will
// accept.
//
// The passed language variant must be one of the constant values defined in
// this package.
func Variant(l LangVariant) ParserOption {
	switch l {
	case LangBash, LangPOSIX, LangMirBSDKorn, LangBats:
	case LangAuto:
		panic("LangAuto is not supported by the parser at this time")
	default:
		panic(fmt.Sprintf("unknown shell language variant: %d", l))
	}
	return func(p *Parser) { p.lang = l }
}

func (l LangVariant) String() string {
	switch l {
	case LangBash:
		return "bash"
	case LangPOSIX:
		return "posix"
	case LangMirBSDKorn:
		return "mksh"
	case LangBats:
		return "bats"
	case LangAuto:
		return "auto"
	}
	return "unknown shell language variant"
}

func (l *LangVariant) Set(s string) error {
	switch s {
	case "bash":
		*l = LangBash
	case "posix", "sh":
		*l = LangPOSIX
	case "mksh":
		*l = LangMirBSDKorn
	case "bats":
		*l = LangBats
	case "auto":
		*l = LangAuto
	default:
		return fmt.Errorf("unknown shell language variant: %q", s)
	}
	return nil
}

func (l LangVariant) isBash() bool {
	return l == LangBash || l == LangBats
}

// StopAt configures the lexer to stop at an arbitrary word, treating it
// as if it were the end of the input. It can contain any characters
// except whitespace, and cannot be over four bytes in size.
//
// This can be useful to embed shell code within another language, as
// one can use a special word to mark the delimiters between the two.
//
// As a word, it will only apply when following whitespace or a
// separating token. For example, StopAt("$$") will act on the inputs
// "foo $$" and "foo;$$", but not on "foo '$$'".
//
// The match is done by prefix, so the example above will also act on
// "foo $$bar".
func StopAt(word string) ParserOption {
	if len(word) > 4 {
		panic("stop word can't be over four bytes in size")
	}
	if strings.ContainsAny(word, " \t\n\r") {
		panic("stop word can't contain whitespace characters")
	}
	return func(p *Parser) { p.stopAt = []byte(word) }
}

// RecoverErrors allows the parser to skip up to a maximum number of
// errors in the given input on a best-effort basis.
// This can be useful to tab-complete an interactive shell prompt,
// or when providing diagnostics on slightly incomplete shell source.
//
// Currently, this only helps with mandatory tokens from the shell grammar
// which are not present in the input. They result in position fields
// or nodes whose position report [Pos.IsRecovered] as true.
//
// For example, given the input
//
//	(foo |
//
// the result will contain two recovered positions; first, the pipe requires
// a statement to follow, and as [Stmt.Pos] reports, the entire node is recovered.
// Second, the subshell needs to be closed, so [Subshell.Rparen] is recovered.
func RecoverErrors(maximum int) ParserOption {
	return func(p *Parser) { p.recoverErrorsMax = maximum }
}

// NewParser allocates a new [Parser] and applies any number of options.
func NewParser(options ...ParserOption) *Parser {
	p := &Parser{}
	for _, opt := range options {
		opt(p)
	}
	return p
}

// Parse reads and parses a shell program with an optional name. It
// returns the parsed program if no issues were encountered. Otherwise,
// an error is returned. Reads from r are buffered.
//
// Parse can be called more than once, but not concurrently. That is, a
// Parser can be reused once it is done working.
func (p *Parser) Parse(r io.Reader, name string) (*File, error) {
	p.reset()
	p.f = &File{Name: name}
	p.src = r
	p.rune()
	p.next()
	p.f.Stmts, p.f.Last = p.stmtList()
	if p.err == nil {
		// EOF immediately after heredoc word so no newline to
		// trigger it
		p.doHeredocs()
	}
	return p.f, p.err
}

// Stmts reads and parses statements 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.
func (p *Parser) Stmts(r io.Reader, fn func(*Stmt) bool) error {
	p.reset()
	p.f = &File{}
	p.src = r
	p.rune()
	p.next()
	p.stmts(fn)
	if p.err == nil {
		// EOF immediately after heredoc word so no newline to
		// trigger it
		p.doHeredocs()
	}
	return p.err
}

type wrappedReader struct {
	*Parser
	io.Reader

	lastLine    int64
	accumulated []*Stmt
	fn          func([]*Stmt) bool
}

func (w *wrappedReader) Read(p []byte) (n int, err error) {
	// If we lexed a newline for the first time, we just finished a line, so
	// we may need to give a callback for the edge cases below not covered
	// by Parser.Stmts.
	if (w.r == '\n' || w.r == escNewl) && w.line > w.lastLine {
		if w.Incomplete() {
			// Incomplete statement; call back to print "> ".
			if !w.fn(w.accumulated) {
				return 0, io.EOF
			}
		} else if len(w.accumulated) == 0 {
			// Nothing was parsed; call back to print another "$ ".
			if !w.fn(nil) {
				return 0, io.EOF
			}
		}
		w.lastLine = w.line
	}
	return w.Reader.Read(p)
}

// Interactive implements what is necessary to parse statements in an
// interactive shell. The parser will call the given function under two
// circumstances outlined below.
//
// If a line containing any number of statements is parsed, the function will be
// 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.
//
// 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
//	}
//
// 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
			}
			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 is a pre-iterators API which now wraps [Parser.WordsSeq].
func (p *Parser) Words(r io.Reader, fn func(*Word) bool) error {
	for w, err := range p.WordsSeq(r) {
		if err != nil {
			return err
		}
		if !fn(w) {
			break
		}
	}
	return nil
}

// WordsSeq reads and parses a sequence of words alongside any error encountered.
//
// Newlines are skipped, meaning that multi-line input will work fine. If the
// parser encounters a token that isn't a word, such as a semicolon, an error
// will be returned.
//
// 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) WordsSeq(r io.Reader) iter.Seq2[*Word, error] {
	p.reset()
	p.f = &File{}
	p.src = r
	return func(yield func(*Word, error) bool) {
		p.rune()
		p.next()
		for {
			p.got(_Newl)
			w := p.getWord()
			if w == nil {
				if p.tok != _EOF {
					p.curErr("%s is not a valid word", p.tok)
				}
				if p.err != nil {
					yield(nil, p.err)
				}
				return
			}
			if !yield(w, nil) {
				return
			}
		}
	}
}

// Document parses a single here-document word. That is, it parses the input as
// if they were lines following a <<EOF redirection.
//
// In practice, this is the same as parsing the input as if it were within
// double quotes, but without having to escape all double quote characters.
// Similarly, the here-document word parsed here cannot be ended by any
// delimiter other than reaching the end of the input.
func (p *Parser) Document(r io.Reader) (*Word, error) {
	p.reset()
	p.f = &File{}
	p.src = r
	p.rune()
	p.quote = hdocBody
	p.hdocStops = [][]byte{[]byte("MVDAN_CC_SH_SYNTAX_EOF")}
	p.parsingDoc = true
	p.next()
	w := p.getWord()
	return w, p.err
}

// Arithmetic parses a single arithmetic expression. That is, as if the input
// were within the $(( and )) tokens.
func (p *Parser) Arithmetic(r io.Reader) (ArithmExpr, error) {
	p.reset()
	p.f = &File{}
	p.src = r
	p.rune()
	p.quote = arithmExpr
	p.next()
	expr := p.arithmExpr(false)
	return expr, p.err
}

// Parser holds the internal state of the parsing mechanism of a
// program.
type Parser struct {
	src io.Reader
	bs  []byte // current chunk of read bytes
	bsp uint   // pos within chunk for the rune after r; uint helps eliminate bounds checks
	r   rune   // next rune
	w   int    // width of r

	f *File

	spaced bool // whether tok has whitespace on its left

	err     error // lexer/parser error
	readErr error // got a read error, but bytes left

	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 int64

	pos Pos // position of tok

	quote   quoteState // current lexer state
	eqlOffs int        // position of '=' in val (a literal)

	keepComments bool
	lang         LangVariant

	stopAt []byte

	recoveredErrors  int
	recoverErrorsMax int

	forbidNested bool

	// list of pending heredoc bodies
	buriedHdocs int
	heredocs    []*Redirect

	hdocStops [][]byte // stack of end words for open heredocs

	parsingDoc bool // true if using Parser.Document

	// 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

	rxOpenParens int
	rxFirstPart  bool

	accComs []Comment
	curComs *[]Comment

	litBatch  []Lit
	wordBatch []wordAlloc

	readBuf [bufSize]byte
	litBuf  [bufSize]byte
	litBs   []byte
}

// 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 [Parser.Interactive].
func (p *Parser) Incomplete() bool {
	// If there are any open nodes, we need to finish them.
	// If we're constructing a literal, we need to finish it.
	return p.openNodes > 0 || len(p.litBs) > 0
}

const bufSize = 1 << 10

func (p *Parser) reset() {
	p.tok, p.val = illegalTok, ""
	p.eqlOffs = 0
	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.quote, p.forbidNested = noState, false
	p.openNodes = 0
	p.recoveredErrors = 0
	p.heredocs, p.buriedHdocs = p.heredocs[:0], 0
	p.hdocStops = nil
	p.parsingDoc = false
	p.openBquotes = 0
	p.accComs = nil
	p.accComs, p.curComs = nil, &p.accComs
	p.litBatch = nil
	p.wordBatch = nil
	p.litBs = nil
}

func (p *Parser) nextPos() Pos {
	// 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.line <= lineMax {
		line = uint(p.line)
	}
	if p.col <= colMax {
		col = uint(p.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, 32)
	}
	l := &p.litBatch[0]
	p.litBatch = p.litBatch[1:]
	l.ValuePos = pos
	l.ValueEnd = p.nextPos()
	l.Value = val
	return l
}

type wordAlloc struct {
	word  Word
	parts [1]WordPart
}

func (p *Parser) wordAnyNumber() *Word {
	if len(p.wordBatch) == 0 {
		p.wordBatch = make([]wordAlloc, 32)
	}
	alloc := &p.wordBatch[0]
	p.wordBatch = p.wordBatch[1:]
	w := &alloc.word
	w.Parts = p.wordParts(alloc.parts[:0])
	return w
}

func (p *Parser) wordOne(part WordPart) *Word {
	if len(p.wordBatch) == 0 {
		p.wordBatch = make([]wordAlloc, 32)
	}
	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 {
	var alloc struct {
		ce CallExpr
		ws [4]*Word
	}
	ce := &alloc.ce
	ce.Args = alloc.ws[:1]
	ce.Args[0] = w
	return ce
}

//go:generate stringer -type=quoteState

type quoteState uint32

const (
	noState quoteState = 1 << iota
	subCmd
	subCmdBckquo
	dblQuotes
	hdocWord
	hdocBody
	hdocBodyTabs
	arithmExpr
	arithmExprLet
	arithmExprCmd
	arithmExprBrack
	testExpr
	testExprRegexp
	switchCase
	paramExpName
	paramExpSlice
	paramExpRepl
	paramExpExp
	arrayElems

	allKeepSpaces = paramExpRepl | dblQuotes | hdocBody |
		hdocBodyTabs | paramExpExp
	allRegTokens = noState | subCmd | subCmdBckquo | hdocWord |
		switchCase | arrayElems | testExpr
	allArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd |
		arithmExprBrack | paramExpSlice
	allParamReg = paramExpName | paramExpSlice
	allParamExp = allParamReg | paramExpRepl | paramExpExp | arithmExprBrack
)

type saveState struct {
	quote       quoteState
	buriedHdocs int
}

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
}

func (p *Parser) postNested(s saveState) {
	p.quote, p.buriedHdocs = s.quote, s.buriedHdocs
}

func (p *Parser) unquotedWordBytes(w *Word) ([]byte, bool) {
	buf := make([]byte, 0, 4)
	didUnquote := false
	for _, wp := range w.Parts {
		buf, didUnquote = p.unquotedWordPart(buf, wp, false)
	}
	return buf, didUnquote
}

func (p *Parser) unquotedWordPart(buf []byte, wp WordPart, quotes bool) (_ []byte, quoted bool) {
	switch wp := wp.(type) {
	case *Lit:
		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 = append(buf, b)
			}
		}
	case *SglQuoted:
		buf = append(buf, []byte(wp.Value)...)
		quoted = true
	case *DblQuoted:
		for _, wp2 := range wp.Parts {
			buf, _ = p.unquotedWordPart(buf, wp2, true)
		}
		quoted = true
	}
	return buf, quoted
}

func (p *Parser) doHeredocs() {
	hdocs := p.heredocs[p.buriedHdocs:]
	if len(hdocs) == 0 {
		// Nothing do do; don't even issue a read.
		return
	}
	p.rune() // consume '\n', since we know p.tok == _Newl
	old := p.quote
	p.heredocs = p.heredocs[:p.buriedHdocs]
	for i, r := range hdocs {
		if p.err != nil {
			break
		}
		p.quote = hdocBody
		if r.Op == DashHdoc {
			p.quote = hdocBodyTabs
		}
		stop, quoted := p.unquotedWordBytes(r.Word)
		p.hdocStops = append(p.hdocStops, stop)
		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 = int64(r.Hdoc.End().Line())
		}
		if lastLine < p.line {
			// TODO: It seems like this triggers more often than it
			// should. Look into it.
			l := p.lit(p.nextPos(), "")
			if r.Hdoc == nil {
				r.Hdoc = p.wordOne(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.hdocStops = p.hdocStops[:len(p.hdocStops)-1]
	}
	p.quote = old
}

func (p *Parser) got(tok token) bool {
	if p.tok == tok {
		p.next()
		return true
	}
	return false
}

func (p *Parser) gotRsrv(val string) (Pos, bool) {
	pos := p.pos
	if p.tok == _LitWord && p.val == val {
		p.next()
		return pos, true
	}
	return pos, false
}

func (p *Parser) recoverError() bool {
	if p.recoveredErrors < p.recoverErrorsMax {
		p.recoveredErrors++
		return true
	}
	return false
}

func readableStr(s string) string {
	// don't quote tokens like & or }
	if s != "" && s[0] >= 'a' && s[0] <= 'z' {
		return strconv.Quote(s)
	}
	return s
}

func (p *Parser) followErr(pos Pos, left, right string) {
	leftStr := readableStr(left)
	p.posErr(pos, "%s must be followed by %s", leftStr, right)
}

func (p *Parser) followErrExp(pos Pos, left string) {
	p.followErr(pos, left, "an expression")
}

func (p *Parser) follow(lpos Pos, left string, tok token) {
	if !p.got(tok) {
		p.followErr(lpos, left, tok.String())
	}
}

func (p *Parser) followRsrv(lpos Pos, left, val string) Pos {
	pos, ok := p.gotRsrv(val)
	if !ok {
		if p.recoverError() {
			return recoveredPos
		}
		p.followErr(lpos, left, fmt.Sprintf("%q", val))
	}
	return pos
}

func (p *Parser) followStmts(left string, lpos Pos, stops ...string) ([]*Stmt, []Comment) {
	if p.got(semicolon) {
		return nil, nil
	}
	newLine := p.got(_Newl)
	stmts, last := p.stmtList(stops...)
	if len(stmts) < 1 && !newLine {
		if p.recoverError() {
			return []*Stmt{{Position: recoveredPos}}, nil
		}
		p.followErr(lpos, left, "a statement list")
	}
	return stmts, last
}

func (p *Parser) followWordTok(tok token, pos Pos) *Word {
	w := p.getWord()
	if w == nil {
		if p.recoverError() {
			return p.wordOne(&Lit{ValuePos: recoveredPos})
		}
		p.followErr(pos, tok.String(), "a word")
	}
	return w
}

func (p *Parser) stmtEnd(n Node, start, end string) Pos {
	pos, ok := p.gotRsrv(end)
	if !ok {
		if p.recoverError() {
			return recoveredPos
		}
		p.posErr(n.Pos(), "%s statement must end with %q", start, end)
	}
	return pos
}

func (p *Parser) quoteErr(lpos Pos, quote token) {
	p.posErr(lpos, "reached %s without closing quote %s",
		p.tok.String(), quote)
}

func (p *Parser) matchingErr(lpos Pos, left, right any) {
	p.posErr(lpos, "reached %s without matching %s with %s",
		p.tok.String(), 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
}

func (p *Parser) errPass(err error) {
	if p.err == nil {
		p.err = err
		p.bsp = uint(len(p.bs)) + 1
		p.r = utf8.RuneSelf
		p.w = 1
		p.tok = _EOF
	}
}

// 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
// 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.
func IsKeyword(word string) bool {
	// This list has been copied from the bash 5.1 source code, file y.tab.c +4460
	switch word {
	case
		"!",
		"[[", // only if COND_COMMAND is defined
		"]]", // only if COND_COMMAND is defined
		"case",
		"coproc", // only if COPROCESS_SUPPORT is defined
		"do",
		"done",
		"else",
		"esac",
		"fi",
		"for",
		"function",
		"if",
		"in",
		"select", // only if SELECT_COMMAND is defined
		"then",
		"time", // only if COMMAND_TIMING is defined
		"until",
		"while",
		"{",
		"}":
		return true
	}
	return false
}

// ParseError represents an error found when parsing a source file, from which
// the parser cannot recover.
type ParseError struct {
	Filename 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: %s", e.Filename, e.Pos.String(), e.Text)
}

// LangError is returned when the parser encounters code that is only valid in
// other shell language variants. The error includes what feature is not present
// in the current language variant, and what languages support it.
type LangError struct {
	Filename string
	Pos      Pos

	// Feature briefly describes which language feature caused the error.
	Feature string
	// 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 sb strings.Builder
	if e.Filename != "" {
		sb.WriteString(e.Filename + ":")
	}
	sb.WriteString(e.Pos.String() + ": ")
	sb.WriteString(e.Feature)
	if strings.HasSuffix(e.Feature, "s") {
		sb.WriteString(" are a ")
	} else {
		sb.WriteString(" is a ")
	}
	for i, lang := range e.Langs {
		if i > 0 {
			sb.WriteString("/")
		}
		sb.WriteString(lang.String())
	}
	sb.WriteString(" feature; tried parsing as ")
	sb.WriteString(e.LangUsed.String())
	return sb.String()
}

func (p *Parser) posErr(pos Pos, format string, a ...any) {
	p.errPass(ParseError{
		Filename:   p.f.Name,
		Pos:        pos,
		Text:       fmt.Sprintf(format, a...),
		Incomplete: p.tok == _EOF && p.Incomplete(),
	})
}

func (p *Parser) curErr(format string, a ...any) {
	p.posErr(p.pos, format, a...)
}

func (p *Parser) langErr(pos Pos, feature string, langs ...LangVariant) {
	p.errPass(LangError{
		Filename: p.f.Name,
		Pos:      pos,
		Feature:  feature,
		Langs:    langs,
		LangUsed: p.lang,
	})
}

func (p *Parser) stmts(fn func(*Stmt) bool, stops ...string) {
	gotEnd := true
loop:
	for p.tok != _EOF {
		newLine := p.got(_Newl)
		switch p.tok {
		case _LitWord:
			for _, stop := range stops {
				if p.val == stop {
					break loop
				}
			}
		case rightParen:
			if p.quote == subCmd {
				break loop
			}
		case bckQuote:
			if p.backquoteEnd() {
				break loop
			}
		case dblSemicolon, semiAnd, dblSemiAnd, semiOr:
			if p.quote == switchCase {
				break loop
			}
			p.curErr("%s can only be used in a case clause", p.tok)
		}
		if !newLine && !gotEnd {
			p.curErr("statements must be separated by &, ; or a newline")
		}
		if p.tok == _EOF {
			break
		}
		p.openNodes++
		s := p.getStmt(true, false, false)
		p.openNodes--
		if s == nil {
			p.invalidStmtStart()
			break
		}
		gotEnd = s.Semicolon.IsValid()
		if !fn(s) {
			break
		}
	}
}

func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) {
	var stmts []*Stmt
	var last []Comment
	fn := func(s *Stmt) bool {
		stmts = append(stmts, s)
		return true
	}
	p.stmts(fn, stops...)
	split := len(p.accComs)
	if p.tok == _LitWord && (p.val == "elif" || p.val == "else" || p.val == "fi") {
		// Split the comments, so that any aligned with an opening token
		// get attached to it. For example:
		//
		//     if foo; then
		//         # inside the body
		//     # document the else
		//     else
		//     fi
		// TODO(mvdan): look into deduplicating this with similar logic
		// in caseItems.
		for i, c := range slices.Backward(p.accComs) {
			if c.Pos().Col() != p.pos.Col() {
				break
			}
			split = i
		}
	}
	if split > 0 { // keep last nil if empty
		last = p.accComs[:split]
	}
	p.accComs = p.accComs[split:]
	return stmts, last
}

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 rightParen:
		p.curErr("%s can only be used to close a subshell", p.tok)
	default:
		p.curErr("%s is not a valid start for a statement", p.tok)
	}
}

func (p *Parser) getWord() *Word {
	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:
		l := p.lit(p.pos, p.val)
		p.next()
		return l
	}
	return nil
}

func (p *Parser) wordParts(wps []WordPart) []WordPart {
	for {
		p.openNodes++
		n := p.wordPart()
		p.openNodes--
		if n == nil {
			if len(wps) == 0 {
				return nil // normalize empty lists into nil
			}
			return wps
		}
		wps = append(wps, n)
		if p.spaced {
			return wps
		}
	}
}

func (p *Parser) ensureNoNested() {
	if p.forbidNested {
		p.curErr("expansions not allowed in heredoc words")
	}
}

func (p *Parser) wordPart() WordPart {
	switch p.tok {
	case _Lit, _LitWord, _LitRedir:
		l := p.lit(p.pos, p.val)
		p.next()
		return l
	case dollBrace:
		p.ensureNoNested()
		switch p.r {
		case '|':
			if p.lang != LangMirBSDKorn {
				p.langErr(p.pos, `"${|stmts;}"`, LangMirBSDKorn)
			}
			fallthrough
		case ' ', '\t', '\n':
			if p.lang != LangMirBSDKorn {
				p.langErr(p.pos, `"${ stmts;}"`, LangMirBSDKorn)
			}
			cs := &CmdSubst{
				Left:     p.pos,
				TempFile: p.r != '|',
				ReplyVar: p.r == '|',
			}
			old := p.preNested(subCmd)
			p.rune() // don't tokenize '|'
			p.next()
			cs.Stmts, cs.Last = p.stmtList("}")
			p.postNested(old)
			pos, ok := p.gotRsrv("}")
			if !ok {
				p.matchingErr(cs.Left, "${", "}")
			}
			cs.Right = pos
			return cs
		default:
			return p.paramExp()
		}
	case dollDblParen, dollBrack:
		p.ensureNoNested()
		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)
		}
		p.next()
		if p.got(hash) {
			if p.lang != LangMirBSDKorn {
				p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn)
			}
			ar.Unsigned = true
		}
		ar.X = p.followArithm(left, ar.Left)
		if ar.Bracket {
			if p.tok != rightBrack {
				p.arithmMatchingErr(ar.Left, dollBrack, rightBrack)
			}
			p.postNested(old)
			ar.Right = p.pos
			p.next()
		} else {
			ar.Right = p.arithmEnd(dollDblParen, ar.Left, old)
		}
		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
	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:
			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
		}
		return pe
	case cmdIn, cmdOut:
		p.ensureNoNested()
		ps := &ProcSubst{Op: ProcOperator(p.tok), OpPos: p.pos}
		old := p.preNested(subCmd)
		p.next()
		ps.Stmts, ps.Last = p.stmtList()
		p.postNested(old)
		ps.Rparen = p.matched(ps.OpPos, token(ps.Op), rightParen)
		return ps
	case sglQuote, dollSglQuote:
		sq := &SglQuoted{Left: p.pos, Dollar: p.tok == dollSglQuote}
		r := p.r
		for p.newLit(r); ; r = p.rune() {
			switch r {
			case '\\':
				if sq.Dollar {
					p.rune()
				}
			case '\'':
				sq.Right = p.nextPos()
				sq.Value = p.endLit()

				p.rune()
				p.next()
				return sq
			case escNewl:
				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
			}
		}
	case dblQuote, dollDblQuote:
		if p.quote == dblQuotes {
			// p.tok == dblQuote, as "foo$" puts $ in the lit
			return nil
		}
		return p.dblQuoted()
	case bckQuote:
		if p.backquoteEnd() {
			return nil
		}
		p.ensureNoNested()
		cs := &CmdSubst{Left: p.pos, Backquotes: true}
		old := p.preNested(subCmdBckquo)
		p.openBquotes++

		// The lexer didn't call p.rune for us, so that it could have
		// the right p.openBquotes to properly handle backslashes.
		p.rune()

		p.next()
		cs.Stmts, cs.Last = p.stmtList()
		if p.tok == bckQuote && p.lastBquoteEsc < p.openBquotes-1 {
			// e.g. found ` before the nested backquote \` was closed.
			p.tok = _EOF
			p.quoteErr(cs.Pos(), bckQuote)
		}
		p.postNested(old)
		p.openBquotes--
		cs.Right = p.pos

		// Like above, the lexer didn't call p.rune for us.
		p.rune()
		if !p.got(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)
		}
		eg := &ExtGlob{Op: GlobOperator(p.tok), OpPos: p.pos}
		lparens := 1
		r := p.r
	globLoop:
		for p.newLit(r); ; r = p.rune() {
			switch r {
			case utf8.RuneSelf:
				break globLoop
			case '(':
				lparens++
			case ')':
				if lparens--; lparens == 0 {
					break globLoop
				}
			}
		}
		eg.Pattern = p.lit(posAddCol(eg.OpPos, 2), p.endLit())
		p.rune()
		p.next()
		if lparens != 0 {
			p.matchingErr(eg.OpPos, eg.Op, rightParen)
		}
		return eg
	default:
		return nil
	}
}

func (p *Parser) dblQuoted() *DblQuoted {
	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(alloc.parts[:0])
	p.quote = old
	q.Right = p.pos
	if !p.got(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
}

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.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.langErr(pe.Pos(), `"${%foo}"`, LangMirBSDKorn)
		}
		if paramNameOp(p.r) {
			pe.Width = true
			p.next()
		}
	case exclMark:
		if paramNameOp(p.r) {
			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")
	}
	switch p.tok {
	case _Lit, _LitWord:
		p.curErr("%s cannot be followed by a word", op)
	case rightBrace:
		if pe.Excl && p.lang == LangPOSIX {
			p.langErr(pe.Pos(), `"${!foo}"`, LangBash, LangMirBSDKorn)
		}
		pe.Rbrace = p.pos
		p.quote = old
		p.next()
		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")
		}
		pe.Index = p.eitherIndex()
	}
	if p.tok == rightBrace {
		pe.Rbrace = p.pos
		p.quote = old
		p.next()
		return pe
	}
	if p.tok != _EOF && (pe.Length || pe.Width) {
		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)
		}
		pe.Repl = &Replace{All: p.tok == dblSlash}
		p.quote = paramExpRepl
		p.next()
		pe.Repl.Orig = p.getWord()
		p.quote = paramExpExp
		if p.got(slash) {
			pe.Repl.With = p.getWord()
		}
	case colon:
		// slicing
		if p.lang == LangPOSIX {
			p.langErr(p.pos, "slicing", LangBash, LangMirBSDKorn)
		}
		pe.Slice = &Slice{}
		colonPos := p.pos
		p.quote = paramExpSlice
		if p.next(); p.tok != colon {
			pe.Slice.Offset = p.followArithm(colon, colonPos)
		}
		colonPos = p.pos
		if p.got(colon) {
			pe.Slice.Length = p.followArithm(colon, colonPos)
		}
		// Need to use a different matched style so arithm errors
		// get reported correctly
		p.quote = old
		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)
		}
		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)
		case pe.Excl && p.r == '}':
			if !p.lang.isBash() {
				p.langErr(pe.Pos(), fmt.Sprintf(`"${!foo%s}"`, p.tok), LangBash)
			}
			pe.Names = ParNamesOperator(p.tok)
			p.next()
		default:
			pe.Exp = p.paramExpExp()
		}
	case plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn,
		perc, dblPerc, hash, dblHash:
		pe.Exp = p.paramExpExp()
	case _EOF:
	default:
		p.curErr("not a valid parameter expansion operator: %v", p.tok)
	}
	p.quote = old
	pe.Rbrace = p.matched(pe.Dollar, dollBrace, rightBrace)
	return pe
}

func (p *Parser) paramExpExp() *Expansion {
	op := ParExpOperator(p.tok)
	p.quote = paramExpExp
	p.next()
	if op == OtherParamOps {
		switch p.tok {
		case _Lit, _LitWord:
		default:
			p.curErr("@ expansion operator requires a literal")
		}
		switch p.val {
		case "a", "k", "u", "A", "E", "K", "L", "P", "U":
			if !p.lang.isBash() {
				p.langErr(p.pos, "this expansion operator", LangBash)
			}
		case "#":
			if p.lang != LangMirBSDKorn {
				p.langErr(p.pos, "this expansion operator", LangMirBSDKorn)
			}
		case "Q":
		default:
			p.curErr("invalid @ expansion operator %q", p.val)
		}
	}
	return &Expansion{Op: op, Word: p.getWord()}
}

func (p *Parser) eitherIndex() ArithmExpr {
	old := p.quote
	lpos := p.pos
	p.quote = arithmExprBrack
	p.next()
	if p.tok == star || p.tok == at {
		p.tok, p.val = _LitWord, p.tok.String()
	}
	expr := p.followArithm(leftBrack, lpos)
	p.quote = old
	p.matchedArithm(lpos, leftBrack, rightBrack)
	return expr
}

func (p *Parser) stopToken() bool {
	switch p.tok {
	case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, dblSemicolon,
		semiAnd, dblSemiAnd, semiOr, rightParen:
		return true
	case bckQuote:
		return p.backquoteEnd()
	}
	return false
}

func (p *Parser) backquoteEnd() bool {
	return p.lastBquoteEsc < p.openBquotes
}

// ValidName returns whether val is a valid name as per the POSIX spec.
func ValidName(val string) bool {
	if val == "" {
		return false
	}
	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':
		default:
			return false
		}
	}
	return true
}

func numberLiteral(val string) bool {
	for _, r := range val {
		if '0' > r || r > '9' {
			return false
		}
	}
	return true
}

func (p *Parser) hasValidIdent() bool {
	if p.tok != _Lit && p.tok != _LitWord {
		return false
	}
	if end := p.eqlOffs; end > 0 {
		if p.val[end-1] == '+' && p.lang != LangPOSIX {
			end-- // a+=x
		}
		if ValidName(p.val[:end]) {
			return true
		}
	} else if !ValidName(p.val) {
		return false // *[i]=x
	}
	return p.r == '[' // a[i]=x
}

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] == '+' {
			// a+=b
			as.Append = true
			nameEnd--
		}
		as.Name = p.lit(p.pos, p.val[:nameEnd])
		// since we're not using the entire p.val
		as.Name.ValueEnd = posAddCol(as.Name.ValuePos, nameEnd)
		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.wordOne(left)
		}
		p.next()
	} else { // foo[x]=bar
		as.Name = p.lit(p.pos, p.val)
		// hasValidIdent already checks p.r is '['
		p.rune()
		p.pos = posAddCol(p.pos, 1)
		as.Index = p.eitherIndex()
		if p.spaced || p.stopToken() {
			if needEqual {
				p.followErr(as.Pos(), "a[b]", "=")
			} else {
				as.Naked = true
				return as
			}
		}
		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]+", "=")
			} else {
				p.followErr(as.Pos(), "a[b]", "=")
			}
			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")
		}
		as.Array = &ArrayExpr{Lparen: p.pos}
		newQuote := p.quote
		if p.lang.isBash() {
			newQuote = arrayElems
		}
		old := p.preNested(newQuote)
		p.next()
		p.got(_Newl)
		for p.tok != _EOF && p.tok != rightParen {
			ae := &ArrayElem{}
			ae.Comments, p.accComs = p.accComs, nil
			if p.tok == leftBrack {
				left := p.pos
				ae.Index = p.eitherIndex()
				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:
					p.curErr("array element values must be words")
					return nil
				}
			}
			if len(p.accComs) > 0 {
				c := p.accComs[0]
				if c.Pos().Line() == ae.End().Line() {
					ae.Comments = append(ae.Comments, c)
					p.accComs = p.accComs[1:]
				}
			}
			as.Array.Elems = append(as.Array.Elems, ae)
			p.got(_Newl)
		}
		as.Array.Last, p.accComs = p.accComs, nil
		p.postNested(old)
		as.Array.Rparen = p.matched(as.Array.Lparen, leftParen, rightParen)
	} else if w := p.getWord(); w != nil {
		if as.Value == nil {
			as.Value = w
		} else {
			as.Value.Parts = append(as.Value.Parts, w.Parts...)
		}
	}
	return as
}

func (p *Parser) peekRedir() bool {
	switch p.tok {
	case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut,
		hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir:
		return true
	}
	return false
}

func (p *Parser) doRedirect(s *Stmt) {
	var r *Redirect
	if s.Redirs == nil {
		var alloc struct {
			redirs [4]*Redirect
			redir  Redirect
		}
		s.Redirs = alloc.redirs[:0]
		r = &alloc.redir
		s.Redirs = append(s.Redirs, r)
	} else {
		r = &Redirect{}
		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 p.lang == LangPOSIX && (p.tok == rdrAll || p.tok == appAll) {
		p.langErr(p.pos, "&> redirects", LangBash, LangMirBSDKorn)
	}
	r.Op, r.OpPos = RedirOperator(p.tok), p.pos
	p.next()
	switch r.Op {
	case Hdoc, DashHdoc:
		old := p.quote
		p.quote, p.forbidNested = hdocWord, true
		p.heredocs = append(p.heredocs, r)
		r.Word = p.followWordTok(token(r.Op), r.OpPos)
		p.quote, p.forbidNested = old, false
		if p.tok == _Newl {
			if len(p.accComs) > 0 {
				c := p.accComs[0]
				if c.Pos().Line() == s.End().Line() {
					s.Comments = append(s.Comments, c)
					p.accComs = p.accComs[1:]
				}
			}
			p.doHeredocs()
		}
	case WordHdoc:
		if p.lang == LangPOSIX {
			p.langErr(r.OpPos, "herestrings", LangBash, LangMirBSDKorn)
		}
		fallthrough
	default:
		r.Word = p.followWordTok(token(r.Op), r.OpPos)
	}
}

func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt {
	pos, ok := p.gotRsrv("!")
	s := &Stmt{Position: pos}
	if ok {
		s.Negated = true
		if p.stopToken() {
			p.posErr(s.Pos(), `"!" cannot form a statement alone`)
		}
		if _, ok := p.gotRsrv("!"); ok {
			p.posErr(s.Pos(), `cannot negate a command multiple times`)
		}
	}
	if s = p.gotStmtPipe(s, false); s == nil || p.err != nil {
		return nil
	}
	// instead of using recursion, iterate manually
	for p.tok == andAnd || p.tok == orOr {
		if binCmd {
			// left associativity: in a list of BinaryCmds, the
			// right recursion should only read a single element
			return s
		}
		b := &BinaryCmd{
			OpPos: p.pos,
			Op:    BinCmdOperator(p.tok),
			X:     s,
		}
		p.next()
		p.got(_Newl)
		b.Y = p.getStmt(false, true, false)
		if b.Y == nil || p.err != nil {
			if p.recoverError() {
				b.Y = &Stmt{Position: recoveredPos}
			} else {
				p.followErr(b.OpPos, b.Op.String(), "a statement")
				return nil
			}
		}
		s = &Stmt{Position: s.Position}
		s.Cmd = b
		s.Comments, b.X.Comments = b.X.Comments, nil
	}
	if readEnd {
		switch p.tok {
		case semicolon:
			s.Semicolon = p.pos
			p.next()
		case and:
			s.Semicolon = p.pos
			p.next()
			s.Background = true
		case orAnd:
			s.Semicolon = p.pos
			p.next()
			s.Coprocess = true
		}
	}
	if len(p.accComs) > 0 && !binCmd && !fnBody {
		c := p.accComs[0]
		if c.Pos().Line() == s.End().Line() {
			s.Comments = append(s.Comments, c)
			p.accComs = p.accComs[1:]
		}
	}
	return s
}

func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt {
	s.Comments, p.accComs = p.accComs, nil
	switch p.tok {
	case _LitWord:
		switch p.val {
		case "{":
			p.block(s)
		case "if":
			p.ifClause(s)
		case "while", "until":
			p.whileClause(s, p.val == "until")
		case "for":
			p.forClause(s)
		case "case":
			p.caseClause(s)
		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)
		case "fi":
			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)
		case "done":
			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)
		case "!":
			if !s.Negated {
				p.curErr(`"!" can only be used in full statements`)
				break
			}
		case "[[":
			if p.lang != LangPOSIX {
				p.testClause(s)
			}
		case "]]":
			if p.lang != LangPOSIX {
				p.curErr(`%q can only be used to close a test`, p.val)
			}
		case "let":
			if p.lang != LangPOSIX {
				p.letClause(s)
			}
		case "function":
			if p.lang != LangPOSIX {
				p.bashFuncDecl(s)
			}
		case "declare":
			if p.lang.isBash() { // Note that mksh lacks this one.
				p.declClause(s)
			}
		case "local", "export", "readonly", "typeset", "nameref":
			if p.lang != LangPOSIX {
				p.declClause(s)
			}
		case "time":
			if p.lang != LangPOSIX {
				p.timeClause(s)
			}
		case "coproc":
			if p.lang.isBash() { // Note that mksh lacks this one.
				p.coprocClause(s)
			}
		case "select":
			if p.lang != LangPOSIX {
				p.selectClause(s)
			}
		case "@test":
			if p.lang == LangBats {
				p.testDecl(s)
			}
		}
		if s.Cmd != nil {
			break
		}
		if p.hasValidIdent() {
			p.callExpr(s, nil, true)
			break
		}
		name := p.lit(p.pos, p.val)
		if p.next(); p.got(leftParen) {
			p.follow(name.ValuePos, "foo(", rightParen)
			if p.lang == LangPOSIX && !ValidName(name.Value) {
				p.posErr(name.Pos(), "invalid func name")
			}
			p.funcDecl(s, name, name.ValuePos, true)
		} else {
			p.callExpr(s, p.wordOne(name), 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
		}
		fallthrough
	case _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut,
		sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack,
		globQuest, globStar, globPlus, globAt, globExcl:
		if p.hasValidIdent() {
			p.callExpr(s, nil, true)
			break
		}
		w := p.wordAnyNumber()
		if p.got(leftParen) {
			p.posErr(w.Pos(), "invalid func name")
		}
		p.callExpr(s, w, false)
	case leftParen:
		p.subshell(s)
	case dblLeftParen:
		p.arithmExpCmd(s)
	default:
		if len(s.Redirs) == 0 {
			return nil
		}
	}
	for p.peekRedir() {
		p.doRedirect(s)
	}
	// instead of using recursion, iterate manually
	for p.tok == or || p.tok == orAnd {
		if binCmd {
			// left associativity: in a list of BinaryCmds, the
			// right recursion should only read a single element
			return s
		}
		if p.tok == orAnd && p.lang == LangMirBSDKorn {
			// No need to check for LangPOSIX, as on that language
			// we parse |& as two tokens.
			break
		}
		b := &BinaryCmd{OpPos: p.pos, Op: BinCmdOperator(p.tok), X: s}
		p.next()
		p.got(_Newl)
		if b.Y = p.gotStmtPipe(&Stmt{Position: p.pos}, true); b.Y == nil || p.err != nil {
			if p.recoverError() {
				b.Y = &Stmt{Position: recoveredPos}
			} else {
				p.followErr(b.OpPos, b.Op.String(), "a statement")
				break
			}
		}
		s = &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
		s.Negated = b.X.Negated
		b.X.Negated = false
	}
	return s
}

func (p *Parser) subshell(s *Stmt) {
	sub := &Subshell{Lparen: p.pos}
	old := p.preNested(subCmd)
	p.next()
	sub.Stmts, sub.Last = p.stmtList()
	p.postNested(old)
	sub.Rparen = p.matched(sub.Lparen, leftParen, rightParen)
	s.Cmd = sub
}

func (p *Parser) arithmExpCmd(s *Stmt) {
	ar := &ArithmCmd{Left: p.pos}
	old := p.preNested(arithmExprCmd)
	p.next()
	if p.got(hash) {
		if p.lang != LangMirBSDKorn {
			p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn)
		}
		ar.Unsigned = true
	}
	ar.X = p.followArithm(dblLeftParen, ar.Left)
	ar.Right = p.arithmEnd(dblLeftParen, ar.Left, old)
	s.Cmd = ar
}

func (p *Parser) block(s *Stmt) {
	b := &Block{Lbrace: p.pos}
	p.next()
	b.Stmts, b.Last = p.stmtList("}")
	if pos, ok := p.gotRsrv("}"); ok {
		b.Rbrace = pos
	} else if p.recoverError() {
		b.Rbrace = recoveredPos
	} else {
		p.matchingErr(b.Lbrace, "{", "}")
	}
	s.Cmd = b
}

func (p *Parser) ifClause(s *Stmt) {
	rootIf := &IfClause{Position: p.pos}
	p.next()
	rootIf.Cond, rootIf.CondLast = p.followStmts("if", rootIf.Position, "then")
	rootIf.ThenPos = p.followRsrv(rootIf.Position, "if <cond>", "then")
	rootIf.Then, rootIf.ThenLast = p.followStmts("then", rootIf.ThenPos, "fi", "elif", "else")
	curIf := rootIf
	for p.tok == _LitWord && p.val == "elif" {
		elf := &IfClause{Position: p.pos}
		curIf.Last = p.accComs
		p.accComs = nil
		p.next()
		elf.Cond, elf.CondLast = p.followStmts("elif", elf.Position, "then")
		elf.ThenPos = p.followRsrv(elf.Position, "elif <cond>", "then")
		elf.Then, elf.ThenLast = p.followStmts("then", elf.ThenPos, "fi", "elif", "else")
		curIf.Else = elf
		curIf = elf
	}
	if elsePos, ok := p.gotRsrv("else"); ok {
		curIf.Last = p.accComs
		p.accComs = nil
		els := &IfClause{Position: elsePos}
		els.Then, els.ThenLast = p.followStmts("else", els.Position, "fi")
		curIf.Else = els
		curIf = els
	}
	curIf.Last = p.accComs
	p.accComs = nil
	rootIf.FiPos = p.stmtEnd(rootIf, "if", "fi")
	for els := rootIf.Else; els != nil; els = els.Else {
		// All the nested IfClauses share the same FiPos.
		els.FiPos = rootIf.FiPos
	}
	s.Cmd = rootIf
}

func (p *Parser) whileClause(s *Stmt, until bool) {
	wc := &WhileClause{WhilePos: p.pos, Until: until}
	rsrv := "while"
	rsrvCond := "while <cond>"
	if wc.Until {
		rsrv = "until"
		rsrvCond = "until <cond>"
	}
	p.next()
	wc.Cond, wc.CondLast = p.followStmts(rsrv, wc.WhilePos, "do")
	wc.DoPos = p.followRsrv(wc.WhilePos, rsrvCond, "do")
	wc.Do, wc.DoLast = p.followStmts("do", wc.DoPos, "done")
	wc.DonePos = p.stmtEnd(wc, rsrv, "done")
	s.Cmd = wc
}

func (p *Parser) forClause(s *Stmt) {
	fc := &ForClause{ForPos: p.pos}
	p.next()
	fc.Loop = p.loop(fc.ForPos)

	start, end := "do", "done"
	if pos, ok := p.gotRsrv("{"); ok {
		if p.lang == LangPOSIX {
			p.langErr(pos, "for loops with braces", LangBash, LangMirBSDKorn)
		}
		fc.DoPos = pos
		fc.Braces = true
		start, end = "{", "}"
	} else {
		fc.DoPos = p.followRsrv(fc.ForPos, "for foo [in words]", start)
	}

	s.Comments = append(s.Comments, p.accComs...)
	p.accComs = nil
	fc.Do, fc.DoLast = p.followStmts(start, fc.DoPos, end)
	fc.DonePos = p.stmtEnd(fc, "for", end)
	s.Cmd = fc
}

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)
		}
	}
	if p.tok == dblLeftParen {
		cl := &CStyleLoop{Lparen: p.pos}
		old := p.preNested(arithmExprCmd)
		p.next()
		cl.Init = p.arithmExpr(false)
		if !p.got(dblSemicolon) {
			p.follow(p.pos, "expr", semicolon)
			cl.Cond = p.arithmExpr(false)
			p.follow(p.pos, "expr", semicolon)
		}
		cl.Post = p.arithmExpr(false)
		cl.Rparen = p.arithmEnd(dblLeftParen, cl.Lparen, old)
		p.got(semicolon)
		p.got(_Newl)
		return cl
	}
	return p.wordIter("for", fpos)
}

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")
	}
	if p.got(semicolon) {
		p.got(_Newl)
		return wi
	}
	p.got(_Newl)
	if pos, ok := p.gotRsrv("in"); ok {
		wi.InPos = pos
		for !p.stopToken() {
			if w := p.getWord(); w == nil {
				p.curErr("word list can only contain words")
			} else {
				wi.Items = append(wi.Items, w)
			}
		}
		p.got(semicolon)
		p.got(_Newl)
	} else if p.tok == _LitWord && p.val == "do" {
	} else {
		p.followErr(fpos, ftok+" foo", `"in", "do", ;, or a newline`)
	}
	return wi
}

func (p *Parser) selectClause(s *Stmt) {
	fc := &ForClause{ForPos: p.pos, Select: true}
	p.next()
	fc.Loop = p.wordIter("select", fc.ForPos)
	fc.DoPos = p.followRsrv(fc.ForPos, "select foo [in words]", "do")
	fc.Do, fc.DoLast = p.followStmts("do", fc.DoPos, "done")
	fc.DonePos = p.stmtEnd(fc, "select", "done")
	s.Cmd = fc
}

func (p *Parser) caseClause(s *Stmt) {
	cc := &CaseClause{Case: p.pos}
	p.next()
	cc.Word = p.getWord()
	if cc.Word == nil {
		p.followErr(cc.Case, "case", "a word")
	}
	end := "esac"
	p.got(_Newl)
	if pos, ok := p.gotRsrv("{"); ok {
		cc.In = pos
		cc.Braces = true
		if p.lang != LangMirBSDKorn {
			p.langErr(cc.Pos(), `"case i {"`, LangMirBSDKorn)
		}
		end = "}"
	} else {
		cc.In = p.followRsrv(cc.Case, "case x", "in")
	}
	cc.Items = p.caseItems(end)
	cc.Last, p.accComs = p.accComs, nil
	cc.Esac = p.stmtEnd(cc, "case", end)
	s.Cmd = cc
}

func (p *Parser) caseItems(stop string) (items []*CaseItem) {
	p.got(_Newl)
	for p.tok != _EOF && (p.tok != _LitWord || p.val != stop) {
		ci := &CaseItem{}
		ci.Comments, p.accComs = p.accComs, nil
		p.got(leftParen)
		for p.tok != _EOF {
			if w := p.getWord(); w == nil {
				p.curErr("case patterns must consist of words")
			} else {
				ci.Patterns = append(ci.Patterns, w)
			}
			if p.tok == rightParen {
				break
			}
			if !p.got(or) {
				p.curErr("case patterns must be separated with |")
			}
		}
		old := p.preNested(switchCase)
		p.next()
		ci.Stmts, ci.Last = p.stmtList(stop)
		p.postNested(old)
		switch p.tok {
		case dblSemicolon, semiAnd, dblSemiAnd, semiOr:
		default:
			ci.Op = Break
			items = append(items, ci)
			return
		}
		ci.Last = append(ci.Last, p.accComs...)
		p.accComs = nil
		ci.OpPos = p.pos
		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)
		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
}

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(false)
	if tc.X == nil {
		p.followErrExp(tc.Left, "[[")
	}
	tc.Right = p.pos
	if _, ok := p.gotRsrv("]]"); !ok {
		p.matchingErr(tc.Left, "[[", "]]")
	}
	p.postNested(old)
	s.Cmd = tc
}

func (p *Parser) testExpr(pastAndOr bool) TestExpr {
	p.got(_Newl)
	var left TestExpr
	if pastAndOr {
		left = p.testExprBase()
	} else {
		left = p.testExpr(true)
	}
	if left == nil {
		return left
	}
	p.got(_Newl)
	switch p.tok {
	case andAnd, orOr:
	case _LitWord:
		if p.val == "]]" {
			return left
		}
		if p.tok = token(testBinaryOp(p.val)); p.tok == illegalTok {
			p.curErr("not a valid test operator: %s", p.val)
		}
	case rdrIn, rdrOut:
	case _EOF, rightParen:
		return left
	case _Lit:
		p.curErr("test operator words must consist of a single literal")
	default:
		p.curErr("not a valid test operator: %v", 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(false); b.Y == nil {
			p.followErrExp(b.OpPos, b.Op.String())
		}
	case TsReMatch:
		if !p.lang.isBash() {
			p.langErr(p.pos, "regex tests", LangBash)
		}
		p.rxOpenParens = 0
		p.rxFirstPart = true
		// TODO(mvdan): Using nested states within a regex will break in
		// all sorts of ways. The better fix is likely to use a stop
		// token, like we do with heredocs.
		p.quote = testExprRegexp
		fallthrough
	default:
		if _, ok := b.X.(*Word); !ok {
			p.posErr(b.OpPos, "expected %s, %s or %s after complex expr",
				AndTest, OrTest, "]]")
		}
		p.next()
		b.Y = p.followWordTok(token(b.Op), b.OpPos)
	}
	p.quote = oldQuote
	return b
}

func (p *Parser) testExprBase() TestExpr {
	switch p.tok {
	case _EOF, rightParen:
		return nil
	case _LitWord:
		op := token(testUnaryOp(p.val))
		switch op {
		case illegalTok:
		case tsRefVar, tsModif: // not available in mksh
			if p.lang.isBash() {
				p.tok = op
			}
		default:
			p.tok = op
		}
	}
	switch p.tok {
	case exclMark:
		u := &UnaryTest{OpPos: p.pos, Op: TsNot}
		p.next()
		if u.X = p.testExpr(false); u.X == nil {
			p.followErrExp(u.OpPos, u.Op.String())
		}
		return u
	case tsExists, tsRegFile, tsDirect, tsCharSp, tsBlckSp, tsNmPipe,
		tsSocket, tsSmbLink, tsSticky, tsGIDSet, tsUIDSet, tsGrpOwn,
		tsUsrOwn, tsModif, tsRead, tsWrite, tsExec, tsNoEmpty,
		tsFdTerm, tsEmpStr, tsNempStr, tsOptSet, tsVarSet, tsRefVar:
		u := &UnaryTest{OpPos: p.pos, Op: UnTestOperator(p.tok)}
		p.next()
		u.X = p.followWordTok(token(u.Op), u.OpPos)
		return u
	case leftParen:
		pe := &ParenTest{Lparen: p.pos}
		p.next()
		if pe.X = p.testExpr(false); pe.X == nil {
			p.followErrExp(pe.Lparen, "(")
		}
		pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen)
		return pe
	case _LitWord:
		if p.val == "]]" {
			return nil
		}
		fallthrough
	default:
		if w := p.getWord(); w != nil {
			return w
		}
		// otherwise we'd return a typed nil above
		return nil
	}
}

func (p *Parser) declClause(s *Stmt) {
	ds := &DeclClause{Variant: p.lit(p.pos, p.val)}
	p.next()
	for !p.stopToken() && !p.peekRedir() {
		if p.hasValidIdent() {
			ds.Args = append(ds.Args, p.getAssign(false))
		} else if p.eqlOffs > 0 {
			p.curErr("invalid var name")
		} else if p.tok == _LitWord && ValidName(p.val) {
			ds.Args = append(ds.Args, &Assign{
				Naked: true,
				Name:  p.getLit(),
			})
		} else if w := p.getWord(); w != nil {
			ds.Args = append(ds.Args, &Assign{
				Naked: true,
				Value: w,
			})
		} else {
			p.followErr(p.pos, ds.Variant.Value, "names or assignments")
		}
	}
	s.Cmd = ds
}

func isBashCompoundCommand(tok token, val string) bool {
	switch tok {
	case leftParen, dblLeftParen:
		return true
	case _LitWord:
		switch val {
		case "{", "if", "while", "until", "for", "case", "[[",
			"coproc", "let", "function", "declare", "local",
			"export", "readonly", "typeset", "nameref":
			return true
		}
	}
	return false
}

func (p *Parser) timeClause(s *Stmt) {
	tc := &TimeClause{Time: p.pos}
	p.next()
	if _, ok := p.gotRsrv("-p"); ok {
		tc.PosixFormat = true
	}
	tc.Stmt = p.gotStmtPipe(&Stmt{Position: p.pos}, false)
	s.Cmd = tc
}

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(&Stmt{Position: p.pos}, false)
		s.Cmd = cc
		return
	}
	cc.Name = p.getWord()
	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 = &Stmt{Position: cc.Name.Pos()}
		cc.Stmt.Cmd = p.call(cc.Name)
		cc.Name = nil
	} else if cc.Name != nil {
		if call, ok := cc.Stmt.Cmd.(*CallExpr); ok {
			// name was in fact the start of a call
			call.Args = append([]*Word{cc.Name}, call.Args...)
			cc.Name = nil
		}
	}
	s.Cmd = cc
}

func (p *Parser) letClause(s *Stmt) {
	lc := &LetClause{Let: p.pos}
	old := p.preNested(arithmExprLet)
	p.next()
	for !p.stopToken() && !p.peekRedir() {
		x := p.arithmExpr(true)
		if x == nil {
			break
		}
		lc.Exprs = append(lc.Exprs, x)
	}
	if len(lc.Exprs) == 0 {
		p.followErrExp(lc.Let, "let")
	}
	p.postNested(old)
	s.Cmd = lc
}

func (p *Parser) bashFuncDecl(s *Stmt) {
	fpos := p.pos
	if p.next(); p.tok != _LitWord {
		p.followErr(fpos, "function", "a name")
	}
	name := p.lit(p.pos, p.val)
	hasParens := false
	if p.next(); p.got(leftParen) {
		hasParens = true
		p.follow(name.ValuePos, "foo(", rightParen)
	}
	p.funcDecl(s, name, fpos, hasParens)
}

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")
	}
	if td.Body = p.getStmt(false, false, true); td.Body == nil {
		p.followErr(td.Position, `@test "desc"`, "a statement")
	}
	s.Cmd = td
}

func (p *Parser) callExpr(s *Stmt, w *Word, assign bool) {
	ce := p.call(w)
	if w == nil {
		ce.Args = ce.Args[:0]
	}
	if assign {
		ce.Assigns = append(ce.Assigns, p.getAssign(true))
	}
loop:
	for {
		switch p.tok {
		case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd,
			dblSemicolon, semiAnd, dblSemiAnd, semiOr:
			break loop
		case _LitWord:
			if len(ce.Args) == 0 && p.hasValidIdent() {
				ce.Assigns = append(ce.Assigns, p.getAssign(true))
				break
			}
			// Avoid failing later with the confusing "} can only be used to close a block".
			if p.lang == LangPOSIX && p.val == "{" && w != nil && w.Lit() == "function" {
				p.langErr(p.pos, `the "function" builtin`, LangBash)
			}
			ce.Args = append(ce.Args, p.wordOne(p.lit(p.pos, p.val)))
			p.next()
		case _Lit:
			if len(ce.Args) == 0 && p.hasValidIdent() {
				ce.Assigns = append(ce.Assigns, p.getAssign(true))
				break
			}
			ce.Args = append(ce.Args, p.wordAnyNumber())
		case bckQuote:
			if p.backquoteEnd() {
				break loop
			}
			fallthrough
		case dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut,
			sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack,
			globQuest, globStar, globPlus, globAt, globExcl:
			ce.Args = append(ce.Args, p.wordAnyNumber())
		case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut,
			hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir:
			p.doRedirect(s)
		case dblLeftParen:
			p.curErr("%s can only be used to open an arithmetic cmd", p.tok)
		case rightParen:
			if p.quote == subCmd {
				break loop
			}
			fallthrough
		default:
			// Note that we'll only keep the first error that happens.
			if len(ce.Args) > 0 {
				if cmd := ce.Args[0].Lit(); p.lang == LangPOSIX && isBashCompoundCommand(_LitWord, cmd) {
					p.langErr(p.pos, fmt.Sprintf("the %q builtin", cmd), LangBash)
				}
			}
			p.curErr("a command can only contain words and redirects; encountered %s", p.tok)
		}
	}
	if len(ce.Assigns) == 0 && len(ce.Args) == 0 {
		return
	}
	if len(ce.Args) == 0 {
		ce.Args = nil
	} else {
		for _, asgn := range ce.Assigns {
			if asgn.Index != nil || asgn.Array != nil {
				p.posErr(asgn.Pos(), "inline variables cannot be arrays")
			}
		}
	}
	s.Cmd = ce
}

func (p *Parser) funcDecl(s *Stmt, name *Lit, pos Pos, withParens bool) {
	fd := &FuncDecl{
		Position: pos,
		RsrvWord: pos != name.ValuePos,
		Parens:   withParens,
		Name:     name,
	}
	p.got(_Newl)
	if fd.Body = p.getStmt(false, false, true); fd.Body == nil {
		p.followErr(fd.Pos(), "foo()", "a statement")
	}
	s.Cmd = fd
}
07070100000055000081A4000000000000000000000001686AE5C0000020DE000000000000000000000000000000000000002200000000sh-3.12.0/syntax/parser_arithm.gopackage syntax

// compact specifies whether we allow spaces between expressions.
// This is true for let
func (p *Parser) arithmExpr(compact bool) ArithmExpr {
	return p.arithmExprComma(compact)
}

// These function names are inspired by Bash's expr.c

func (p *Parser) arithmExprComma(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprAssign, Comma)
}

func (p *Parser) arithmExprAssign(compact bool) ArithmExpr {
	// Assign is different from the other binary operators because it's
	// right-associative and needs to check that it's placed after a name
	value := p.arithmExprTernary(compact)
	switch BinAritOperator(p.tok) {
	case AddAssgn, SubAssgn, MulAssgn, QuoAssgn, RemAssgn, AndAssgn,
		OrAssgn, XorAssgn, ShlAssgn, ShrAssgn, Assgn:
		if compact && p.spaced {
			return value
		}
		if !isArithName(value) {
			p.posErr(p.pos, "%s must follow a name", p.tok.String())
		}
		pos := p.pos
		tok := p.tok
		p.nextArithOp(compact)
		y := p.arithmExprAssign(compact)
		if y == nil {
			p.followErrExp(pos, tok.String())
		}
		return &BinaryArithm{
			OpPos: pos,
			Op:    BinAritOperator(tok),
			X:     value,
			Y:     y,
		}
	}
	return value
}

func (p *Parser) arithmExprTernary(compact bool) ArithmExpr {
	value := p.arithmExprLor(compact)
	if BinAritOperator(p.tok) != TernQuest || (compact && p.spaced) {
		return value
	}

	if value == nil {
		p.curErr("%s must follow an expression", p.tok.String())
	}
	questPos := p.pos
	p.nextArithOp(compact)
	if BinAritOperator(p.tok) == TernColon {
		p.followErrExp(questPos, TernQuest.String())
	}
	trueExpr := p.arithmExpr(compact)
	if trueExpr == nil {
		p.followErrExp(questPos, TernQuest.String())
	}
	if BinAritOperator(p.tok) != TernColon {
		p.posErr(questPos, "ternary operator missing : after ?")
	}
	colonPos := p.pos
	p.nextArithOp(compact)
	falseExpr := p.arithmExprTernary(compact)
	if falseExpr == nil {
		p.followErrExp(colonPos, TernColon.String())
	}
	return &BinaryArithm{
		OpPos: questPos,
		Op:    TernQuest,
		X:     value,
		Y: &BinaryArithm{
			OpPos: colonPos,
			Op:    TernColon,
			X:     trueExpr,
			Y:     falseExpr,
		},
	}
}

func (p *Parser) arithmExprLor(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprLand, OrArit)
}

func (p *Parser) arithmExprLand(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprBor, AndArit)
}

func (p *Parser) arithmExprBor(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprBxor, Or)
}

func (p *Parser) arithmExprBxor(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprBand, Xor)
}

func (p *Parser) arithmExprBand(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprEquality, And)
}

func (p *Parser) arithmExprEquality(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprComparison, Eql, Neq)
}

func (p *Parser) arithmExprComparison(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprShift, Lss, Gtr, Leq, Geq)
}

func (p *Parser) arithmExprShift(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprAddition, Shl, Shr)
}

func (p *Parser) arithmExprAddition(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprMultiplication, Add, Sub)
}

func (p *Parser) arithmExprMultiplication(compact bool) ArithmExpr {
	return p.arithmExprBinary(compact, p.arithmExprPower, Mul, Quo, Rem)
}

func (p *Parser) arithmExprPower(compact bool) ArithmExpr {
	// Power is different from the other binary operators because it's right-associative
	value := p.arithmExprUnary(compact)
	if BinAritOperator(p.tok) != Pow || (compact && p.spaced) {
		return value
	}

	if value == nil {
		p.curErr("%s must follow an expression", p.tok.String())
	}

	op := p.tok
	pos := p.pos
	p.nextArithOp(compact)
	y := p.arithmExprPower(compact)
	if y == nil {
		p.followErrExp(pos, op.String())
	}
	return &BinaryArithm{
		OpPos: pos,
		Op:    BinAritOperator(op),
		X:     value,
		Y:     y,
	}
}

func (p *Parser) arithmExprUnary(compact bool) ArithmExpr {
	if !compact {
		p.got(_Newl)
	}

	switch UnAritOperator(p.tok) {
	case Not, BitNegation, Plus, Minus:
		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())
		}
		return ue
	}
	return p.arithmExprValue(compact)
}

func (p *Parser) arithmExprValue(compact bool) ArithmExpr {
	var x ArithmExpr
	switch p.tok {
	case addAdd, subSub:
		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")
		}
		ue.X = p.arithmExprValue(compact)
		return ue
	case leftParen:
		pe := &ParenArithm{Lparen: p.pos}
		p.nextArithOp(compact)
		pe.X = p.followArithm(leftParen, pe.Lparen)
		pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen)
		x = pe
	case leftBrack:
		p.curErr("[ must follow a name")
	case colon:
		p.curErr("ternary operator missing ? before :")
	case _LitWord:
		l := p.getLit()
		if p.tok != leftBrack {
			x = p.wordOne(l)
			break
		}
		pe := &ParamExp{Dollar: l.ValuePos, Short: true, Param: l}
		pe.Index = p.eitherIndex()
		x = p.wordOne(pe)
	case bckQuote:
		if p.quote == arithmExprLet && p.openBquotes > 0 {
			return nil
		}
		fallthrough
	default:
		if w := p.getWord(); w != nil {
			x = w
		} else {
			return nil
		}
	}

	if compact && p.spaced {
		return x
	}
	if !compact {
		p.got(_Newl)
	}

	// we want real nil, not (*Word)(nil) as that
	// 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())
		}
		u := &UnaryArithm{
			Post:  true,
			OpPos: p.pos,
			Op:    UnAritOperator(p.tok),
			X:     x,
		}
		p.nextArith(compact)
		return u
	}
	return x
}

// nextArith consumes a token.
// It returns true if compact and the token was followed by spaces
func (p *Parser) nextArith(compact bool) bool {
	p.next()
	if compact && p.spaced {
		return true
	}
	if !compact {
		p.got(_Newl)
	}
	return false
}

func (p *Parser) nextArithOp(compact bool) {
	pos := p.pos
	tok := p.tok
	if p.nextArith(compact) {
		p.followErrExp(pos, tok.String())
	}
}

// arithmExprBinary is used for all left-associative binary operators
func (p *Parser) arithmExprBinary(compact bool, nextOp func(bool) ArithmExpr, operators ...BinAritOperator) ArithmExpr {
	value := nextOp(compact)
	for {
		var foundOp BinAritOperator
		for _, op := range operators {
			if p.tok == token(op) {
				foundOp = op
				break
			}
		}

		if token(foundOp) == illegalTok || (compact && p.spaced) {
			return value
		}

		if value == nil {
			p.curErr("%s must follow an expression", p.tok.String())
		}

		pos := p.pos
		p.nextArithOp(compact)
		y := nextOp(compact)
		if y == nil {
			p.followErrExp(pos, foundOp.String())
		}

		value = &BinaryArithm{
			OpPos: pos,
			Op:    foundOp,
			X:     value,
			Y:     y,
		}
	}
}

func isArithName(left ArithmExpr) bool {
	w, ok := left.(*Word)
	if !ok || len(w.Parts) != 1 {
		return false
	}
	switch wp := w.Parts[0].(type) {
	case *Lit:
		return ValidName(wp.Value)
	case *ParamExp:
		return wp.nakedIndex()
	default:
		return false
	}
}

func (p *Parser) followArithm(ftok token, fpos Pos) ArithmExpr {
	x := p.arithmExpr(false)
	if x == nil {
		p.followErrExp(fpos, ftok.String())
	}
	return x
}

func (p *Parser) peekArithmEnd() bool {
	return p.tok == rightParen && p.r == ')'
}

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)
	case leftBrack:
		p.curErr("[ must follow a name")
	case colon:
		p.curErr("ternary operator missing ? before :")
	case rightParen, _EOF:
		p.matchingErr(pos, left, right)
	default:
		if p.quote == arithmExpr {
			p.curErr("not a valid arithmetic operator: %v", p.tok)
		}
		p.matchingErr(pos, left, right)
	}
}

func (p *Parser) matchedArithm(lpos Pos, left, right token) {
	if !p.got(right) {
		p.arithmMatchingErr(lpos, left, right)
	}
}

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()
	p.postNested(old)
	pos := p.pos
	p.next()
	return pos
}
07070100000056000081A4000000000000000000000001686AE5C00000F102000000000000000000000000000000000000002000000000sh-3.12.0/syntax/parser_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"reflect"
	"regexp"
	"strings"
	"sync"
	"testing"

	"github.com/go-quicktest/qt"
	"github.com/google/go-cmp/cmp"
)

func TestParseBashKeepComments(t *testing.T) {
	t.Parallel()
	p := NewParser(KeepComments(true))
	for i, c := range fileTestsKeepComments {
		want := c.Bash
		if want == nil {
			continue
		}
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("#%03d-%d", i, j), singleParse(p, in, want))
		}
	}
}

func TestParseBash(t *testing.T) {
	t.Parallel()
	p := NewParser()
	for i, c := range append(fileTests, fileTestsNoPrint...) {
		want := c.Bash
		if want == nil {
			continue
		}
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("#%03d-%d", i, j), singleParse(p, in, want))
		}
	}
}

func TestParsePosOverflow(t *testing.T) {
	t.Parallel()

	// Consider using a custom reader to save memory.
	tests := []struct {
		name, in, want string
	}{
		{
			"LineOverflowIsValid",
			strings.Repeat("\n", lineMax) + "foo; bar",
			"<nil>",
		},
		{
			"LineOverflowPosString",
			strings.Repeat("\n", lineMax) + ")",
			"?:1: ) can only be used to close a subshell",
		},
		{
			"LineOverflowExtraPosString",
			strings.Repeat("\n", lineMax+5) + ")",
			"?:1: ) can only be used to close a subshell",
		},
		{
			"ColOverflowPosString",
			strings.Repeat(" ", colMax) + ")",
			"1:?: ) can only be used to close a subshell",
		},
		{
			"ColOverflowExtraPosString",
			strings.Repeat(" ", colMax) + ")",
			"1:?: ) can only be used to close a subshell",
		},
		{
			"ColOverflowSkippedPosString",
			strings.Repeat(" ", colMax+5) + "\n)",
			"2:1: ) can only be used to close a subshell",
		},
		{
			"LargestLineNumber",
			strings.Repeat("\n", lineMax-1) + ")",
			"262143:1: ) can only be used to close a subshell",
		},
		{
			"LargestColNumber",
			strings.Repeat(" ", colMax-1) + ")",
			"1:16383: ) can only be used to close a subshell",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			p := NewParser()
			_, err := p.Parse(strings.NewReader(test.in), "")
			got := fmt.Sprint(err)
			if got != test.want {
				t.Fatalf("want error %q, got %q", test.want, got)
			}
		})
	}
}

func TestParsePosix(t *testing.T) {
	t.Parallel()
	p := NewParser(Variant(LangPOSIX))
	for i, c := range append(fileTests, fileTestsNoPrint...) {
		want := c.Posix
		if want == nil {
			continue
		}
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("#%03d-%d", i, j),
				singleParse(p, in, want))
		}
	}
}

func TestParseMirBSDKorn(t *testing.T) {
	t.Parallel()
	p := NewParser(Variant(LangMirBSDKorn))
	for i, c := range append(fileTests, fileTestsNoPrint...) {
		want := c.MirBSDKorn
		if want == nil {
			continue
		}
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("#%03d-%d", i, j),
				singleParse(p, in, want))
		}
	}
}

func TestParseBats(t *testing.T) {
	t.Parallel()
	p := NewParser(Variant(LangBats))
	for i, c := range append(fileTests, fileTestsNoPrint...) {
		want := c.Bats
		if want == nil {
			continue
		}
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("#%03d-%d", i, j),
				singleParse(p, in, want))
		}
	}
}

func TestMain(m *testing.M) {
	// 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")
	}
	os.Exit(m.Run())
}

var (
	onceHasBash52 = sync.OnceValue(func() bool {
		return cmdContains("version 5.2", "bash", "--version")
	})

	onceHasDash059 = sync.OnceValue(func() bool {
		// dash provides no way to check its version, so we have to
		// check if it's new enough as to not have the bug that breaks
		// our integration tests.
		// This also means our check does not require a specific version.
		return cmdContains("Bad subst", "dash", "-c", "echo ${#<}")
	})

	onceHasMksh59 = sync.OnceValue(func() bool {
		return cmdContains(" R59 ", "mksh", "-c", "echo $KSH_VERSION")
	})
)

func requireBash52(tb testing.TB) {
	if !onceHasBash52() {
		tb.Skipf("bash 5.2 required to run")
	}
}

func requireDash059(tb testing.TB) {
	if !onceHasDash059() {
		tb.Skipf("dash 0.5.9+ required to run")
	}
}

func requireMksh59(tb testing.TB) {
	if !onceHasMksh59() {
		tb.Skipf("mksh 59 required to run")
	}
}

func cmdContains(substr, cmd string, args ...string) bool {
	out, err := exec.Command(cmd, args...).CombinedOutput()
	got := string(out)
	if err != nil {
		got += "\n" + err.Error()
	}
	return strings.Contains(got, substr)
}

var extGlobRe = regexp.MustCompile(`[@?*+!]\(`)

func confirmParse(in, cmd string, wantErr bool) func(*testing.T) {
	return func(t *testing.T) {
		t.Helper()
		t.Parallel()
		var opts []string
		if strings.Contains(in, "\\\r\n") {
			t.Skip("shells do not generally support CRLF line endings")
		}
		if cmd == "bash" && extGlobRe.MatchString(in) {
			// otherwise bash refuses to parse these
			// properly. Also avoid -n since that too makes
			// bash bail.
			in = "shopt -s extglob\n" + in
		} else if !wantErr {
			// -n makes bash accept invalid inputs like
			// "let" or "`{`", so only use it in
			// non-erroring tests. Should be safe to not use
			// -n anyway since these are supposed to just fail.
			// also, -n will break if we are using extglob
			// as extglob is not actually applied.
			opts = append(opts, "-n")
		}
		cmd := exec.Command(cmd, opts...)
		cmd.Dir = t.TempDir() // to be safe
		cmd.Stdin = strings.NewReader(in)
		var stderr bytes.Buffer
		cmd.Stderr = &stderr
		err := cmd.Run()
		if stderr.Len() > 0 {
			// bash sometimes likes to error on an input via stderr
			// while forgetting to set the exit code to non-zero. Fun.
			// Note that we also treat warnings as errors.
			err = errors.New(stderr.String())
		}
		if err != nil && strings.Contains(err.Error(), "command not found") {
			err = nil
		}
		if wantErr && err == nil {
			t.Fatalf("Expected error in %q of %q, found none", strings.Join(cmd.Args, " "), in)
		} else if !wantErr && err != nil {
			t.Fatalf("Unexpected error in %q of %q: %v", strings.Join(cmd.Args, " "), in, err)
		}
	}
}

func TestParseBashConfirm(t *testing.T) {
	if testing.Short() {
		t.Skip("calling bash is slow.")
	}
	requireBash52(t)
	i := 0
	for _, c := range append(fileTests, fileTestsNoPrint...) {
		if c.Bash == nil {
			continue
		}
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("#%03d-%d", i, j),
				confirmParse(in, "bash", false))
		}
		i++
	}
}

func TestParsePosixConfirm(t *testing.T) {
	if testing.Short() {
		t.Skip("calling dash is slow.")
	}
	requireDash059(t)
	i := 0
	for _, c := range append(fileTests, fileTestsNoPrint...) {
		if c.Posix == nil {
			continue
		}
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("#%03d-%d", i, j),
				confirmParse(in, "dash", false))
		}
		i++
	}
}

func TestParseMirBSDKornConfirm(t *testing.T) {
	if testing.Short() {
		t.Skip("calling mksh is slow.")
	}
	requireMksh59(t)
	i := 0
	for _, c := range append(fileTests, fileTestsNoPrint...) {
		if c.MirBSDKorn == nil {
			continue
		}
		for j, in := range c.Strs {
			t.Run(fmt.Sprintf("#%03d-%d", i, j),
				confirmParse(in, "mksh", false))
		}
		i++
	}
}

func TestParseErrBashConfirm(t *testing.T) {
	if testing.Short() {
		t.Skip("calling bash is slow.")
	}
	requireBash52(t)
	for _, c := range shellTests {
		want := c.common
		if c.bsmk != nil {
			want = c.bsmk
		}
		if c.bash != nil {
			want = c.bash
		}
		if want == nil {
			continue
		}
		wantErr := !strings.Contains(want.(string), " #NOERR")
		t.Run("", confirmParse(c.in, "bash", wantErr))
	}
}

func TestParseErrPosixConfirm(t *testing.T) {
	if testing.Short() {
		t.Skip("calling dash is slow.")
	}
	requireDash059(t)
	for _, c := range shellTests {
		want := c.common
		if c.posix != nil {
			want = c.posix
		}
		if want == nil {
			continue
		}
		wantErr := !strings.Contains(want.(string), " #NOERR")
		t.Run("", confirmParse(c.in, "dash", wantErr))
	}
}

func TestParseErrMirBSDKornConfirm(t *testing.T) {
	if testing.Short() {
		t.Skip("calling mksh is slow.")
	}
	requireMksh59(t)
	for _, c := range shellTests {
		want := c.common
		if c.bsmk != nil {
			want = c.bsmk
		}
		if c.mksh != nil {
			want = c.mksh
		}
		if want == nil {
			continue
		}
		wantErr := !strings.Contains(want.(string), " #NOERR")
		t.Run("", confirmParse(c.in, "mksh", wantErr))
	}
}

var cmpOpt = cmp.FilterValues(func(p1, p2 Pos) bool { return true }, cmp.Ignore())

func singleParse(p *Parser, in string, want *File) func(t *testing.T) {
	return func(t *testing.T) {
		t.Helper()
		got, err := p.Parse(newStrictReader(in), "")
		if err != nil {
			t.Fatalf("Unexpected error in %q: %v", in, err)
		}
		recursiveSanityCheck(t, in, got)
		qt.Assert(t, qt.CmpEquals(got, want, cmpOpt))
	}
}

type errorCase struct {
	in          string
	common      any
	bash, posix any
	bsmk, mksh  any
}

var shellTests = []errorCase{
	{
		in:     "echo \x80",
		common: `1:6: invalid UTF-8 encoding #NOERR common shells use bytes`,
	},
	{
		in:     "\necho \x80",
		common: `2:6: invalid UTF-8 encoding #NOERR common shells use bytes`,
	},
	{
		in:     "echo foo\x80bar",
		common: `1:9: invalid UTF-8 encoding #NOERR common shells use bytes`,
	},
	{
		in:     "echo foo\xc3",
		common: `1:9: invalid UTF-8 encoding #NOERR common shells use bytes`,
	},
	{
		in:     "#foo\xc3",
		common: `1:5: invalid UTF-8 encoding #NOERR common shells use bytes`,
	},
	{
		in:     "echo a\x80",
		common: `1:7: invalid UTF-8 encoding #NOERR common shells use bytes`,
	},
	{
		in:     "<<$\xc8\n$\xc8",
		common: `1:4: invalid UTF-8 encoding #NOERR common shells use bytes`,
	},
	{
		in:     "echo $((foo\x80bar",
		common: `1:12: invalid UTF-8 encoding`,
	},
	{
		in:   "z=($\\\n#\\\n\\\n$#\x91\\\n",
		bash: `4:3: invalid UTF-8 encoding`,
	},
	{
		in:   `((# 1 + 2))`,
		bash: `1:1: unsigned expressions are a mksh feature; tried parsing as bash`,
	},
	{
		in:    `$((# 1 + 2))`,
		posix: `1:1: unsigned expressions are a mksh feature; tried parsing as posix`,
		bash:  `1:1: unsigned expressions are a mksh feature; tried parsing as bash`,
	},
	{
		in:    `${ foo;}`,
		posix: `1:1: "${ stmts;}" is a mksh feature; tried parsing as posix`,
		bash:  `1:1: "${ stmts;}" is a mksh feature; tried parsing as bash`,
	},
	{
		in:   `${ `,
		mksh: `1:1: reached EOF without matching ${ with }`,
	},
	{
		in:   `${ foo;`,
		mksh: `1:1: reached EOF without matching ${ with }`,
	},
	{
		in:   `${ foo }`,
		mksh: `1:1: reached EOF without matching ${ with }`,
	},
	{
		in:    `${|foo;}`,
		posix: `1:1: "${|stmts;}" is a mksh feature; tried parsing as posix`,
		bash:  `1:1: "${|stmts;}" is a mksh feature; tried parsing as bash`,
	},
	{
		in:   `${|`,
		mksh: `1:1: reached EOF without matching ${ with }`,
	},
	{
		in:   `${|foo;`,
		mksh: `1:1: reached EOF without matching ${ with }`,
	},
	{
		in:   `${|foo }`,
		mksh: `1:1: reached EOF without matching ${ with }`,
	},
	{
		in:     "((foo\x80bar",
		common: `1:6: invalid UTF-8 encoding`,
	},
	{
		in:     ";\x80",
		common: `1:2: invalid UTF-8 encoding`,
	},
	{
		in:     "${a\x80",
		common: `1:4: invalid UTF-8 encoding`,
	},
	{
		in:     "${a#\x80",
		common: `1:5: invalid UTF-8 encoding`,
	},
	{
		in:     "${a-'\x80",
		common: `1:6: invalid UTF-8 encoding`,
	},
	{
		in:     "echo $((a |\x80",
		common: `1:12: invalid UTF-8 encoding`,
	},
	{
		in:     "!",
		common: `1:1: "!" cannot form a statement alone`,
	},
	{
		// bash allows lone '!', unlike dash, mksh, and us.
		in:     "! !",
		common: `1:1: cannot negate a command multiple times`,
		bash:   `1:1: cannot negate a command multiple times #NOERR`,
	},
	{
		in:     "! ! foo",
		common: `1:1: cannot negate a command multiple times #NOERR`,
		posix:  `1:1: cannot negate a command multiple times`,
	},
	{
		in:     "}",
		common: `1:1: "}" can only be used to close a block`,
	},
	{
		in:     "then",
		common: `1:1: "then" can only be used in an if`,
	},
	{
		in:     "elif",
		common: `1:1: "elif" can only be used in an if`,
	},
	{
		in:     "fi",
		common: `1:1: "fi" can only be used to end an if`,
	},
	{
		in:     "do",
		common: `1:1: "do" can only be used in a loop`,
	},
	{
		in:     "done",
		common: `1:1: "done" can only be used to end a loop`,
	},
	{
		in:     "esac",
		common: `1:1: "esac" can only be used to end a case`,
	},
	{
		in:     "a=b { foo; }",
		common: `1:12: "}" can only be used to close a block`,
	},
	{
		in:     "a=b foo() { bar; }",
		common: `1:8: a command can only contain words and redirects; encountered (`,
	},
	{
		in:     "a=b if foo; then bar; fi",
		common: `1:13: "then" can only be used in an if`,
	},
	{
		in:     ">f { foo; }",
		common: `1:11: "}" can only be used to close a block`,
	},
	{
		in:     ">f foo() { bar; }",
		common: `1:7: a command can only contain words and redirects; encountered (`,
	},
	{
		in:     ">f if foo; then bar; fi",
		common: `1:12: "then" can only be used in an if`,
	},
	{
		in:     "if done; then b; fi",
		common: `1:4: "done" can only be used to end a loop`,
	},
	{
		in:     "'",
		common: `1:1: reached EOF without closing quote '`,
	},
	{
		in:     `"`,
		common: `1:1: reached EOF without closing quote "`,
	},
	{
		in:     `'\''`,
		common: `1:4: reached EOF without closing quote '`,
	},
	{
		in:     ";",
		common: `1:1: ; can only immediately follow a statement`,
	},
	{
		in:     "{ ; }",
		common: `1:3: ; can only immediately follow a statement`,
	},
	{
		in:     `"foo"(){ :; }`,
		common: `1:1: invalid func name`,
		mksh:   `1:1: invalid func name #NOERR`,
	},
	{
		in:     `foo$bar(){ :; }`,
		common: `1:1: invalid func name`,
	},
	{
		in:     "{",
		common: `1:1: reached EOF without matching { with }`,
	},
	{
		in:     "{ #}",
		common: `1:1: reached EOF without matching { with }`,
	},
	{
		in:     "(",
		common: `1:1: reached EOF without matching ( with )`,
	},
	{
		in:     ")",
		common: `1:1: ) can only be used to close a subshell`,
	},
	{
		in:     "`",
		common: "1:1: reached EOF without closing quote `",
	},
	{
		in:     ";;",
		common: `1:1: ;; can only be used in a case clause`,
	},
	{
		in:     "( foo;",
		common: `1:1: reached EOF without matching ( with )`,
	},
	{
		in:     "&",
		common: `1:1: & can only immediately follow a statement`,
	},
	{
		in:     "|",
		common: `1:1: | can only immediately follow a statement`,
	},
	{
		in:     "&&",
		common: `1:1: && can only immediately follow a statement`,
	},
	{
		in:     "||",
		common: `1:1: || can only immediately follow a statement`,
	},
	{
		in:     "foo; || bar",
		common: `1:6: || can only immediately follow a statement`,
	},
	{
		in:     "echo & || bar",
		common: `1:8: || can only immediately follow a statement`,
	},
	{
		in:     "echo & ; bar",
		common: `1:8: ; can only immediately follow a statement`,
	},
	{
		in:     "foo;;",
		common: `1:4: ;; can only be used in a case clause`,
	},
	{
		in:     "foo(",
		common: `1:1: "foo(" must be followed by )`,
	},
	{
		in:     "foo(bar",
		common: `1:1: "foo(" must be followed by )`,
	},
	{
		in:     "à(",
		common: `1:1: "foo(" must be followed by )`,
	},
	{
		in:     "foo'",
		common: `1:4: reached EOF without closing quote '`,
	},
	{
		in:     `foo"`,
		common: `1:4: reached EOF without closing quote "`,
	},
	{
		in:     `"foo`,
		common: `1:1: reached EOF without closing quote "`,
	},
	{
		in:     `"foobar\`,
		common: `1:1: reached EOF without closing quote "`,
	},
	{
		in:     `"foo\a`,
		common: `1:1: reached EOF without closing quote "`,
	},
	{
		in:     "foo()",
		common: `1:1: "foo()" must be followed by a statement`,
		mksh:   `1:1: "foo()" must be followed by a statement #NOERR`,
	},
	{
		in:     "foo() {",
		common: `1:7: reached EOF without matching { with }`,
	},
	{
		in:    "foo-bar() { x; }",
		posix: `1:1: invalid func name`,
	},
	{
		in:    "foò() { x; }",
		posix: `1:1: invalid func name`,
	},
	{
		in:     "echo foo(",
		common: `1:9: a command can only contain words and redirects; encountered (`,
	},
	{
		in:     "echo &&",
		common: `1:6: && must be followed by a statement`,
	},
	{
		in:     "echo |",
		common: `1:6: | must be followed by a statement`,
	},
	{
		in:     "echo ||",
		common: `1:6: || must be followed by a statement`,
	},
	{
		in:     "echo | #bar",
		common: `1:6: | must be followed by a statement`,
	},
	{
		in:     "echo && #bar",
		common: `1:6: && must be followed by a statement`,
	},
	{
		in:     "`echo &&`",
		common: `1:7: && must be followed by a statement`,
	},
	{
		in:     "`echo |`",
		common: `1:7: | must be followed by a statement`,
	},
	{
		in:     "echo | ! bar",
		common: `1:8: "!" can only be used in full statements`,
	},
	{
		in:     "echo >",
		common: `1:6: > must be followed by a word`,
	},
	{
		in:     "echo >>",
		common: `1:6: >> must be followed by a word`,
	},
	{
		in:     "echo <",
		common: `1:6: < must be followed by a word`,
	},
	{
		in:     "echo 2>",
		common: `1:7: > must be followed by a word`,
	},
	{
		in:     "echo <\nbar",
		common: `1:6: < must be followed by a word`,
	},
	{
		in:     "echo | < #bar",
		common: `1:8: < must be followed by a word`,
	},
	{
		in:     "echo && > #",
		common: `1:9: > must be followed by a word`,
	},
	{
		in:    "foo &>/dev/null",
		posix: `1:5: &> redirects are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "foo &>>/dev/null",
		posix: `1:5: &> redirects are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:     "<<",
		common: `1:1: << must be followed by a word`,
	},
	{
		in:     "<<EOF",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<EOF\n\\",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<EOF\n\\\n",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in: "<<EOF\n\\\nEOF",
		// Seems like mksh has a bug here.
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
	},
	{
		in:     "<<EOF\nfoo\\\nEOF",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<'EOF'\n\\\n",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<EOF <`\n#\n`\n``",
		common: `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<'EOF'",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<\\EOF",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<\\\\EOF",
		common: `1:1: unclosed here-document '\EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document '\EOF'`,
	},
	{
		in:     "<<-EOF",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<-EOF\n\t",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<-'EOF'\n\t",
		common: `1:1: unclosed here-document 'EOF' #NOERR`,
		bsmk:   `1:1: unclosed here-document 'EOF'`,
	},
	{
		in:     "<<\nEOF\nbar\nEOF",
		common: `1:1: << must be followed by a word`,
	},
	{
		in:   "$(<<EOF\nNOTEOF)",
		bsmk: `1:3: unclosed here-document 'EOF'`,
	},
	{
		in:   "`<<EOF\nNOTEOF`",
		bsmk: `1:2: unclosed here-document 'EOF'`,
	},
	{
		in:     "if",
		common: `1:1: "if" must be followed by a statement list`,
	},
	{
		in:     "if true;",
		common: `1:1: "if <cond>" must be followed by "then"`,
	},
	{
		in:     "if true then",
		common: `1:1: "if <cond>" must be followed by "then"`,
	},
	{
		in:     "if true; then bar;",
		common: `1:1: if statement must end with "fi"`,
	},
	{
		in:     "if true; then bar; fi#etc",
		common: `1:1: if statement must end with "fi"`,
	},
	{
		in:     "if a; then b; elif c;",
		common: `1:15: "elif <cond>" must be followed by "then"`,
	},
	{
		in:     "'foo' '",
		common: `1:7: reached EOF without closing quote '`,
	},
	{
		in:     "'foo\n' '",
		common: `2:3: reached EOF without closing quote '`,
	},
	{
		in:     "while",
		common: `1:1: "while" must be followed by a statement list`,
	},
	{
		in:     "while true;",
		common: `1:1: "while <cond>" must be followed by "do"`,
	},
	{
		in:     "while true; do bar",
		common: `1:1: while statement must end with "done"`,
	},
	{
		in:     "while true; do bar;",
		common: `1:1: while statement must end with "done"`,
	},
	{
		in:     "until",
		common: `1:1: "until" must be followed by a statement list`,
	},
	{
		in:     "until true;",
		common: `1:1: "until <cond>" must be followed by "do"`,
	},
	{
		in:     "until true; do bar",
		common: `1:1: until statement must end with "done"`,
	},
	{
		in:     "until true; do bar;",
		common: `1:1: until statement must end with "done"`,
	},
	{
		in:     "for",
		common: `1:1: "for" must be followed by a literal`,
	},
	{
		in:     "for i",
		common: `1:1: "for foo" must be followed by "in", "do", ;, or a newline`,
	},
	{
		in:     "for i in;",
		common: `1:1: "for foo [in words]" must be followed by "do"`,
	},
	{
		in:     "for i in 1 2 3;",
		common: `1:1: "for foo [in words]" must be followed by "do"`,
	},
	{
		in:     "for i in 1 2 &",
		common: `1:1: "for foo [in words]" must be followed by "do"`,
	},
	{
		in:     "for i in 1 2 (",
		common: `1:14: word list can only contain words`,
	},
	{
		in:     "for i in 1 2 3; do echo $i;",
		common: `1:1: for statement must end with "done"`,
	},
	{
		in:     "for i in 1 2 3; echo $i;",
		common: `1:1: "for foo [in words]" must be followed by "do"`,
	},
	{
		in:     "for 'i' in 1 2 3; do echo $i; done",
		common: `1:1: "for" must be followed by a literal`,
	},
	{
		in:     "for in 1 2 3; do echo $i; done",
		common: `1:1: "for foo" must be followed by "in", "do", ;, or a newline`,
	},
	{
		in:   "select",
		bsmk: `1:1: "select" must be followed by a literal`,
	},
	{
		in:   "select i",
		bsmk: `1:1: "select foo" must be followed by "in", "do", ;, or a newline`,
	},
	{
		in:   "select i in;",
		bsmk: `1:1: "select foo [in words]" must be followed by "do"`,
	},
	{
		in:   "select i in 1 2 3;",
		bsmk: `1:1: "select foo [in words]" must be followed by "do"`,
	},
	{
		in:   "select i in 1 2 3; do echo $i;",
		bsmk: `1:1: select statement must end with "done"`,
	},
	{
		in:   "select i in 1 2 3; echo $i;",
		bsmk: `1:1: "select foo [in words]" must be followed by "do"`,
	},
	{
		in:   "select 'i' in 1 2 3; do echo $i; done",
		bsmk: `1:1: "select" must be followed by a literal`,
	},
	{
		in:   "select in 1 2 3; do echo $i; done",
		bsmk: `1:1: "select foo" must be followed by "in", "do", ;, or a newline`,
	},
	{
		in:     "echo foo &\n;",
		common: `2:1: ; can only immediately follow a statement`,
	},
	{
		in:     "echo $(foo",
		common: `1:6: reached EOF without matching ( with )`,
	},
	{
		in:     "echo $((foo",
		common: `1:6: reached EOF without matching $(( with ))`,
	},
	{
		in:     `echo $((\`,
		common: `1:6: reached EOF without matching $(( with ))`,
	},
	{
		in:     `echo $((o\`,
		common: `1:6: reached EOF without matching $(( with ))`,
	},
	{
		in:     `echo $((foo\a`,
		common: `1:6: reached EOF without matching $(( with ))`,
	},
	{
		in:     `echo $(($(a"`,
		common: `1:12: reached EOF without closing quote "`,
	},
	{
		in:     "echo $((`echo 0`",
		common: `1:6: reached EOF without matching $(( with ))`,
	},
	{
		in:     `echo $((& $(`,
		common: `1:9: & must follow an expression`,
	},
	{
		in:     `echo $((a'`,
		common: `1:10: reached EOF without closing quote '`,
	},
	{
		in:     `echo $((a b"`,
		common: `1:11: not a valid arithmetic operator: b`,
	},
	{
		in:     "echo $(())",
		common: `1:6: $(( must be followed by an expression #NOERR`,
	},
	{
		in:     "echo $((()))",
		common: `1:9: ( must be followed by an expression`,
	},
	{
		in:     "echo $(((3))",
		common: `1:6: reached ) without matching $(( with ))`,
	},
	{
		in:     "echo $((+))",
		common: `1:9: + must be followed by an expression`,
	},
	{
		in:     "echo $((a b c))",
		common: `1:11: not a valid arithmetic operator: b`,
	},
	{
		in:     "echo $((a ; c))",
		common: `1:11: not a valid arithmetic operator: ;`,
	},
	{
		in:   "echo $((foo) )",
		bsmk: `1:6: reached ) without matching $(( with )) #NOERR`,
	},
	{
		in:     "echo $((a *))",
		common: `1:11: * must be followed by an expression`,
	},
	{
		in:     "echo $((++))",
		common: `1:9: ++ must be followed by a literal`,
	},
	{
		in:     "echo $((a ? b))",
		common: `1:11: ternary operator missing : after ?`,
	},
	{
		in:     "echo $((a : b))",
		common: `1:11: ternary operator missing ? before :`,
	},
	{
		in:     "echo $((/",
		common: `1:9: / must follow an expression`,
	},
	{
		in:     "echo $((:",
		common: `1:9: ternary operator missing ? before :`,
	},
	{
		in:     "echo $(((a)+=b))",
		common: `1:12: += must follow a name`,
		mksh:   `1:12: += must follow a name #NOERR`,
	},
	{
		in:     "echo $((1=2))",
		common: `1:10: = must follow a name`,
	},
	{
		in:     "echo $(($0=2))",
		common: `1:11: = must follow a name #NOERR`,
	},
	{
		in:     "echo $(($(a)=2))",
		common: `1:13: = must follow a name #NOERR`,
	},
	{
		in: "echo $((1'2'))",
		// TODO: Take a look at this again, since this no longer fails
		// after fixing github.com/mvdan/sh/issues/587.
		// Note that Bash seems to treat code inside $(()) as if it were
		// within double quotes, yet still requires single quotes to be
		// matched.
		// common: `1:10: not a valid arithmetic operator: '`,
	},
	{
		in:     "<<EOF\n$(()a",
		common: `2:1: $(( must be followed by an expression`,
	},
	{
		in:     "<<EOF\n`))",
		common: `2:2: ) can only be used to close a subshell`,
	},
	{
		in:     "echo ${foo",
		common: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:     "echo $foo ${}",
		common: `1:13: parameter expansion requires a literal`,
	},
	{
		in:     "echo ${à}",
		common: `1:8: invalid parameter name`,
	},
	{
		in:     "echo ${1a}",
		common: `1:8: invalid parameter name`,
	},
	{
		in:     "echo ${foo-bar",
		common: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:     "#foo\n{",
		common: `2:1: reached EOF without matching { with }`,
	},
	{
		in:     `echo "foo${bar"`,
		common: `1:15: not a valid parameter expansion operator: "`,
	},
	{
		in:     "echo ${%",
		common: `1:6: "${%foo}" is a mksh feature; tried parsing as LANG`,
		mksh:   `1:8: parameter expansion requires a literal`,
	},
	{
		in:     "echo ${##",
		common: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:     "echo ${#<}",
		common: `1:9: parameter expansion requires a literal`,
	},
	{
		in:   "echo ${%<}",
		mksh: `1:9: parameter expansion requires a literal`,
	},
	{
		in:   "echo ${!<}",
		bsmk: `1:9: parameter expansion requires a literal`,
	},
	{
		in:     "echo ${@foo}",
		common: `1:9: @ cannot be followed by a word`,
	},
	{
		in:     "echo ${$foo}",
		common: `1:9: $ cannot be followed by a word`,
	},
	{
		in:     "echo ${?foo}",
		common: `1:9: ? cannot be followed by a word`,
	},
	{
		in:     "echo ${-foo}",
		common: `1:9: - cannot be followed by a word`,
	},
	{
		in:   "echo ${@[@]} ${@[*]}",
		bsmk: `1:9: cannot index a special parameter name`,
	},
	{
		in:   "echo ${*[@]} ${*[*]}",
		bsmk: `1:9: cannot index a special parameter name`,
	},
	{
		in:   "echo ${#[x]}",
		bsmk: `1:9: cannot index a special parameter name`,
	},
	{
		in:   "echo ${$[0]}",
		bsmk: `1:9: cannot index a special parameter name`,
	},
	{
		in:   "echo ${?[@]}",
		bsmk: `1:9: cannot index a special parameter name`,
	},
	{
		in:   "echo ${2[@]}",
		bsmk: `1:9: cannot index a special parameter name`,
	},
	{
		in:   "echo ${foo*}",
		bsmk: `1:11: not a valid parameter expansion operator: *`,
	},
	{
		in:   "echo ${foo;}",
		bsmk: `1:11: not a valid parameter expansion operator: ;`,
	},
	{
		in:   "echo ${foo!}",
		bsmk: `1:11: not a valid parameter expansion operator: !`,
	},
	{
		in:   "echo ${#foo:-bar}",
		bsmk: `1:12: cannot combine multiple parameter expansion operators`,
	},
	{
		in:   "echo ${%foo:1:3}",
		mksh: `1:12: cannot combine multiple parameter expansion operators`,
	},
	{
		in:   "echo ${#foo%x}",
		mksh: `1:12: cannot combine multiple parameter expansion operators`,
	},
	{
		in:     "echo foo\n;",
		common: `2:1: ; can only immediately follow a statement`,
	},
	{
		in:   "<<$ <<0\n$(<<$<<",
		bsmk: `2:6: << must be followed by a word`,
	},
	{
		in:     "(foo) bar",
		common: `1:7: statements must be separated by &, ; or a newline`,
	},
	{
		in:     "{ foo; } bar",
		common: `1:10: statements must be separated by &, ; or a newline`,
	},
	{
		in:     "if foo; then bar; fi bar",
		common: `1:22: statements must be separated by &, ; or a newline`,
	},
	{
		in:     "case",
		common: `1:1: "case" must be followed by a word`,
	},
	{
		in:     "case i",
		common: `1:1: "case x" must be followed by "in"`,
	},
	{
		in:     "case i in 3) foo;",
		common: `1:1: case statement must end with "esac"`,
	},
	{
		in:     "case i in 3) foo; 4) bar; esac",
		common: `1:20: a command can only contain words and redirects; encountered )`,
	},
	{
		in:     "case i in 3&) foo;",
		common: `1:12: case patterns must be separated with |`,
	},
	{
		in:     "case $i in &) foo;",
		common: `1:12: case patterns must consist of words`,
	},
	{
		in:     "case i {",
		common: `1:1: "case i {" is a mksh feature; tried parsing as LANG`,
		mksh:   `1:1: case statement must end with "}"`,
	},
	{
		in:   "case i { x) y ;;",
		mksh: `1:1: case statement must end with "}"`,
	},
	{
		in:     "\"`\"",
		common: `1:3: reached EOF without closing quote "`,
	},
	{
		in:     "`\"`",
		common: "1:2: reached ` without closing quote \"",
	},
	{
		in:     "`\\```",
		common: "1:3: reached EOF without closing quote `",
	},
	{
		in:     "`{\n`",
		common: "1:2: reached ` without matching { with }",
	},
	{
		in:    "echo \"`)`\"",
		bsmk:  `1:8: ) can only be used to close a subshell`,
		posix: `1:8: ) can only be used to close a subshell #NOERR dash bug`,
	},
	{
		in:     "<<$bar\n$bar",
		common: `1:3: expansions not allowed in heredoc words #NOERR`,
	},
	{
		in:     "<<${bar}\n${bar}",
		common: `1:3: expansions not allowed in heredoc words #NOERR`,
	},

	// bash uses "$(bar)" as the closing word, but other shells use "$".
	// We instead give an error for expansions in heredoc words.
	{
		in:    "<<$(bar)\n$",
		posix: `1:3: expansions not allowed in heredoc words`,
		mksh:  `1:3: expansions not allowed in heredoc words #NOERR`,
	},
	{
		in:   "<<$(bar)\n$(bar)",
		bash: `1:3: expansions not allowed in heredoc words #NOERR`,
	},

	{
		in:     "<<$-\n$-",
		common: `1:3: expansions not allowed in heredoc words #NOERR`,
	},
	{
		in:     "<<`bar`\n`bar`",
		common: `1:3: expansions not allowed in heredoc words #NOERR`,
	},
	{
		in:     "<<\"$bar\"\n$bar",
		common: `1:4: expansions not allowed in heredoc words #NOERR`,
	},
	{
		in:     "<<a <<0\n$(<<$<<",
		common: `2:6: << must be followed by a word`,
	},
	{
		in:     `""()`,
		common: `1:1: invalid func name`,
		mksh:   `1:1: invalid func name #NOERR`,
	},
	{
		// bash errors on the empty condition here, this is to
		// add coverage for empty statement lists
		in:     `if; then bar; fi; ;`,
		common: `1:19: ; can only immediately follow a statement`,
	},
	{
		in:    "]] )",
		bsmk:  `1:1: "]]" can only be used to close a test`,
		posix: `1:4: a command can only contain words and redirects; encountered )`,
	},
	{
		in:    "((foo",
		bsmk:  `1:1: reached EOF without matching (( with ))`,
		posix: `1:2: reached EOF without matching ( with )`,
	},
	{
		in:   "(())",
		bsmk: `1:1: (( must be followed by an expression`,
	},
	{
		in:    "echo ((foo",
		bsmk:  `1:6: (( can only be used to open an arithmetic cmd`,
		posix: `1:1: "foo(" must be followed by )`,
	},
	{
		in:    "echo |&",
		bash:  `1:6: |& must be followed by a statement`,
		posix: `1:6: | must be followed by a statement`,
	},
	{
		in:   "|& a",
		bsmk: `1:1: |& is not a valid start for a statement`,
	},
	{
		in:    "foo |& bar",
		posix: `1:5: | must be followed by a statement`,
	},
	{
		in:   "let",
		bsmk: `1:1: "let" must be followed by an expression`,
	},
	{
		in:   "let a+ b",
		bsmk: `1:6: + must be followed by an expression`,
	},
	{
		in:   "let + a",
		bsmk: `1:5: + must be followed by an expression`,
	},
	{
		in:   "let a ++",
		bsmk: `1:7: ++ must be followed by a literal`,
	},
	{
		in:   "let (a)++",
		bsmk: `1:8: ++ must follow a name`,
	},
	{
		in:   "let 1++",
		bsmk: `1:6: ++ must follow a name`,
	},
	{
		in:   "let $0++",
		bsmk: `1:7: ++ must follow a name`,
	},
	{
		in:   "let --(a)",
		bsmk: `1:5: -- must be followed by a literal`,
	},
	{
		in:   "let --$a",
		bsmk: `1:5: -- must be followed by a literal`,
	},
	{
		in:   "let a+\n",
		bsmk: `1:6: + must be followed by an expression`,
	},
	{
		in:   "let ))",
		bsmk: `1:1: "let" must be followed by an expression`,
	},
	{
		in:   "`let !`",
		bsmk: `1:6: ! must be followed by an expression`,
	},
	{
		in:   "let a:b",
		bsmk: `1:6: ternary operator missing ? before :`,
	},
	{
		in:   "let a+b=c",
		bsmk: `1:8: = must follow a name`,
	},
	{
		in:   "`let` { foo; }",
		bsmk: `1:2: "let" must be followed by an expression`,
	},
	{
		in:   "$(let)",
		bsmk: `1:3: "let" must be followed by an expression`,
	},
	{
		in:   "[[",
		bsmk: `1:1: test clause requires at least one expression`,
	},
	{
		in:   "[[ ]]",
		bsmk: `1:1: test clause requires at least one expression`,
	},
	{
		in:   "[[ a",
		bsmk: `1:1: reached EOF without matching [[ with ]]`,
	},
	{
		in:   "[[ a ||",
		bsmk: `1:6: || must be followed by an expression`,
	},
	{
		in:   "[[ a && &&",
		bsmk: `1:6: && must be followed by an expression`,
	},
	{
		in:   "[[ a && ]]",
		bsmk: `1:6: && must be followed by an expression`,
	},
	{
		in:   "[[ a ==",
		bsmk: `1:6: == must be followed by a word`,
	},
	{
		in:   "[[ a =~",
		bash: `1:6: =~ must be followed by a word`,
		mksh: `1:6: regex tests are a bash feature; tried parsing as mksh`,
	},
	{
		in:   "[[ -f a",
		bsmk: `1:1: reached EOF without matching [[ with ]]`,
	},
	{
		in:   "[[ -n\na ]]",
		bsmk: `1:4: -n must be followed by a word`,
	},
	{
		in:   "[[ a -ef\nb ]]",
		bsmk: `1:6: -ef must be followed by a word`,
	},
	{
		in:   "[[ a ==\nb ]]",
		bsmk: `1:6: == must be followed by a word`,
	},
	{
		in:   "[[ a -nt b",
		bsmk: `1:1: reached EOF without matching [[ with ]]`,
	},
	{
		in:   "[[ a =~ b",
		bash: `1:1: reached EOF without matching [[ with ]]`,
	},
	{
		in:   "[[ a b c ]]",
		bsmk: `1:6: not a valid test operator: b`,
	},
	{
		in:   "[[ a b$x c ]]",
		bsmk: `1:6: test operator words must consist of a single literal`,
	},
	{
		in:   "[[ a & b ]]",
		bsmk: `1:6: not a valid test operator: &`,
	},
	{
		in:   "[[ true && () ]]",
		bsmk: `1:12: ( must be followed by an expression`,
	},
	{
		in:   "[[ true && (&& ]]",
		bsmk: `1:12: ( must be followed by an expression`,
	},
	{
		in:   "[[ a == ! b ]]",
		bsmk: `1:11: not a valid test operator: b`,
	},
	{
		in:   "[[ (! ) ]]",
		bsmk: `1:5: ! must be followed by an expression`,
	},
	{
		in:   "[[ ! && ]]",
		bsmk: `1:4: ! must be followed by an expression`,
	},
	{
		in:   "[[ (-e ) ]]",
		bsmk: `1:5: -e must be followed by a word`,
	},
	{
		in:   "[[ (a) == b ]]",
		bsmk: `1:8: expected &&, || or ]] after complex expr`,
	},
	{
		in:   "[[ a =~ ; ]]",
		bash: `1:6: =~ must be followed by a word`,
	},
	{
		in:   "[[ a =~ )",
		bash: `1:6: =~ must be followed by a word`,
	},
	{
		in:   "[[ a =~ ())",
		bash: `1:1: reached ) without matching [[ with ]]`,
	},
	{
		in:   "[[ >",
		bsmk: `1:1: [[ must be followed by an expression`,
	},
	{
		in:   "local (",
		bash: `1:7: "local" must be followed by names or assignments`,
	},
	{
		in:   "declare 0=${o})",
		bash: `1:9: invalid var name`,
	},
	{
		in:   "a=(<)",
		bsmk: `1:4: array element values must be words`,
	},
	{
		in:   "a=([)",
		bash: `1:4: [ must be followed by an expression`,
	},
	{
		in:   "a=([i)",
		bash: `1:4: reached ) without matching [ with ]`,
	},
	{
		in:   "a=([i])",
		bash: `1:4: "[x]" must be followed by = #NOERR`,
	},
	{
		in:   "a[i]=(y)",
		bash: `1:6: arrays cannot be nested`,
	},
	{
		in:   "a=([i]=(y))",
		bash: `1:8: arrays cannot be nested`,
	},
	{
		in:   "o=([0]=#",
		bash: `1:8: array element values must be words`,
	},
	{
		in:   "a[b] ==[",
		bash: `1:1: "a[b]" must be followed by = #NOERR stringifies`,
	},
	{
		in:   "a[b] +=c",
		bash: `1:1: "a[b]" must be followed by = #NOERR stringifies`,
	},
	{
		in:   "a=(x y) foo",
		bash: `1:1: inline variables cannot be arrays #NOERR stringifies`,
	},
	{
		in:   "a[2]=x foo",
		bash: `1:1: inline variables cannot be arrays #NOERR stringifies`,
	},
	{
		in:   "function",
		bsmk: `1:1: "function" must be followed by a name`,
	},
	{
		in:   "function foo(",
		bsmk: `1:10: "foo(" must be followed by )`,
	},
	{
		in:   "function `function",
		bsmk: `1:1: "function" must be followed by a name`,
	},
	{
		in:   `function "foo"(){}`,
		bsmk: `1:1: "function" must be followed by a name`,
	},
	{
		in:   "function foo()",
		bsmk: `1:1: "foo()" must be followed by a statement`,
	},
	{
		in:   "echo <<<",
		bsmk: `1:6: <<< must be followed by a word`,
	},
	{
		in:   "a[",
		bsmk: `1:2: [ must be followed by an expression`,
	},
	{
		in:   "a[b",
		bsmk: `1:2: reached EOF without matching [ with ]`,
	},
	{
		in:   "a[]",
		bsmk: `1:2: [ must be followed by an expression #NOERR is cmd`,
	},
	{
		in:   "a[[",
		bsmk: `1:3: [ must follow a name`,
	},
	{
		in:   "echo $((a[))",
		bsmk: `1:10: [ must be followed by an expression`,
	},
	{
		in:   "echo $((a[b))",
		bsmk: `1:10: reached ) without matching [ with ]`,
	},
	{
		in:   "echo $((a[]))",
		bash: `1:10: [ must be followed by an expression`,
		mksh: `1:10: [ must be followed by an expression #NOERR wrong?`,
	},
	{
		in:   "echo $((x$t[",
		bsmk: `1:12: [ must follow a name`,
	},
	{
		in:   "a[1]",
		bsmk: `1:1: "a[b]" must be followed by = #NOERR is cmd`,
	},
	{
		in:   "a[i]+",
		bsmk: `1:1: "a[b]+" must be followed by = #NOERR is cmd`,
	},
	{
		in:   "a[1]#",
		bsmk: `1:1: "a[b]" must be followed by = #NOERR is cmd`,
	},
	{
		in:   "echo $[foo",
		bash: `1:6: reached EOF without matching $[ with ]`,
	},
	{
		in:   "echo $'",
		bsmk: `1:6: reached EOF without closing quote '`,
	},
	{
		in:   `echo $"`,
		bsmk: `1:6: reached EOF without closing quote "`,
	},
	{
		in:   "echo @(",
		bsmk: `1:6: reached EOF without matching @( with )`,
	},
	{
		in:   "echo @(a",
		bsmk: `1:6: reached EOF without matching @( with )`,
	},
	{
		in:   "((@(",
		bsmk: `1:1: reached ( without matching (( with ))`,
	},
	{
		in:   "time {",
		bsmk: `1:6: reached EOF without matching { with }`,
	},
	{
		in:   "time ! foo",
		bash: `1:6: "!" can only be used in full statements #NOERR wrong`,
		mksh: `1:6: "!" can only be used in full statements`,
	},
	{
		in:   "coproc",
		bash: `1:1: coproc clause requires a command`,
	},
	{
		in:   "coproc\n$",
		bash: `1:1: coproc clause requires a command`,
	},
	{
		in:   "coproc declare (",
		bash: `1:16: "declare" must be followed by names or assignments`,
	},
	{
		in:   "echo ${foo[1 2]}",
		bsmk: `1:14: not a valid arithmetic operator: 2`,
	},
	{
		in:   "echo ${foo[}",
		bsmk: `1:11: [ must be followed by an expression`,
	},
	{
		in:   "echo ${foo]}",
		bsmk: `1:11: not a valid parameter expansion operator: ]`,
	},
	{
		in:   "echo ${foo[]}",
		bash: `1:11: [ must be followed by an expression`,
		mksh: `1:11: [ must be followed by an expression #NOERR wrong?`,
	},
	{
		in:   "echo ${a/\n",
		bsmk: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:   "echo ${a/''",
		bsmk: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:   "echo ${a-\n",
		bsmk: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:   "echo ${foo:",
		bsmk: `1:11: : must be followed by an expression`,
	},
	{
		in:   "echo ${foo:1 2}",
		bsmk: `1:14: not a valid arithmetic operator: 2 #NOERR lazy eval`,
	},
	{
		in:   "echo ${foo:1",
		bsmk: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:   "echo ${foo:1:",
		bsmk: `1:13: : must be followed by an expression`,
	},
	{
		in:   "echo ${foo:1:2",
		bsmk: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:   "echo ${foo,",
		bash: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:   "echo ${foo@",
		bash: `1:11: @ expansion operator requires a literal`,
	},
	{
		in:   "echo ${foo@}",
		bash: `1:12: @ expansion operator requires a literal #NOERR empty string fallback`,
	},
	{
		in:   "echo ${foo@Q",
		bash: `1:6: reached EOF without matching ${ with }`,
	},
	{
		in:   "echo ${foo@bar}",
		bash: `1:12: invalid @ expansion operator "bar" #NOERR at runtime`,
	},
	{
		in:   "echo ${foo@'Q'}",
		bash: `1:12: @ expansion operator requires a literal #NOERR at runtime`,
	},
	{
		in:   `echo $((echo a); (echo b))`,
		bsmk: `1:14: not a valid arithmetic operator: a #NOERR backtrack`,
	},
	{
		in:   `((echo a); (echo b))`,
		bsmk: `1:8: not a valid arithmetic operator: a #NOERR backtrack`,
	},
	{
		in:   "for ((;;",
		bash: `1:5: reached EOF without matching (( with ))`,
	},
	{
		in:   "for ((;;0000000",
		bash: `1:5: reached EOF without matching (( with ))`,
	},
	{
		in:    "function foo() { bar; }",
		posix: `1:13: the "function" builtin is a bash feature; tried parsing as posix`,
	},
	{
		in:    "function foo { bar; }",
		posix: `1:14: the "function" builtin is a bash feature; tried parsing as posix`,
	},
	{
		in:    "declare foo=(bar)",
		posix: `1:13: the "declare" builtin is a bash feature; tried parsing as posix`,
	},
	{
		in:    "let foo=(bar)",
		posix: `1:9: the "let" builtin is a bash feature; tried parsing as posix`,
	},
	{
		in:    "echo <(",
		posix: `1:6: < must be followed by a word`,
		mksh:  `1:6: < must be followed by a word`,
	},
	{
		in:    "echo >(",
		posix: `1:6: > must be followed by a word`,
		mksh:  `1:6: > must be followed by a word`,
	},
	{
		// shells treat {var} as an argument, but we are a bit stricter
		// so that users won't think this will work like they expect in
		// POSIX shell.
		in:    "echo {var}>foo",
		posix: `1:6: {varname} redirects are a bash feature; tried parsing as posix #NOERR`,
		mksh:  `1:6: {varname} redirects are a bash feature; tried parsing as mksh #NOERR`,
	},
	{
		in:    "echo ;&",
		posix: `1:7: & can only immediately follow a statement`,
		bsmk:  `1:6: ;& can only be used in a case clause`,
	},
	{
		in:    "echo ;;&",
		posix: `1:6: ;; can only be used in a case clause`,
		mksh:  `1:6: ;; can only be used in a case clause`,
	},
	{
		in:    "echo ;|",
		posix: `1:7: | can only immediately follow a statement`,
		bash:  `1:7: | can only immediately follow a statement`,
	},
	{
		in:    "for i in 1 2 3; { echo; }",
		posix: `1:17: for loops with braces are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "for ((i=0; i<5; i++)); do echo; done",
		posix: `1:5: c-style fors are a bash feature; tried parsing as posix`,
		mksh:  `1:5: c-style fors are a bash feature; tried parsing as mksh`,
	},
	{
		in:    "echo !(a)",
		posix: `1:6: extended globs are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "echo $a@(b)",
		posix: `1:8: extended globs are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "foo=(1 2)",
		posix: `1:5: arrays are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:     "a=$c\n'",
		common: `2:1: reached EOF without closing quote '`,
	},
	{
		in:    "echo ${!foo}",
		posix: `1:6: "${!foo}" is a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "echo ${!foo*}",
		posix: `1:6: "${!foo*}" is a bash feature; tried parsing as posix`,
		mksh:  `1:6: "${!foo*}" is a bash feature; tried parsing as mksh`,
	},
	{
		in:    "echo ${!foo@}",
		posix: `1:12: this expansion operator is a bash/mksh feature; tried parsing as posix`,
		mksh:  `1:6: "${!foo@}" is a bash feature; tried parsing as mksh`,
	},
	{
		in:    "echo ${!foo[@]}",
		posix: `1:12: arrays are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "echo ${foo[1]}",
		posix: `1:11: arrays are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "echo ${foo/a/b}",
		posix: `1:11: search and replace is a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "echo ${foo:1}",
		posix: `1:11: slicing is a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "foo <<< bar",
		posix: `1:5: herestrings are a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:    "foo << < bar",
		posix: `1:5: << must be followed by a word`,
	},
	{
		in:    "echo ${foo,bar}",
		posix: `1:11: this expansion operator is a bash feature; tried parsing as posix`,
		mksh:  `1:11: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:    "echo ${foo@Q}",
		posix: `1:11: this expansion operator is a bash/mksh feature; tried parsing as posix`,
	},
	{
		in:   "echo ${foo@a}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@u}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@A}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@E}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@K}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@k}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@L}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@P}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@U}",
		mksh: `1:12: this expansion operator is a bash feature; tried parsing as mksh`,
	},
	{
		in:   "echo ${foo@#}",
		bash: `1:12: this expansion operator is a mksh feature; tried parsing as bash #NOERR`,
	},
	{
		in:     "`\"`\\",
		common: "1:2: reached ` without closing quote \"",
	},
}

func checkError(p *Parser, in, want string) func(*testing.T) {
	return func(t *testing.T) {
		if i := strings.Index(want, " #NOERR"); i >= 0 {
			want = want[:i]
		}
		want = strings.Replace(want, "LANG", p.lang.String(), 1)
		_, err := p.Parse(newStrictReader(in), "")
		if err == nil {
			t.Fatalf("Expected error in %q: %v", in, want)
		}
		if got := err.Error(); got != want {
			t.Fatalf("Error mismatch in %q\nwant: %s\ngot:  %s",
				in, want, got)
		}
	}
}

func TestParseErrPosix(t *testing.T) {
	t.Parallel()
	p := NewParser(KeepComments(true), Variant(LangPOSIX))
	for _, c := range shellTests {
		want := c.common
		if c.posix != nil {
			want = c.posix
		}
		if want == nil {
			continue
		}
		t.Run("", checkError(p, c.in, want.(string)))
	}
}

func TestParseErrBash(t *testing.T) {
	t.Parallel()
	p := NewParser(KeepComments(true))
	for _, c := range shellTests {
		want := c.common
		if c.bsmk != nil {
			want = c.bsmk
		}
		if c.bash != nil {
			want = c.bash
		}
		if want == nil {
			continue
		}
		t.Run("", checkError(p, c.in, want.(string)))
	}
}

func TestParseErrMirBSDKorn(t *testing.T) {
	t.Parallel()
	p := NewParser(KeepComments(true), Variant(LangMirBSDKorn))
	for _, c := range shellTests {
		want := c.common
		if c.bsmk != nil {
			want = c.bsmk
		}
		if c.mksh != nil {
			want = c.mksh
		}
		if want == nil {
			continue
		}
		t.Run("", checkError(p, c.in, want.(string)))
	}
}

func TestInputName(t *testing.T) {
	t.Parallel()
	in := "("
	want := "some-file.sh:1:1: reached EOF without matching ( with )"
	p := NewParser()
	_, err := p.Parse(strings.NewReader(in), "some-file.sh")
	if err == nil {
		t.Fatalf("Expected error in %q: %v", in, want)
	}
	got := err.Error()
	if got != want {
		t.Fatalf("Error mismatch in %q\nwant: %s\ngot:  %s",
			in, want, got)
	}
}

var errBadReader = fmt.Errorf("write: expected error")

type badReader struct{}

func (b badReader) Read(p []byte) (int, error) { return 0, errBadReader }

func TestReadErr(t *testing.T) {
	t.Parallel()
	p := NewParser()
	_, err := p.Parse(badReader{}, "")
	if err == nil {
		t.Fatalf("Expected error with bad reader")
	}
	if err != errBadReader {
		t.Fatalf("Error mismatch with bad reader:\nwant: %v\ngot:  %v",
			errBadReader, err)
	}
}

type strictStringReader struct {
	*strings.Reader
	gaveEOF bool
}

func newStrictReader(s string) *strictStringReader {
	return &strictStringReader{Reader: strings.NewReader(s)}
}

func (r *strictStringReader) Read(p []byte) (int, error) {
	n, err := r.Reader.Read(p)
	if err == io.EOF {
		if r.gaveEOF {
			return n, fmt.Errorf("duplicate EOF read")
		}
		r.gaveEOF = true
	}
	return n, err
}

func TestParseStmts(t *testing.T) {
	t.Parallel()
	p := NewParser()
	inReader, inWriter := io.Pipe()
	recv := make(chan bool, 10)
	errc := make(chan error, 1)
	go func() {
		errc <- p.Stmts(inReader, func(s *Stmt) bool {
			recv <- true
			return true
		})
	}()
	io.WriteString(inWriter, "foo\n")
	<-recv
	io.WriteString(inWriter, "bar; baz")
	inWriter.Close()
	<-recv
	<-recv
	if err := <-errc; err != nil {
		t.Fatalf("Expected no error: %v", err)
	}
}

func TestParseStmtsStopEarly(t *testing.T) {
	t.Parallel()
	p := NewParser()
	inReader, inWriter := io.Pipe()
	defer inWriter.Close()
	recv := make(chan bool, 10)
	errc := make(chan error, 1)
	go func() {
		errc <- p.Stmts(inReader, func(s *Stmt) bool {
			recv <- true
			return !s.Background
		})
	}()
	io.WriteString(inWriter, "a\n")
	<-recv
	io.WriteString(inWriter, "b &\n") // stop here
	<-recv
	if err := <-errc; err != nil {
		t.Fatalf("Expected no error: %v", err)
	}
}

func TestParseStmtsError(t *testing.T) {
	t.Parallel()
	for _, in := range []string{
		"foo; )",
		"bar; <<EOF",
	} {
		t.Run("", func(t *testing.T) {
			p := NewParser()
			recv := make(chan bool, 10)
			errc := make(chan error, 1)
			go func() {
				errc <- p.Stmts(strings.NewReader(in), func(s *Stmt) bool {
					recv <- true
					return true
				})
			}()
			<-recv
			if err := <-errc; err == nil {
				t.Fatalf("Expected an error in %q, but got nil", in)
			}
		})
	}
}

func TestParseWords(t *testing.T) {
	t.Parallel()
	p := NewParser()
	inReader, inWriter := io.Pipe()
	recv := make(chan bool, 10)
	errc := make(chan error, 1)
	go func() {
		errc <- p.Words(inReader, func(w *Word) bool {
			recv <- true
			return true
		})
	}()
	// TODO: Allow a single space to end parsing a word. At the moment, the
	// parser must read the next non-space token (the next literal or
	// newline, in this case) to finish parsing a word.
	io.WriteString(inWriter, "foo ")
	io.WriteString(inWriter, "bar\n")
	<-recv
	io.WriteString(inWriter, "baz etc")
	inWriter.Close()
	<-recv
	<-recv
	<-recv
	if err := <-errc; err != nil {
		t.Fatalf("Expected no error: %v", err)
	}
}

func TestParseWordsStopEarly(t *testing.T) {
	t.Parallel()
	p := NewParser()
	r := strings.NewReader("a\nb\nc\n")
	parsed := 0
	err := p.Words(r, func(w *Word) bool {
		parsed++
		return w.Lit() != "b"
	})
	if err != nil {
		t.Fatalf("Expected no error: %v", err)
	}
	if want := 2; parsed != want {
		t.Fatalf("wanted %d words parsed, got %d", want, parsed)
	}
}

func TestParseWordsError(t *testing.T) {
	t.Parallel()
	in := "foo )"
	p := NewParser()
	recv := make(chan bool, 10)
	errc := make(chan error, 1)
	go func() {
		errc <- p.Words(strings.NewReader(in), func(w *Word) bool {
			recv <- true
			return true
		})
	}()
	<-recv
	want := "1:5: ) is not a valid word"
	got := fmt.Sprintf("%v", <-errc)
	if got != want {
		t.Fatalf("Expected %q as an error, but got %q", want, got)
	}
}

var documentTests = []struct {
	in   string
	want []WordPart
}{
	{
		"foo",
		[]WordPart{lit("foo")},
	},
	{
		" foo  $bar",
		[]WordPart{
			lit(" foo  "),
			litParamExp("bar"),
		},
	},
	{
		"$bar\n\n",
		[]WordPart{
			litParamExp("bar"),
			lit("\n\n"),
		},
	},
}

func TestParseDocument(t *testing.T) {
	t.Parallel()
	p := NewParser()

	for _, tc := range documentTests {
		t.Run("", func(t *testing.T) {
			got, err := p.Document(strings.NewReader(tc.in))
			if err != nil {
				t.Fatal(err)
			}
			recursiveSanityCheck(t, "", got)
			want := &Word{Parts: tc.want}
			qt.Assert(t, qt.CmpEquals(got, want, cmpOpt))
		})
	}
}

func TestParseDocumentError(t *testing.T) {
	t.Parallel()
	in := "foo $("
	p := NewParser()
	_, err := p.Document(strings.NewReader(in))
	want := "1:5: reached EOF without matching ( with )"
	got := fmt.Sprintf("%v", err)
	if got != want {
		t.Fatalf("Expected %q as an error, but got %q", want, got)
	}
}

var arithmeticTests = []struct {
	in   string
	want ArithmExpr
}{
	{
		"foo",
		litWord("foo"),
	},
	{
		"3 + 4",
		&BinaryArithm{
			Op: Add,
			X:  litWord("3"),
			Y:  litWord("4"),
		},
	},
	{
		"3 + 4 + 5",
		&BinaryArithm{
			Op: Add,
			X: &BinaryArithm{
				Op: Add,
				X:  litWord("3"),
				Y:  litWord("4"),
			},
			Y: litWord("5"),
		},
	},
	{
		"1 ? 0 : 2",
		&BinaryArithm{
			Op: TernQuest,
			X:  litWord("1"),
			Y: &BinaryArithm{
				Op: TernColon,
				X:  litWord("0"),
				Y:  litWord("2"),
			},
		},
	},
	{
		"a = 3, ++a, a--",
		&BinaryArithm{
			Op: Comma,
			X: &BinaryArithm{
				Op: Comma,
				X: &BinaryArithm{
					Op: Assgn,
					X:  litWord("a"),
					Y:  litWord("3"),
				},
				Y: &UnaryArithm{
					Op: Inc,
					X:  litWord("a"),
				},
			},
			Y: &UnaryArithm{
				Op:   Dec,
				Post: true,
				X:    litWord("a"),
			},
		},
	},
}

func TestParseArithmetic(t *testing.T) {
	t.Parallel()
	p := NewParser()

	for _, tc := range arithmeticTests {
		t.Run("", func(t *testing.T) {
			got, err := p.Arithmetic(strings.NewReader(tc.in))
			if err != nil {
				t.Fatal(err)
			}
			recursiveSanityCheck(t, "", got)
			qt.Assert(t, qt.CmpEquals(got, tc.want, cmpOpt))
		})
	}
}

func TestParseArithmeticError(t *testing.T) {
	t.Parallel()
	in := "3 +"
	p := NewParser()
	_, err := p.Arithmetic(strings.NewReader(in))
	want := "1:3: + must be followed by an expression"
	got := fmt.Sprintf("%v", err)
	if got != want {
		t.Fatalf("Expected %q as an error, but got %q", want, got)
	}
}

var stopAtTests = []struct {
	in   string
	stop string
	want any
}{
	{
		"foo bar", "$$",
		litCall("foo", "bar"),
	},
	{
		"$foo $", "$$",
		call(word(litParamExp("foo")), litWord("$")),
	},
	{
		"echo foo $$", "$$",
		litCall("echo", "foo"),
	},
	{
		"$$", "$$",
		&File{},
	},
	{
		"echo foo\n$$\n", "$$",
		litCall("echo", "foo"),
	},
	{
		"echo foo; $$", "$$",
		litCall("echo", "foo"),
	},
	{
		"echo foo; $$", "$$",
		litCall("echo", "foo"),
	},
	{
		"echo foo;$$", "$$",
		litCall("echo", "foo"),
	},
	{
		"echo '$$'", "$$",
		call(litWord("echo"), word(sglQuoted("$$"))),
	},
}

func TestParseStmtsStopAt(t *testing.T) {
	t.Parallel()
	for _, c := range stopAtTests {
		p := NewParser(StopAt(c.stop))
		want := fullProg(c.want)
		t.Run("", singleParse(p, c.in, want))
	}
}

func TestValidName(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name string
		in   string
		want bool
	}{
		{"Empty", "", false},
		{"Simple", "foo", true},
		{"MixedCase", "Foo", true},
		{"Underscore", "_foo", true},
		{"NumberPrefix", "3foo", false},
		{"NumberSuffix", "foo3", true},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := ValidName(tc.in)
			if got != tc.want {
				t.Fatalf("ValidName(%q) got %t, wanted %t",
					tc.in, got, tc.want)
			}
		})
	}
}

func TestIsIncomplete(t *testing.T) {
	t.Parallel()

	tests := []struct {
		in       string
		notWords bool
		want     bool
	}{
		{in: "foo\n", want: false},
		{in: "foo;", want: false},
		{in: "\n", want: false},
		{in: "badsyntax)", want: false},
		{in: "foo 'incomp", want: true},
		{in: `foo "incomp`, want: true},
		{in: "foo ${incomp", want: true},

		{in: "foo; 'incomp", notWords: true, want: true},
		{in: `foo; "incomp`, notWords: true, want: true},
		{in: " (incomp", notWords: true, want: true},
	}
	p := NewParser()
	for i, tc := range tests {
		t.Run(fmt.Sprintf("Parse%02d", i), func(t *testing.T) {
			r := strings.NewReader(tc.in)
			_, err := p.Parse(r, "")
			if got := IsIncomplete(err); got != tc.want {
				t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want)
			}
		})
		t.Run(fmt.Sprintf("Interactive%02d", i), func(t *testing.T) {
			r := strings.NewReader(tc.in)
			err := p.Interactive(r, func([]*Stmt) bool {
				return false
			})
			if got := IsIncomplete(err); got != tc.want {
				t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want)
			}
		})
		if !tc.notWords {
			t.Run(fmt.Sprintf("WordsSeq%02d", i), func(t *testing.T) {
				r := strings.NewReader(tc.in)
				var firstErr error
				for _, err := range p.WordsSeq(r) {
					if err != nil {
						firstErr = err
					}
				}
				if got := IsIncomplete(firstErr); got != tc.want {
					t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want)
				}
			})
		}
	}
}

func TestPosEdgeCases(t *testing.T) {
	in := "`\\\\foo`\n" + // one escaped backslash and 3 bytes
		"\x00foo\x00bar\n" // 8 bytes and newline
	p := NewParser()
	f, err := p.Parse(strings.NewReader(in), "")
	qt.Assert(t, qt.IsNil(err))
	cmdSubst := f.Stmts[0].Cmd.(*CallExpr).Args[0].Parts[0].(*CmdSubst)
	lit := cmdSubst.Stmts[0].Cmd.(*CallExpr).Args[0].Parts[0].(*Lit)

	qt.Check(t, qt.Equals(lit.Value, lit.Value))
	// Note that positions of literals with escape sequences inside backquote command substitutions
	// are weird, since we effectively skip over the double escaping in the literal value and positions.
	// Even though the input source has '\\foo' between columns 2 and 7 (length 5)
	// we end up keeping '\foo' between columns 3 and 7 (length 4).
	qt.Check(t, qt.Equals(lit.ValuePos.String(), "1:3"))
	qt.Check(t, qt.Equals(lit.ValueEnd.String(), "1:7"))

	// Check that we skip over null bytes when counting columns.
	qt.Check(t, qt.Equals(f.Stmts[1].Pos().String(), "2:2"))
	qt.Check(t, qt.Equals(f.Stmts[1].End().String(), "2:9"))
}

func TestParseRecoverErrors(t *testing.T) {
	t.Parallel()

	tests := []struct {
		src string

		wantErr     bool
		wantMissing int
	}{
		{src: "foo;"},
		{src: "foo"},
		{
			src:         "'incomp",
			wantMissing: 1,
		},
		{
			src:         "foo; 'incomp",
			wantMissing: 1,
		},
		{
			src:         "{ incomp",
			wantMissing: 1,
		},
		{
			src:         "(incomp",
			wantMissing: 1,
		},
		{
			src:         "(incomp; foo",
			wantMissing: 1,
		},
		{
			src:         "$(incomp",
			wantMissing: 1,
		},
		{
			src:         "((incomp",
			wantMissing: 1,
		},
		{
			src:         "$((incomp",
			wantMissing: 1,
		},
		{
			src:         "if foo",
			wantMissing: 3,
		},
		{
			src:         "if foo; then bar",
			wantMissing: 1,
		},
		{
			src:         "for i in 1 2 3; echo $i; done",
			wantMissing: 1,
		},
		{
			src:         `"incomp`,
			wantMissing: 1,
		},
		{
			src:         "`incomp",
			wantMissing: 1,
		},
		{
			src:         "incomp >",
			wantMissing: 1,
		},
		{
			src:         "${incomp",
			wantMissing: 1,
		},
		{
			src:         "incomp | ",
			wantMissing: 1,
		},
		{
			src:         "incomp || ",
			wantMissing: 1,
		},
		{
			src:         "incomp && ",
			wantMissing: 1,
		},
		{
			src:         `(one | { two >`,
			wantMissing: 3,
		},
		{
			src:         `(one > ; two | ); { three`,
			wantMissing: 3,
		},
		{
			src:     "badsyntax)",
			wantErr: true,
		},
	}
	parser := NewParser(RecoverErrors(3))
	printer := NewPrinter()
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			t.Logf("input: %s", tc.src)
			r := strings.NewReader(tc.src)
			f, err := parser.Parse(r, "")
			if tc.wantErr {
				qt.Assert(t, qt.Not(qt.IsNil(err)))
			} else {
				qt.Assert(t, qt.IsNil(err))
				switch len(f.Stmts) {
				case 0:
					t.Fatalf("result has no statements")
				case 1:
					if f.Stmts[0].Pos().IsRecovered() {
						t.Fatalf("result is only a recovered statement")
					}
				}
			}
			qt.Assert(t, qt.Equals(countRecoveredPositions(reflect.ValueOf(f)), tc.wantMissing))

			// Check that walking or printing the syntax tree still appears to work
			// even when the input source was incomplete.
			Walk(f, func(node Node) bool {
				if node == nil {
					return true
				}
				// Each position should either be valid, pointing to an offset within the input,
				// or invalid, which could be due to the position being recovered.
				for _, pos := range []Pos{node.Pos(), node.End()} {
					qt.Assert(t, qt.IsFalse(pos.IsValid() && pos.IsRecovered()), qt.Commentf("positions cannot be valid and recovered"))
					if !pos.IsValid() {
						qt.Assert(t, qt.Equals(pos.Offset(), 0), qt.Commentf("invalid positions have no offset"))
						qt.Assert(t, qt.Equals(pos.Line(), 0), qt.Commentf("invalid positions have no line"))
						qt.Assert(t, qt.Equals(pos.Col(), 0), qt.Commentf("invalid positions have no column"))
					}
				}
				return true
			})
			// Note that we don't particularly care about good formatting here.
			printer.Print(io.Discard, f)
		})
	}
}

func countRecoveredPositions(x reflect.Value) int {
	switch x.Kind() {
	case reflect.Interface:
		return countRecoveredPositions(x.Elem())
	case reflect.Ptr:
		if !x.IsNil() {
			return countRecoveredPositions(x.Elem())
		}
	case reflect.Slice:
		n := 0
		for i := range x.Len() {
			n += countRecoveredPositions(x.Index(i))
		}
		return n
	case reflect.Struct:
		if pos, ok := x.Interface().(Pos); ok {
			if pos.IsRecovered() {
				return 1
			}
			return 0
		}
		n := 0
		for i := range x.NumField() {
			n += countRecoveredPositions(x.Field(i))
		}
		return n
	}
	return 0
}
07070100000057000081A4000000000000000000000001686AE5C0000091CA000000000000000000000000000000000000001C00000000sh-3.12.0/syntax/printer.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"strings"
	"text/tabwriter"
	"unicode"

	"mvdan.cc/sh/v3/fileutil"
)

// PrinterOption is a function which can be passed to NewPrinter
// to alter its behavior. To apply option to existing Printer
// call it directly, for example KeepPadding(true)(printer).
type PrinterOption func(*Printer)

// Indent sets the number of spaces used for indentation. If set to 0,
// tabs will be used instead.
func Indent(spaces uint) PrinterOption {
	return func(p *Printer) { p.indentSpaces = spaces }
}

// BinaryNextLine will make binary operators appear on the next line
// when a binary command, such as a pipe, spans multiple lines. A
// backslash will be used.
func BinaryNextLine(enabled bool) PrinterOption {
	return func(p *Printer) { p.binNextLine = enabled }
}

// SwitchCaseIndent will make switch cases be indented. As such, switch
// case bodies will be two levels deeper than the switch itself.
func SwitchCaseIndent(enabled bool) PrinterOption {
	return func(p *Printer) { p.swtCaseIndent = enabled }
}

// TODO(v4): consider turning this into a "space all operators" option, to also
// allow foo=( bar baz ), (( x + y )), and so on.

// SpaceRedirects will put a space after most redirection operators. The
// exceptions are '>&', '<&', '>(', and '<('.
func SpaceRedirects(enabled bool) PrinterOption {
	return func(p *Printer) { p.spaceRedirects = enabled }
}

// KeepPadding will keep most nodes and tokens in the same column that
// they were in the original source. This allows the user to decide how
// to align and pad their code with spaces.
//
// 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

		} else if !enabled && p.keepPadding {
			// Ensure we reset the state to that of NewPrinter.
			p.keepPadding = false
			p.bufWriter = p.cols.Writer
			p.cols = colCounter{}
		}
	}
}

// Minify will print programs in a way to save the most bytes possible.
// For example, indentation and comments are skipped, and extra
// whitespace is avoided when possible.
func Minify(enabled bool) PrinterOption {
	return func(p *Printer) { p.minify = enabled }
}

// SingleLine will attempt to print programs in one line. For example, lists of
// commands or nested blocks do not use newlines in this mode. Note that some
// 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.
func SingleLine(enabled bool) PrinterOption {
	return func(p *Printer) { p.singleLine = enabled }
}

// FunctionNextLine will place a function's opening braces on the next line.
func FunctionNextLine(enabled bool) PrinterOption {
	return func(p *Printer) { p.funcNextLine = enabled }
}

// NewPrinter allocates a new Printer and applies any number of options.
func NewPrinter(opts ...PrinterOption) *Printer {
	p := &Printer{
		bufWriter: bufio.NewWriter(nil),
		tabWriter: new(tabwriter.Writer),
	}
	for _, opt := range opts {
		opt(p)
	}
	return p
}

// 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.
func (p *Printer) Print(w io.Writer, node Node) error {
	p.reset()

	if p.minify && p.singleLine {
		return fmt.Errorf("Minify and SingleLine together are not supported yet; please file an issue describing your use case: https://github.com/mvdan/sh/issues")
	}

	// TODO: consider adding a raw mode to skip the tab writer, much like in
	// go/printer.
	twmode := tabwriter.DiscardEmptyColumns | tabwriter.StripEscape
	tabwidth := 8
	if p.indentSpaces == 0 {
		// indenting with tabs
		twmode |= tabwriter.TabIndent
	} else {
		// indenting with spaces
		tabwidth = int(p.indentSpaces)
	}
	p.tabWriter.Init(w, 0, tabwidth, 1, ' ', twmode)
	w = p.tabWriter

	p.bufWriter.Reset(w)
	switch node := node.(type) {
	case *File:
		p.stmtList(node.Stmts, node.Last)
		p.newline(Pos{})
	case *Stmt:
		p.stmtList([]*Stmt{node}, nil)
	case Command:
		p.command(node, nil)
	case *Word:
		p.line = node.Pos().Line()
		p.word(node)
	case WordPart:
		p.line = node.Pos().Line()
		p.wordPart(node, nil)
	case *Assign:
		p.line = node.Pos().Line()
		p.assigns([]*Assign{node})
	default:
		return fmt.Errorf("unsupported node type: %T", node)
	}
	p.flushHeredocs()
	p.flushComments()

	// flush the writers
	if err := p.bufWriter.Flush(); err != nil {
		return err
	}
	if tw, _ := w.(*tabwriter.Writer); tw != nil {
		if err := tw.Flush(); err != nil {
			return err
		}
	}
	return nil
}

type bufWriter interface {
	Write([]byte) (int, error)
	WriteString(string) (int, error)
	WriteByte(byte) error
	Reset(io.Writer)
	Flush() error
}

type colCounter struct {
	*bufio.Writer
	column    int
	lineStart bool
}

func (c *colCounter) addByte(b byte) {
	switch b {
	case '\n':
		c.column = 0
		c.lineStart = true
	case '\t', ' ', tabwriter.Escape:
	default:
		c.lineStart = false
	}
	c.column++
}

func (c *colCounter) WriteByte(b byte) error {
	c.addByte(b)
	return c.Writer.WriteByte(b)
}

func (c *colCounter) WriteString(s string) (int, error) {
	for _, b := range []byte(s) {
		c.addByte(b)
	}
	return c.Writer.WriteString(s)
}

func (c *colCounter) Reset(w io.Writer) {
	c.column = 1
	c.lineStart = true
	c.Writer.Reset(w)
}

// Printer holds the internal state of the printing mechanism of a
// program.
type Printer struct {
	bufWriter // TODO: embedding this makes the methods part of the API, which we did not intend
	tabWriter *tabwriter.Writer
	cols      colCounter

	indentSpaces   uint
	binNextLine    bool
	swtCaseIndent  bool
	spaceRedirects bool
	keepPadding    bool
	minify         bool
	singleLine     bool
	funcNextLine   bool

	wantSpace wantSpaceState // whether space is required or has been written

	wantNewline bool // newline is wanted for pretty-printing; ignored by singleLine; ignored by singleLine
	mustNewline bool // newline is required to keep shell syntax valid
	wroteSemi   bool // wrote ';' for the current statement

	// pendingComments are any comments in the current line or statement
	// that we have yet to print. This is useful because that way, we can
	// ensure that all comments are written immediately before a newline.
	// Otherwise, in some edge cases we might wrongly place words after a
	// comment in the same line, breaking programs.
	pendingComments []Comment

	// firstLine means we are still writing the first line
	firstLine bool
	// line is the current line number
	line uint

	// lastLevel is the last level of indentation that was used.
	lastLevel uint
	// level is the current level of indentation.
	level uint
	// levelIncs records which indentation level increments actually
	// took place, to revert them once their section ends.
	levelIncs []bool

	nestedBinary bool

	// pendingHdocs is the list of pending heredocs to write.
	pendingHdocs []*Redirect

	// used when printing <<- heredocs with tab indentation
	tabsPrinter *Printer
}

func (p *Printer) reset() {
	p.wantSpace = spaceWritten
	p.wantNewline, p.mustNewline = false, false
	p.pendingComments = p.pendingComments[:0]

	// minification uses its own newline logic
	p.firstLine = !p.minify
	p.line = 0

	p.lastLevel, p.level = 0, 0
	p.levelIncs = p.levelIncs[:0]
	p.nestedBinary = false
	p.pendingHdocs = p.pendingHdocs[:0]
}

func (p *Printer) spaces(n uint) {
	for range n {
		p.WriteByte(' ')
	}
}

func (p *Printer) space() {
	p.WriteByte(' ')
	p.wantSpace = spaceWritten
}

func (p *Printer) spacePad(pos Pos) {
	if p.cols.lineStart && p.indentSpaces == 0 {
		// Never add padding at the start of a line unless we are indenting
		// with spaces, since this may result in mixing of spaces and tabs.
		return
	}
	if p.wantSpace == spaceRequired {
		p.WriteByte(' ')
		p.wantSpace = spaceWritten
	}
	for p.cols.column > 0 && p.cols.column < int(pos.Col()) {
		p.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, escapingNewline bool) bool {
	if p.mustNewline {
		// We must have a newline here.
		return true
	}
	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
	}
	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
}

func (p *Printer) bslashNewl() {
	if p.wantSpace == spaceRequired {
		p.space()
	}
	p.WriteString("\\\n")
	p.line++
	p.indent()
}

func (p *Printer) spacedString(s string, pos Pos) {
	p.spacePad(pos)
	p.WriteString(s)
	p.wantSpace = spaceRequired
}

func (p *Printer) spacedToken(s string, pos Pos) {
	if p.minify {
		p.WriteString(s)
		p.wantSpace = spaceNotRequired
		return
	}
	p.spacePad(pos)
	p.WriteString(s)
	p.wantSpace = spaceRequired
}

func (p *Printer) semiOrNewl(s string, pos Pos) {
	if p.wantsNewline(Pos{}, false) {
		p.newline(pos)
		p.indent()
	} else {
		if !p.wroteSemi {
			p.WriteByte(';')
		}
		if !p.minify {
			p.space()
		}
		p.advanceLine(pos.Line())
	}
	p.WriteString(s)
	p.wantSpace = spaceRequired
}

func (p *Printer) writeLit(s string) {
	// If p.tabWriter is nil, this is the nested printer being used to print
	// <<- 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.WriteString(s)
}

func (p *Printer) incLevel() {
	inc := false
	if p.level <= p.lastLevel || len(p.levelIncs) == 0 {
		p.level++
		inc = true
	} else if last := &p.levelIncs[len(p.levelIncs)-1]; *last {
		*last = false
		inc = true
	}
	p.levelIncs = append(p.levelIncs, inc)
}

func (p *Printer) decLevel() {
	if p.levelIncs[len(p.levelIncs)-1] {
		p.level--
	}
	p.levelIncs = p.levelIncs[:len(p.levelIncs)-1]
}

func (p *Printer) indent() {
	if p.minify {
		return
	}
	p.lastLevel = p.level
	switch {
	case p.level == 0:
	case p.indentSpaces == 0:
		p.WriteByte(tabwriter.Escape)
		for i := uint(0); i < p.level; i++ {
			p.WriteByte('\t')
		}
		p.WriteByte(tabwriter.Escape)
	default:
		p.spaces(p.indentSpaces * p.level)
	}
}

// TODO(mvdan): add an indent call at the end of newline?

// newline prints one newline and advances p.line to pos.Line().
func (p *Printer) newline(pos Pos) {
	p.flushHeredocs()
	p.flushComments()
	p.WriteByte('\n')
	p.wantSpace = spaceWritten
	p.wantNewline, p.mustNewline = false, false
	p.advanceLine(pos.Line())
}

func (p *Printer) advanceLine(line uint) {
	if p.line < line {
		p.line = line
	}
}

func (p *Printer) flushHeredocs() {
	if len(p.pendingHdocs) == 0 {
		return
	}
	hdocs := p.pendingHdocs
	p.pendingHdocs = p.pendingHdocs[:0]
	coms := p.pendingComments
	p.pendingComments = nil
	if len(coms) > 0 {
		c := coms[0]
		if c.Pos().Line() == p.line {
			p.pendingComments = append(p.pendingComments, c)
			p.flushComments()
			coms = coms[1:]
		}
	}

	// Reuse the last indentation level, as
	// indentation levels are usually changed before
	// newlines are printed along with their
	// subsequent indentation characters.
	newLevel := p.level
	p.level = p.lastLevel

	for _, r := range hdocs {
		p.line++
		p.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,
					baseIndent:  int(p.level + 1),
					firstIndent: -1,
				}
				p.tabsPrinter = &Printer{
					bufWriter: &extra,

					// The options need to persist.
					indentSpaces:   p.indentSpaces,
					binNextLine:    p.binNextLine,
					swtCaseIndent:  p.swtCaseIndent,
					spaceRedirects: p.spaceRedirects,
					keepPadding:    p.keepPadding,
					minify:         p.minify,
					funcNextLine:   p.funcNextLine,

					line: r.Hdoc.Pos().Line(),
				}
				p.tabsPrinter.wordParts(r.Hdoc.Parts, true)
			}
			p.indent()
		} else if r.Hdoc != nil {
			p.wordParts(r.Hdoc.Parts, true)
		}
		p.unquotedWord(r.Word)
		if r.Hdoc != nil {
			// Overwrite p.line, since printing r.Word again can set
			// p.line to the beginning of the heredoc again.
			p.advanceLine(r.Hdoc.End().Line())
		}
		p.wantSpace = spaceNotRequired
	}
	p.level = newLevel
	p.pendingComments = coms
	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, false) {
		return
	}
	p.flushHeredocs()
	p.flushComments()
	p.WriteByte('\n')
	p.wantSpace = spaceWritten
	p.wantNewline, p.mustNewline = false, false

	l := pos.Line()
	if l > p.line+1 && !p.minify {
		p.WriteByte('\n') // preserve single empty lines
	}
	p.advanceLine(l)
	p.indent()
}

func (p *Printer) rightParen(pos Pos) {
	if len(p.pendingHdocs) > 0 || !p.minify {
		p.newlines(pos)
	}
	p.WriteByte(')')
	p.wantSpace = spaceRequired
}

func (p *Printer) semiRsrv(s string, pos Pos) {
	if p.wantsNewline(pos, false) {
		p.newlines(pos)
	} else {
		if !p.wroteSemi {
			p.WriteByte(';')
		}
		if !p.minify {
			p.spacePad(pos)
		}
	}
	p.WriteString(s)
	p.wantSpace = spaceRequired
}

func (p *Printer) flushComments() {
	for i, c := range p.pendingComments {
		if i == 0 {
			// Flush any pending heredocs first. Otherwise, the
			// comments would become part of a heredoc body.
			p.flushHeredocs()
		}
		p.firstLine = false
		// We can't call any of the newline methods, as they call this
		// function and we'd recurse forever.
		cline := c.Hash.Line()
		switch {
		case p.mustNewline, i > 0, cline > p.line && p.line > 0:
			p.WriteByte('\n')
			if cline > p.line+1 {
				p.WriteByte('\n')
			}
			p.indent()
			p.wantSpace = spaceWritten
			p.spacePad(c.Pos())
		case p.wantSpace == spaceRequired:
			if p.keepPadding {
				p.spacePad(c.Pos())
			} else {
				p.WriteByte('\t')
			}
		case p.wantSpace != spaceWritten:
			p.space()
		}
		// don't go back one line, which may happen in some edge cases
		p.advanceLine(cline)
		p.WriteByte('#')
		p.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace))
		p.wantNewline = true
		p.mustNewline = true
	}
	p.pendingComments = nil
}

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.line++
			}
		}
		return
	}
	p.pendingComments = append(p.pendingComments, comments...)
}

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) {
			next = wps[i+1]
		}
		// Keep escaped newlines separating word parts when quoted.
		// Note that those escaped newlines don't cause indentaiton.
		// When not quoted, we strip them out consistently,
		// because attempting to keep them would prevent indentation.
		// 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.line++
		}
		p.wordPart(wp, next)
		p.advanceLine(wp.End().Line())
	}
}

func (p *Printer) wordPart(wp, next WordPart) {
	switch wp := wp.(type) {
	case *Lit:
		p.writeLit(wp.Value)
	case *SglQuoted:
		if wp.Dollar {
			p.WriteByte('$')
		}
		p.WriteByte('\'')
		p.writeLit(wp.Value)
		p.WriteByte('\'')
		p.advanceLine(wp.End().Line())
	case *DblQuoted:
		p.dblQuoted(wp)
	case *CmdSubst:
		p.advanceLine(wp.Pos().Line())
		switch {
		case wp.TempFile:
			p.WriteString("${")
			p.wantSpace = spaceRequired
			p.nestedStmts(wp.Stmts, wp.Last, wp.Right)
			p.wantSpace = spaceNotRequired
			p.semiRsrv("}", wp.Right)
		case wp.ReplyVar:
			p.WriteString("${|")
			p.nestedStmts(wp.Stmts, wp.Last, wp.Right)
			p.wantSpace = spaceNotRequired
			p.semiRsrv("}", wp.Right)
		// Special case: `# inline comment`
		case wp.Backquotes && len(wp.Stmts) == 0 &&
			len(wp.Last) == 1 && wp.Right.Line() == p.line:
			p.WriteString("`#")
			p.WriteString(wp.Last[0].Text)
			p.WriteString("`")
		default:
			p.WriteString("$(")
			if len(wp.Stmts) > 0 && startsWithLparen(wp.Stmts[0]) {
				p.wantSpace = spaceRequired
			} else {
				p.wantSpace = spaceNotRequired
			}
			p.nestedStmts(wp.Stmts, wp.Last, wp.Right)
			p.rightParen(wp.Right)
		}
	case *ParamExp:
		litCont := ";"
		if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" {
			litCont = nextLit.Value[:1]
		}
		name := wp.Param.Value
		switch {
		case !p.minify:
		case wp.Excl, wp.Length, wp.Width:
		case wp.Index != nil, wp.Slice != nil:
		case wp.Repl != nil, wp.Exp != nil:
		case len(name) > 1 && !ValidName(name): // ${10}
		case ValidName(name + litCont): // ${var}cont
		default:
			x2 := *wp
			x2.Short = true
			p.paramExp(&x2)
			return
		}
		p.paramExp(wp)
	case *ArithmExp:
		p.WriteString("$((")
		if wp.Unsigned {
			p.WriteString("# ")
		}
		p.arithmExpr(wp.X, false, false)
		p.WriteString("))")
	case *ExtGlob:
		p.WriteString(wp.Op.String())
		p.writeLit(wp.Pattern.Value)
		p.WriteByte(')')
	case *ProcSubst:
		// avoid conflict with << and others
		if p.wantSpace == spaceRequired {
			p.space()
		}
		p.WriteString(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.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.line++
	}
	p.WriteByte('"')
}

func (p *Printer) wroteIndex(index ArithmExpr) bool {
	if index == nil {
		return false
	}
	p.WriteByte('[')
	p.arithmExpr(index, false, false)
	p.WriteByte(']')
	return true
}

func (p *Printer) paramExp(pe *ParamExp) {
	if pe.nakedIndex() { // arr[x]
		p.writeLit(pe.Param.Value)
		p.wroteIndex(pe.Index)
		return
	}
	if pe.Short { // $var
		p.WriteByte('$')
		p.writeLit(pe.Param.Value)
		return
	}
	// ${var...}
	p.WriteString("${")
	switch {
	case pe.Length:
		p.WriteByte('#')
	case pe.Width:
		p.WriteByte('%')
	case pe.Excl:
		p.WriteByte('!')
	}
	p.writeLit(pe.Param.Value)
	p.wroteIndex(pe.Index)
	switch {
	case pe.Slice != nil:
		p.WriteByte(':')
		p.arithmExpr(pe.Slice.Offset, true, true)
		if pe.Slice.Length != nil {
			p.WriteByte(':')
			p.arithmExpr(pe.Slice.Length, true, false)
		}
	case pe.Repl != nil:
		if pe.Repl.All {
			p.WriteByte('/')
		}
		p.WriteByte('/')
		if pe.Repl.Orig != nil {
			p.word(pe.Repl.Orig)
		}
		p.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())
		if pe.Exp.Word != nil {
			p.word(pe.Exp.Word)
		}
	}
	p.WriteByte('}')
}

func (p *Printer) loop(loop Loop) {
	switch loop := loop.(type) {
	case *WordIter:
		p.writeLit(loop.Name.Value)
		if loop.InPos.IsValid() {
			p.spacedString(" in", Pos{})
			p.wordJoin(loop.Items)
		}
	case *CStyleLoop:
		p.WriteString("((")
		if loop.Init == nil {
			p.space()
		}
		p.arithmExpr(loop.Init, false, false)
		p.WriteString("; ")
		p.arithmExpr(loop.Cond, false, false)
		p.WriteString("; ")
		p.arithmExpr(loop.Post, false, false)
		p.WriteString("))")
	}
}

func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) {
	if p.minify {
		compact = true
	}
	switch expr := expr.(type) {
	case *Word:
		p.word(expr)
	case *BinaryArithm:
		if compact {
			p.arithmExpr(expr.X, compact, spacePlusMinus)
			p.WriteString(expr.Op.String())
			p.arithmExpr(expr.Y, compact, false)
		} else {
			p.arithmExpr(expr.X, compact, spacePlusMinus)
			if expr.Op != Comma {
				p.space()
			}
			p.WriteString(expr.Op.String())
			p.space()
			p.arithmExpr(expr.Y, compact, false)
		}
	case *UnaryArithm:
		if expr.Post {
			p.arithmExpr(expr.X, compact, spacePlusMinus)
			p.WriteString(expr.Op.String())
		} else {
			if spacePlusMinus {
				switch expr.Op {
				case Plus, Minus:
					p.space()
				}
			}
			p.WriteString(expr.Op.String())
			p.arithmExpr(expr.X, compact, false)
		}
	case *ParenArithm:
		p.WriteByte('(')
		p.arithmExpr(expr.X, false, false)
		p.WriteByte(')')
	}
}

func (p *Printer) testExpr(expr TestExpr) {
	// Multi-line test expressions don't need to escape newlines.
	if expr.Pos().Line() > p.line {
		p.newlines(expr.Pos())
		p.spacePad(expr.Pos())
	} else if p.wantSpace == spaceRequired {
		p.space()
	}
	p.testExprSameLine(expr)
}

func (p *Printer) testExprSameLine(expr TestExpr) {
	p.advanceLine(expr.Pos().Line())
	switch expr := expr.(type) {
	case *Word:
		p.word(expr)
	case *BinaryTest:
		p.testExprSameLine(expr.X)
		p.space()
		p.WriteString(expr.Op.String())
		switch expr.Op {
		case AndTest, OrTest:
			p.wantSpace = spaceRequired
			p.testExpr(expr.Y)
		default:
			p.space()
			p.testExprSameLine(expr.Y)
		}
	case *UnaryTest:
		p.WriteString(expr.Op.String())
		p.space()
		p.testExprSameLine(expr.X)
	case *ParenTest:
		p.WriteByte('(')
		if startsWithLparen(expr.X) {
			p.wantSpace = spaceRequired
		} else {
			p.wantSpace = spaceNotRequired
		}
		p.testExpr(expr.X)
		p.WriteByte(')')
	}
}

func (p *Printer) word(w *Word) {
	p.wordParts(w.Parts, false)
	p.wantSpace = spaceRequired
}

func (p *Printer) unquotedWord(w *Word) {
	for _, wp := range w.Parts {
		switch wp := wp.(type) {
		case *SglQuoted:
			p.writeLit(wp.Value)
		case *DblQuoted:
			p.wordParts(wp.Parts, true)
		case *Lit:
			for i := 0; i < len(wp.Value); i++ {
				if b := wp.Value[i]; b == '\\' {
					if i++; i < len(wp.Value) {
						p.WriteByte(wp.Value[i])
					}
				} else {
					p.WriteByte(b)
				}
			}
		}
	}
}

func (p *Printer) wordJoin(ws []*Word) {
	anyNewline := false
	for _, w := range ws {
		if pos := w.Pos(); pos.Line() > p.line && !p.singleLine {
			if !anyNewline {
				p.incLevel()
				anyNewline = true
			}
			p.bslashNewl()
		}
		p.spacePad(w.Pos())
		p.word(w)
	}
	if anyNewline {
		p.decLevel()
	}
}

func (p *Printer) casePatternJoin(pats []*Word) {
	anyNewline := false
	for i, w := range pats {
		if i > 0 {
			p.spacedToken("|", Pos{})
		}
		if p.wantsNewline(w.Pos(), true) {
			if !anyNewline {
				p.incLevel()
				anyNewline = true
			}
			p.bslashNewl()
		} else {
			p.spacePad(w.Pos())
		}
		p.word(w)
	}
	if anyNewline {
		p.decLevel()
	}
}

func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) {
	p.incLevel()
	for _, el := range elems {
		var left []Comment
		for _, c := range el.Comments {
			if c.Pos().After(el.Pos()) {
				left = append(left, c)
				break
			}
			p.comments(c)
		}
		// Multi-line array expressions don't need to escape newlines.
		if el.Pos().Line() > p.line {
			p.newlines(el.Pos())
			p.spacePad(el.Pos())
		} else if p.wantSpace == spaceRequired {
			p.space()
		}
		if p.wroteIndex(el.Index) {
			p.WriteByte('=')
		}
		if el.Value != nil {
			p.word(el.Value)
		}
		p.comments(left...)
	}
	if len(last) > 0 {
		p.comments(last...)
		p.flushComments()
	}
	p.decLevel()
}

func (p *Printer) stmt(s *Stmt) {
	p.wroteSemi = false
	if s.Negated {
		p.spacedString("!", s.Pos())
	}
	var startRedirs int
	if s.Cmd != nil {
		startRedirs = p.command(s.Cmd, s.Redirs)
	}
	p.incLevel()
	for _, r := range s.Redirs[startRedirs:] {
		if p.wantsNewline(r.OpPos, true) {
			p.bslashNewl()
		}
		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)
		if r.Op == Hdoc || r.Op == DashHdoc {
			p.pendingHdocs = append(p.pendingHdocs, r)
		}
	}
	sep := s.Semicolon.IsValid() && s.Semicolon.Line() > p.line && !p.singleLine
	if sep || s.Background || s.Coprocess {
		if sep {
			p.bslashNewl()
		} else if !p.minify {
			p.space()
		}
		if s.Background {
			p.WriteString("&")
		} else if s.Coprocess {
			p.WriteString("|&")
		} else {
			p.WriteString(";")
		}
		p.wroteSemi = true
		p.wantSpace = spaceRequired
	}
	p.decLevel()
}

func (p *Printer) printRedirsUntil(redirs []*Redirect, startRedirs int, pos Pos) int {
	for _, r := range redirs[startRedirs:] {
		if r.Pos().After(pos) || r.Op == Hdoc || r.Op == DashHdoc {
			break
		}
		if p.wantSpace == spaceRequired {
			p.spacePad(r.Pos())
		}
		if r.N != nil {
			p.writeLit(r.N.Value)
		}
		p.WriteString(r.Op.String())
		if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) {
			p.space()
		} else {
			p.wantSpace = spaceRequired
		}
		p.word(r.Word)
		startRedirs++
	}
	return startRedirs
}

func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
	p.advanceLine(cmd.Pos().Line())
	p.spacePad(cmd.Pos())
	switch cmd := cmd.(type) {
	case *CallExpr:
		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(cmd.Args[:1])
		startRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[1].Pos())
		p.wordJoin(cmd.Args[1:])
	case *Block:
		p.WriteByte('{')
		p.wantSpace = spaceRequired
		// Forbid "foo()\n{ bar; }"
		p.wantNewline = p.wantNewline || p.funcNextLine
		p.nestedStmts(cmd.Stmts, cmd.Last, cmd.Rbrace)
		p.semiRsrv("}", cmd.Rbrace)
	case *IfClause:
		p.ifClause(cmd, false)
	case *Subshell:
		p.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 {
			p.wantSpace = spaceNotRequired
		}

		p.spacePad(stmtsPos(cmd.Stmts, cmd.Last))
		p.nestedStmts(cmd.Stmts, cmd.Last, cmd.Rparen)
		p.wantSpace = spaceNotRequired
		p.spacePad(cmd.Rparen)
		p.rightParen(cmd.Rparen)
	case *WhileClause:
		if cmd.Until {
			p.spacedString("until", cmd.Pos())
		} else {
			p.spacedString("while", cmd.Pos())
		}
		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 cmd.Select {
			p.WriteString("select ")
		} else {
			p.WriteString("for ")
		}
		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(cmd.X)
		if p.minify || p.singleLine || cmd.Y.Pos().Line() <= p.line {
			// leave p.nestedBinary untouched
			p.spacedToken(cmd.Op.String(), cmd.OpPos)
			p.advanceLine(cmd.Y.Pos().Line())
			p.stmt(cmd.Y)
			break
		}
		indent := !p.nestedBinary
		if indent {
			p.incLevel()
		}
		if p.binNextLine {
			if len(p.pendingHdocs) == 0 {
				p.bslashNewl()
			}
			p.spacedToken(cmd.Op.String(), cmd.OpPos)
			if len(cmd.Y.Comments) > 0 {
				p.wantSpace = spaceNotRequired
				p.newline(cmd.Y.Pos())
				p.indent()
				p.comments(cmd.Y.Comments...)
				p.newline(Pos{})
				p.indent()
			}
		} else {
			p.spacedToken(cmd.Op.String(), cmd.OpPos)
			p.advanceLine(cmd.OpPos.Line())
			p.comments(cmd.Y.Comments...)
			p.newline(Pos{})
			p.indent()
		}
		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 cmd.RsrvWord {
			p.WriteString("function ")
		}
		p.writeLit(cmd.Name.Value)
		if !cmd.RsrvWord || cmd.Parens {
			p.WriteString("()")
		}
		if p.funcNextLine {
			p.newline(Pos{})
			p.indent()
		} else if !cmd.Parens || !p.minify {
			p.space()
		}
		p.advanceLine(cmd.Body.Pos().Line())
		p.comments(cmd.Body.Comments...)
		p.stmt(cmd.Body)
	case *CaseClause:
		p.WriteString("case ")
		p.word(cmd.Word)
		p.WriteString(" in")
		p.advanceLine(cmd.In.Line())
		p.wantSpace = spaceRequired
		if p.swtCaseIndent {
			p.incLevel()
		}
		if len(cmd.Items) == 0 {
			// Apparently "case x in; esac" is invalid shell.
			p.mustNewline = true
		}
		for i, ci := range cmd.Items {
			var last []Comment
			for i, c := range ci.Comments {
				if c.Pos().After(ci.Pos()) {
					last = ci.Comments[i:]
					break
				}
				p.comments(c)
			}
			p.newlines(ci.Pos())
			p.spacePad(ci.Pos())
			p.casePatternJoin(ci.Patterns)
			p.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(cmd.Items)-1 {
				if sep {
					p.newlines(ci.OpPos)
					p.wantNewline = true
				}
				p.spacedToken(ci.Op.String(), ci.OpPos)
				p.advanceLine(ci.OpPos.Line())
				// avoid ; directly after tokens like ;;
				p.wroteSemi = true
			}
			p.comments(last...)
			p.flushComments()
			p.level--
		}
		p.comments(cmd.Last...)
		if p.swtCaseIndent {
			p.flushComments()
			p.decLevel()
		}
		p.semiRsrv("esac", cmd.Esac)
	case *ArithmCmd:
		p.WriteString("((")
		if cmd.Unsigned {
			p.WriteString("# ")
		}
		p.arithmExpr(cmd.X, false, false)
		p.WriteString("))")
	case *TestClause:
		p.WriteString("[[ ")
		p.incLevel()
		p.testExpr(cmd.X)
		p.decLevel()
		p.spacedString("]]", cmd.Right)
	case *DeclClause:
		p.spacedString(cmd.Variant.Value, cmd.Pos())
		p.assigns(cmd.Args)
	case *TimeClause:
		p.spacedString("time", cmd.Pos())
		if cmd.PosixFormat {
			p.spacedString("-p", cmd.Pos())
		}
		if cmd.Stmt != nil {
			p.stmt(cmd.Stmt)
		}
	case *CoprocClause:
		p.spacedString("coproc", cmd.Pos())
		if cmd.Name != nil {
			p.space()
			p.word(cmd.Name)
		}
		p.space()
		p.stmt(cmd.Stmt)
	case *LetClause:
		p.spacedString("let", cmd.Pos())
		for _, n := range cmd.Exprs {
			p.space()
			p.arithmExpr(n, true, false)
		}
	case *TestDecl:
		p.spacedString("@test", cmd.Pos())
		p.space()
		p.word(cmd.Description)
		p.space()
		p.stmt(cmd.Body)
	default:
		panic(fmt.Sprintf("syntax.Printer: unexpected node type %T", cmd))
	}
	return startRedirs
}

func (p *Printer) ifClause(ic *IfClause, elif bool) {
	if !elif {
		p.spacedString("if", ic.Pos())
	}
	p.nestedStmts(ic.Cond, ic.CondLast, Pos{})
	p.semiOrNewl("then", ic.ThenPos)
	thenEnd := ic.FiPos
	el := ic.Else
	if el != nil {
		thenEnd = el.Position
	}
	p.nestedStmts(ic.Then, ic.ThenLast, thenEnd)

	if el != nil && el.ThenPos.IsValid() {
		p.comments(ic.Last...)
		p.semiRsrv("elif", el.Position)
		p.ifClause(el, true)
		return
	}
	if el == nil {
		p.comments(ic.Last...)
	} else {
		var left []Comment
		for _, c := range ic.Last {
			if c.Pos().After(el.Position) {
				left = append(left, c)
				break
			}
			p.comments(c)
		}
		p.semiRsrv("else", el.Position)
		p.comments(left...)
		p.nestedStmts(el.Then, el.ThenLast, ic.FiPos)
		p.comments(el.Last...)
	}
	p.semiRsrv("fi", ic.FiPos)
}

func (p *Printer) stmtList(stmts []*Stmt, last []Comment) {
	sep := p.wantNewline || (len(stmts) > 0 && stmts[0].Pos().Line() > p.line)
	for i, s := range stmts {
		if i > 0 && p.singleLine && p.wantNewline && !p.wroteSemi {
			// In singleLine mode, ensure we use semicolons between
			// statements.
			p.WriteByte(';')
			p.wantSpace = spaceRequired
		}
		pos := s.Pos()
		var midComs, endComs []Comment
		for _, c := range s.Comments {
			// Comments after the end of this command. Note that
			// this includes "<<EOF # comment".
			if s.Cmd != nil && c.End().After(s.Cmd.End()) {
				endComs = append(endComs, c)
				break
			}
			// Comments between the beginning of the statement and
			// the end of the command.
			if c.Pos().After(pos) {
				midComs = append(midComs, c)
				continue
			}
			// The rest of the comments are before the entire
			// statement.
			p.comments(c)
		}
		if p.mustNewline || !p.minify || p.wantSpace == spaceRequired {
			p.newlines(pos)
		}
		p.advanceLine(pos.Line())
		p.comments(midComs...)
		p.stmt(s)
		p.comments(endComs...)
		p.wantNewline = true
	}
	if len(stmts) == 1 && !sep {
		p.wantNewline = false
	}
	p.comments(last...)
}

func (p *Printer) nestedStmts(stmts []*Stmt, last []Comment, closing Pos) {
	p.incLevel()
	switch {
	case len(stmts) > 1:
		// Force a newline if we find:
		//     { stmt; stmt; }
		p.wantNewline = true
	case closing.Line() > p.line && len(stmts) > 0 &&
		stmtsEnd(stmts, last).Line() < closing.Line():
		// Force a newline if we find:
		//     { stmt
		//     }
		p.wantNewline = true
	case len(p.pendingComments) > 0 && len(stmts) > 0:
		// Force a newline if we find:
		//     for i in a b # stmt
		//     do foo; done
		p.wantNewline = true
	}
	p.stmtList(stmts, last)
	if closing.IsValid() {
		p.flushComments()
	}
	p.decLevel()
}

func (p *Printer) assigns(assigns []*Assign) {
	p.incLevel()
	for _, a := range assigns {
		if p.wantsNewline(a.Pos(), true) {
			p.bslashNewl()
		} else {
			p.spacePad(a.Pos())
		}
		if a.Name != nil {
			p.writeLit(a.Name.Value)
			p.wroteIndex(a.Index)
			if a.Append {
				p.WriteByte('+')
			}
			if !a.Naked {
				p.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.advanceLine(a.Value.Pos().Line())
			p.word(a.Value)
		} else if a.Array != nil {
			p.wantSpace = spaceNotRequired
			p.WriteByte('(')
			p.elemJoin(a.Array.Elems, a.Array.Last)
			p.rightParen(a.Array.Rparen)
		}
		p.wantSpace = spaceRequired
	}
	p.decLevel()
}

type wantSpaceState uint8

const (
	spaceNotRequired wantSpaceState = iota
	spaceRequired                   // we should generally print a space or a newline next
	spaceWritten                    // we have just written a space or newline
)

// extraIndenter ensures that all lines in a '<<-' heredoc body have at least
// baseIndent leading tabs. Those that had more tab indentation than the first
// heredoc line will keep that relative indentation.
type extraIndenter struct {
	bufWriter
	baseIndent int

	firstIndent int
	firstChange int
	curLine     []byte
}

func (e *extraIndenter) WriteByte(b byte) error {
	e.curLine = append(e.curLine, b)
	if b != '\n' {
		return nil
	}
	trimmed := bytes.TrimLeft(e.curLine, "\t")
	if len(trimmed) == 1 {
		// no tabs if this is an empty line, i.e. "\n"
		e.bufWriter.Write(trimmed)
		e.curLine = e.curLine[:0]
		return nil
	}

	lineIndent := len(e.curLine) - len(trimmed)
	if e.firstIndent < 0 {
		// This is the first heredoc line we add extra indentation to.
		// Keep track of how much we indented.
		e.firstIndent = lineIndent
		e.firstChange = e.baseIndent - lineIndent
		lineIndent = e.baseIndent

	} else if lineIndent < e.firstIndent {
		// This line did not have enough indentation; simply indent it
		// like the first line.
		lineIndent = e.firstIndent
	} else {
		// This line had plenty of indentation. Add the extra
		// indentation that the first line had, for consistency.
		lineIndent += e.firstChange
	}
	e.bufWriter.WriteByte(tabwriter.Escape)
	for range lineIndent {
		e.bufWriter.WriteByte('\t')
	}
	e.bufWriter.WriteByte(tabwriter.Escape)
	e.bufWriter.Write(trimmed)
	e.curLine = e.curLine[:0]
	return nil
}

func (e *extraIndenter) WriteString(s string) (int, error) {
	for i := range len(s) {
		e.WriteByte(s[i])
	}
	return len(s), nil
}

func startsWithLparen(node Node) bool {
	switch node := node.(type) {
	case *Stmt:
		return startsWithLparen(node.Cmd)
	case *BinaryCmd:
		return startsWithLparen(node.X)
	case *Subshell:
		return true // keep ( (
	case *ArithmCmd:
		return true // keep ( ((
	}
	return false
}
07070100000058000081A4000000000000000000000001686AE5C000007EA2000000000000000000000000000000000000002100000000sh-3.12.0/syntax/printer_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"bytes"
	"fmt"
	"os"
	"regexp"
	"strings"
	"testing"
)

func TestPrintCompact(t *testing.T) {
	t.Parallel()
	parserBash := NewParser(KeepComments(true))
	parserPosix := NewParser(KeepComments(true), Variant(LangPOSIX))
	parserMirBSD := NewParser(KeepComments(true), Variant(LangMirBSDKorn))
	parserBats := NewParser(KeepComments(true), Variant(LangBats))
	printer := NewPrinter()
	for _, c := range append(fileTests, fileTestsKeepComments...) {
		t.Run("", func(t *testing.T) {
			in := c.Strs[0]
			parser := parserPosix
			if c.Bats != nil {
				parser = parserBats
			} else if c.Bash != nil {
				parser = parserBash
			} else if c.MirBSDKorn != nil {
				parser = parserMirBSD
			}
			printTest(t, parser, printer, in, in)
		})
	}
}

func strPrint(p *Printer, node Node) (string, error) {
	var buf bytes.Buffer
	err := p.Print(&buf, node)
	return buf.String(), err
}

type printCase struct {
	in, want string
}

func samePrint(s string) printCase { return printCase{in: s, want: s} }

var printTests = []printCase{
	samePrint(`fo○ b\år`),
	samePrint(`"fo○ b\år"`),
	samePrint(`'fo○ b\år'`),
	samePrint(`${a#fo○ b\år}`),
	samePrint(`#fo○ b\år`),
	samePrint("<<EOF\nfo○ b\\år\nEOF"),
	samePrint(`$'○ b\år'`),
	samePrint("${a/b//○}"),
	{strings.Repeat(" ", bufSize-3) + "○", "○"}, // at the end of a chunk
	{strings.Repeat(" ", bufSize-0) + "○", "○"}, // at the start of a chunk
	{strings.Repeat(" ", bufSize-2) + "○", "○"}, // split after 1st byte
	{strings.Repeat(" ", bufSize-1) + "○", "○"}, // split after 2nd byte
	// peekByte that would (but cannot) go to the next chunk
	{strings.Repeat(" ", bufSize-2) + ">(a)", ">(a)"},
	// escaped newline at end of chunk
	{"a" + strings.Repeat(" ", bufSize-2) + "\\\nb", "a \\\n\tb"},
	// panics if padding is only 4 (utf8.UTFMax)
	{strings.Repeat(" ", bufSize-10) + "${a/b//○}", "${a/b//○}"},
	// multiple p.fill calls
	{"a" + strings.Repeat(" ", bufSize*4) + "b", "a b"},
	// newline at the beginning of second chunk
	{"a" + strings.Repeat(" ", bufSize-2) + "\nb", "a\nb"},
	{"foo; bar", "foo\nbar"},
	{"foo\n\n\nbar", "foo\n\nbar"},
	{"foo\n\n", "foo"},
	{"\n\nfoo", "foo"},
	{"# foo \n # bar\t", "# foo\n# bar"},
	samePrint("#"),
	samePrint("#c1\\\n#c2"),
	samePrint("#\\\n#"),
	{"#\\\r\n#", "#\\\n#"},
	samePrint("{\n\t# foo \\\n}"),
	samePrint("foo\\\\\nbar"),
	samePrint("a=b # inline\nbar"),
	samePrint("a=$(b) # inline"),
	samePrint("foo # inline\n# after"),
	samePrint("$(a) $(b)"),
	{"if a\nthen\n\tb\nfi", "if a; then\n\tb\nfi"},
	samePrint("if a; then\n\tb\nelse\nfi"),
	{"if a; then b\nelse c\nfi", "if a; then\n\tb\nelse\n\tc\nfi"},
	samePrint("foo >&2 <f bar"),
	samePrint("foo >&2 bar <f"),
	samePrint(">&2 foo bar <f"),
	samePrint(">&2 foo"),
	samePrint(">&2 foo 2>&1 bar <f"),
	{"foo >&2>/dev/null", "foo >&2 >/dev/null"},
	{"foo <<EOF bar\nl1\nEOF", "foo bar <<EOF\nl1\nEOF"},
	samePrint("foo <<\\\\\\\\EOF\nbar\n\\\\EOF"),
	samePrint("foo <<\"\\EOF\"\nbar\n\\EOF"),
	samePrint("foo <<\"EOF\"\nbar\nEOF\nbar"),
	samePrint("foo <<EOF && bar\nl1\nEOF"),
	samePrint("foo <<EOF &&\nl1\nEOF\n\tbar"),
	samePrint("foo <<EOF\nl1\nEOF\n\nfoo2"),
	samePrint("<<EOF\nfoo\\\nbar\nEOF"),
	samePrint("<<'EOF'\nfoo\\\nbar\nEOF"),
	samePrint("<<EOF\n\\\n$foo\nEOF"),
	samePrint("<<'EOF'\n\\\nEOF"),
	samePrint("{\n\t<<EOF\nfoo\\\nbar\nEOF\n}"),
	samePrint("{\n\t<<'EOF'\nfoo\\\nbar\nEOF\n}"),
	samePrint("<<EOF\nEOF"),
	samePrint("foo <<EOF\nEOF\n\nbar"),
	samePrint("foo <<'EOF'\nEOF\n\nbar"),
	{
		"{ foo; bar; }",
		"{\n\tfoo\n\tbar\n}",
	},
	{
		"{ foo; bar; }\n#etc",
		"{\n\tfoo\n\tbar\n}\n#etc",
	},
	{
		"{\n\tfoo; }",
		"{\n\tfoo\n}",
	},
	{
		"{ foo\n}",
		"{\n\tfoo\n}",
	},
	{
		"(foo\n)",
		"(\n\tfoo\n)",
	},
	{
		"$(foo\n)",
		"$(\n\tfoo\n)",
	},
	{
		"a\n\n\n# etc\nb",
		"a\n\n# etc\nb",
	},
	{
		"a b\\\nc d",
		"a bc d",
	},
	{
		"a bb\\\ncc d",
		"a bbcc d",
	},
	samePrint("a \\\n\tb \\\n\t\"c\" \\\n\t;"),
	samePrint("a=1 \\\n\tb=2 \\\n\tc=\"3\" \\\n\t;"),
	{
		"a=\\\nfoo\nb=\\\n\"bar\"\nc=\\\n'baz'",
		"a=foo\nb=\"bar\"\nc='baz'",
	},

	samePrint("if a \\\n\t; then b; fi"),
	samePrint("a > \\\n\tfoo"),
	samePrint("a <<< \\\n\t\"foo\""),
	samePrint("a 'b\nb' c"),
	samePrint("a $'b\nb' c"),
	{
		"(foo; bar)",
		"(\n\tfoo\n\tbar\n)",
	},
	{
		"{\nfoo\nbar; }",
		"{\n\tfoo\n\tbar\n}",
	},
	samePrint("\"$foo\"\n{\n\tbar\n}"),
	{
		"{\nbar\n# extra\n}",
		"{\n\tbar\n\t# extra\n}",
	},
	{
		"foo\nbar  # extra",
		"foo\nbar # extra",
	},
	{
		"foo # 1\nfooo # 2\nfo # 3",
		"foo  # 1\nfooo # 2\nfo   # 3",
	},
	{
		" foo # 1\n fooo # 2\n fo # 3",
		"foo  # 1\nfooo # 2\nfo   # 3",
	},
	{
		"foo   # 1\nfooo  # 2\nfo    # 3",
		"foo  # 1\nfooo # 2\nfo   # 3",
	},
	{
		"foooooa\nfoo # 1\nfooo # 2\nfo # 3\nfooooo",
		"foooooa\nfoo  # 1\nfooo # 2\nfo   # 3\nfooooo",
	},
	{
		"foo\nbar\nfoo # 1\nfooo # 2",
		"foo\nbar\nfoo  # 1\nfooo # 2",
	},
	samePrint("foobar # 1\nfoo\nfoo # 2"),
	samePrint("foobar # 1\n#foo\nfoo # 2"),
	samePrint("foobar # 1\n\nfoo # 2"),
	{
		"foo # 2\nfoo2 bar # 1",
		"foo      # 2\nfoo2 bar # 1",
	},
	{
		"foo bar # 1\n! foo # 2",
		"foo bar # 1\n! foo   # 2",
	},
	{
		"aa #b\nc  #d\ne\nf #g",
		"aa #b\nc  #d\ne\nf #g",
	},
	{
		"{ a; } #x\nbbb #y\n{ #z\n}",
		"{ a; } #x\nbbb    #y\n{      #z\n}",
	},
	{
		"foo; foooo # 1",
		"foo\nfoooo # 1",
	},
	{
		"aaa; b #1\nc #2",
		"aaa\nb #1\nc #2",
	},
	{
		"a #1\nbbb; c #2\nd #3",
		"a #1\nbbb\nc #2\nd #3",
	},
	samePrint("aa #c1\n{  #c2\n\tb\n}"),
	{
		"aa #c1\n{ b; c; } #c2",
		"aa #c1\n{\n\tb\n\tc\n} #c2",
	},
	samePrint("a #c1\n'b\ncc' #c2"),
	{
		"(\nbar\n# extra\n)",
		"(\n\tbar\n\t# extra\n)",
	},
	{
		"for a in 1 2\ndo\n\t# bar\ndone",
		"for a in 1 2; do\n\t# bar\ndone",
	},
	samePrint("#before\nfoo | bar"),
	samePrint("#before\nfoo && bar"),
	samePrint("foo | bar # inline"),
	samePrint("foo && bar # inline"),
	samePrint("foo `# inline` \\\n\tbar"),
	samePrint("for a in 1 2; do\n\n\tbar\ndone"),
	{
		"a \\\n\t&& b",
		"a &&\n\tb",
	},
	{
		"a \\\n\t&& b\nc",
		"a &&\n\tb\nc",
	},
	{
		"{\n(a \\\n&& b)\nc\n}",
		"{\n\t(a &&\n\t\tb)\n\tc\n}",
	},
	{
		"a && b \\\n&& c",
		"a && b &&\n\tc",
	},
	{
		"a \\\n&& $(b) && c \\\n&& d",
		"a &&\n\t$(b) && c &&\n\td",
	},
	{
		"a \\\n&& b\nc \\\n&& d",
		"a &&\n\tb\nc &&\n\td",
	},
	{
		"a \\\n&&\n#c\nb",
		"a &&\n\t#c\n\tb",
	},
	{
		"a | {\nb \\\n| c\n}",
		"a | {\n\tb |\n\t\tc\n}",
	},
	{
		"a \\\n\t&& if foo; then\nbar\nfi",
		"a &&\n\tif foo; then\n\t\tbar\n\tfi",
	},
	{
		"if\nfoo\nthen\nbar\nfi",
		"if\n\tfoo\nthen\n\tbar\nfi",
	},
	{
		"if foo \\\nbar\nthen\nbar\nfi",
		"if foo \\\n\tbar; then\n\tbar\nfi",
	},
	{
		"if foo \\\n&& bar\nthen\nbar\nfi",
		"if foo &&\n\tbar; then\n\tbar\nfi",
	},
	{
		"a |\nb |\nc",
		"a |\n\tb |\n\tc",
	},
	samePrint("a |\n\tb | c |\n\td"),
	samePrint("a | b |\n\tc |\n\td"),
	{
		"foo |\n# misplaced\nbar",
		"foo |\n\t# misplaced\n\tbar",
	},
	samePrint("{\n\tfoo\n\t#a\n\tbar\n} | etc"),
	{
		"foo &&\n#a1\n#a2\n$(bar)",
		"foo &&\n\t#a1\n\t#a2\n\t$(bar)",
	},
	{
		"{\n\tfoo\n\t#a\n} |\n# misplaced\nbar",
		"{\n\tfoo\n\t#a\n} |\n\t# misplaced\n\tbar",
	},
	samePrint("foo | bar\n#after"),
	{
		"a |\nb | #c2\nc",
		"a |\n\tb | #c2\n\tc",
	},
	{
		"{\nfoo &&\n#a1\n#a2\n$(bar)\n}",
		"{\n\tfoo &&\n\t\t#a1\n\t\t#a2\n\t\t$(bar)\n}",
	},
	{
		"foo | while read l; do\nbar\ndone",
		"foo | while read l; do\n\tbar\ndone",
	},
	samePrint("while x; do\n\t#comment\ndone"),
	{
		"while x\ndo\n\ty\ndone",
		"while x; do\n\ty\ndone",
	},
	samePrint("\"\\\nfoo\""),
	samePrint("'\\\nfoo'"),
	samePrint("\"foo\\\n  bar\""),
	samePrint("'foo\\\n  bar'"),
	samePrint("v=\"\\\nfoo\""),
	{
		"v=foo\\\nbar",
		"v=foobar",
	},
	{
		"v='foo'\\\n'bar'",
		"v='foo''bar'",
	},
	{
		"v=\\\n\"foo\"",
		"v=\"foo\"",
	},
	{
		"v=\\\nfoo\\\n$bar",
		"v=foo$bar",
	},
	samePrint("\"\\\n\\\nfoo\\\n\\\n\""),
	samePrint("'\\\n\\\nfoo\\\n\\\n'"),
	{
		"foo \\\n>bar\netc",
		"foo \\\n\t>bar\netc",
	},
	{
		"foo \\\nfoo2 \\\n>bar",
		"foo \\\n\tfoo2 \\\n\t>bar",
	},
	samePrint("> >(foo)"),
	samePrint("x > >(foo) y"),
	samePrint("a | () |\n\tb"),
	samePrint("a | (\n\tx\n\ty\n) |\n\tb"),
	samePrint("a |\n\tif foo; then\n\t\tbar\n\tfi |\n\tb"),
	samePrint("a | if foo; then\n\tbar\nfi"),
	samePrint("a | b | if foo; then\n\tbar\nfi"),
	{
		"case $i in\n1)\nfoo\n;;\nesac",
		"case $i in\n1)\n\tfoo\n\t;;\nesac",
	},
	{
		"case $i in\n1)\nfoo\nesac",
		"case $i in\n1)\n\tfoo\n\t;;\nesac",
	},
	{
		"case $i in\n1) foo\nesac",
		"case $i in\n1) foo ;;\nesac",
	},
	{
		"case $i in\n1) foo; bar\nesac",
		"case $i in\n1)\n\tfoo\n\tbar\n\t;;\nesac",
	},
	{
		"case $i in\n1) foo; bar;;\nesac",
		"case $i in\n1)\n\tfoo\n\tbar\n\t;;\nesac",
	},
	{
		"case $i in\n1)\n#foo \t\n;;\nesac",
		"case $i in\n1)\n\t#foo\n\t;;\nesac",
	},
	{
		"case $i in\n1)\n\t;;\n\n2)\n\t;;\nesac",
		"case $i in\n1) ;;\n\n2) ;;\nesac",
	},
	{
		"case $i\nin\n1)\n\t;;\nesac",
		"case $i in\n1) ;;\nesac",
	},
	samePrint("case $i in\n1)\n\ta\n\t#b\n\t;;\nesac"),
	samePrint("case $i in\n1) foo() { bar; } ;;\nesac"),
	samePrint("case $i in\n1) ;; #foo\nesac"),
	samePrint("case $i in\n#foo\nesac"),
	samePrint("case $i in\n#before\n1) ;;\nesac"),
	samePrint("case $i in\n#bef\n1) ;; #inl\nesac"),
	samePrint("case $i in\n#before1\n'1') ;;\n#before2\n'2') ;;\nesac"),
	samePrint("case $i in\n1) ;; #inl1\n2) ;; #inl2\nesac"),
	samePrint("case $i in\n#bef\n1) #inl\n\tfoo\n\t;;\nesac"),
	samePrint("case $i in\n1) #inl\n\t;;\nesac"),
	samePrint("case $i in\n1) a \\\n\tb ;;\nesac"),
	samePrint("case $i in\n1 | 2 | \\\n\t3 | 4) a b ;;\nesac"),
	samePrint("case $i in\n1 | 2 | \\\n\t3 | 4)\n\ta b\n\t;;\nesac"),
	samePrint("case $i in\nx) ;;\ny) for n in 1; do echo $n; done ;;\nesac"),
	samePrint("case a in b) [[ x =~ y ]] ;; esac"),
	samePrint("case a in b) [[ a =~ b$ || c =~ d$ ]] ;; esac"),
	samePrint("case a in b) [[ a =~ (b) ]] ;; esac"),
	samePrint("[[ (a =~ b$) ]]"),
	samePrint("[[ a && ((b || c) && d) ]]"),
	samePrint("[[ a &&\n\tb ]]"),
	samePrint("[[ a ||\n\tb ]]"),
	{
		"[[ -f \\\n\tfoo ]]",
		"[[ -f foo ]]",
	},
	{
		"[[ foo \\\n\t-ef \\\n\tbar ]]",
		"[[ foo -ef bar ]]",
	},
	{
		"[[ a && \\\nb \\\n && c ]]",
		"[[ a &&\n\tb &&\n\tc ]]",
	},
	samePrint("{\n\t[[ a || b ]]\n}"),
	{
		"a=(\nb\nc\n) b=c",
		"a=(\n\tb\n\tc\n) b=c",
	},
	samePrint("a=(\n\t#before\n\tb #inline\n)"),
	samePrint("a=(\n\tb #foo\n\tc #bar\n)"),
	samePrint("a=(\n\tb\n\n\t#foo\n\t#bar\n\tc\n)"),
	samePrint("a=(\n\t#foo\n\t#bar\n\tc\n)"),
	samePrint("a=(\n\t#lone\n)"),
	samePrint("a=(\n\n)"),
	samePrint("a=(\n\tx\n\n\ty\n)"),
	samePrint("foo <<EOF | $(bar)\n3\nEOF"),
	{
		"a <<EOF\n$(\n\tb\n\tc)\nEOF",
		"a <<EOF\n$(\n\tb\n\tc\n)\nEOF",
	},
	samePrint("<<EOF1\n$(\n\t<<EOF2\ninner\nEOF2\n)\nEOF1"),
	{
		"<(<<EOF\nbody\nEOF\n)",
		"<(\n\t<<EOF\nbody\nEOF\n)",
	},
	{
		"( (foo) )\n$( (foo) )\n<( (foo) )",
		"( (foo))\n$( (foo))\n<((foo))",
	},
	{
		"if ( ((foo)) || bar ); then baz; fi",
		"if ( ((foo)) || bar); then baz; fi",
	},
	samePrint("if x; then (\n\ty\n) & fi"),
	samePrint("\"foo\n$(bar)\""),
	samePrint("\"foo\\\n$(bar)\""),
	samePrint("\"foo\\\nbar\""),
	samePrint("((foo++)) || bar"),
	{
		"a=b \\\nc=d \\\nfoo",
		"a=b \\\n\tc=d \\\n\tfoo",
	},
	{
		"a=b \\\nc=d \\\nfoo \\\nbar",
		"a=b \\\n\tc=d \\\n\tfoo \\\n\tbar",
	},
	samePrint("a $(x) \\\n\tb"),
	samePrint("\"foo\nbar\"\netc"),
	samePrint("\"foo\nbar\nbar2\"\netc"),
	samePrint("a=\"$b\n\"\nd=e"),
	samePrint("\"\n\"\n\nfoo"),
	samePrint("$\"\n\"\n\nfoo"),
	samePrint("'\n'\n\nfoo"),
	samePrint("$'\n'\n\nfoo"),
	samePrint("foo <<EOF\na\nb\nc\nd\nEOF\n{\n\tbar\n}"),
	samePrint("foo bar # one\nif a; then\n\tb\nfi # two"),
	{
		"# foo\n\n\nbar",
		"# foo\n\nbar",
	},
	{
		"# foo\n\n\nbar\nbaz",
		"# foo\n\nbar\nbaz",
	},
	samePrint("#foo\n#\n#bar"),
	{
		"(0 #\n0)#\n0",
		"(\n\t0 #\n\t0\n) #\n0",
	},
	samePrint("a | #c1\n\t(\n\t\tb\n\t)"),
	samePrint("a | #c1\n\t{\n\t\tb\n\t}"),
	samePrint("a | #c1\n\tif b; then\n\t\tc\n\tfi"),
	samePrint("a | #c1\n\t#c2\n\t#c3\n\tb"),
	samePrint("a && #c1\n\t(\n\t\tb\n\t)"),
	samePrint("f() body # comment"),
	samePrint("f <<EOF\nbody\nEOF"),
	samePrint("f <<EOF\nEOF"),
	samePrint("f <<-EOF\n\tbody\nEOF"),
	{
		"f <<-EOF\nbody\nEOF",
		"f <<-EOF\n\tbody\nEOF",
	},
	samePrint("f <<-EOF\nEOF"),
	samePrint("f <<-EOF\n\n\nEOF"),
	samePrint("f <<-EOF\n\n\tindented\n\nEOF"),
	samePrint("{\n\tf <<EOF\nEOF\n}"),
	samePrint("{\n\tf <<-EOF\n\t\tbody\n\tEOF\n}"),
	samePrint("{\n\tf <<-EOF\n\t\tbody\n\tEOF\n\tf2\n}"),
	samePrint("f <<-EOF\n\t{\n\t\tnicely indented\n\t}\nEOF"),
	samePrint("f <<-EOF\n\t{\n\t\tnicely indented\n\t}\nEOF"),
	{
		"f <<-EOF\n\t{\nbadly indented\n\t}\nEOF",
		"f <<-EOF\n\t{\n\tbadly indented\n\t}\nEOF",
	},
	{
		"f <<-EOF\n\t\t{\n\t\t\ttoo indented\n\t\t}\nEOF",
		"f <<-EOF\n\t{\n\t\ttoo indented\n\t}\nEOF",
	},
	{
		"f <<-EOF\n{\n\ttoo little indented\n}\nEOF",
		"f <<-EOF\n\t{\n\t\ttoo little indented\n\t}\nEOF",
	},
	samePrint("<<-EOF\n\t$foo\nEOF\n\n{\n\tbar\n}"),
	samePrint("f <<EOF\nEOF\n# comment"),
	samePrint("f <<EOF\nEOF\n# comment\nbar"),
	samePrint("f <<EOF # inline\n$(\n\t# inside\n)\nEOF\n# outside\nbar"),
	samePrint("while foo; do\n\tbar\ndone <<-EOF # inline\n\tbaz\nEOF"),
	samePrint("{\n\tcat <<EOF\nEOF\n\t# comment\n}"),
	{
		"if foo # inline\nthen\n\tbar\nfi",
		"if foo; then # inline\n\tbar\nfi",
	},
	samePrint("for i; do echo $i; done"),
	samePrint("for i in; do echo $i; done"),
	{
		"for foo in a b # inline\ndo\n\tbar\ndone",
		"for foo in a b; do # inline\n\tbar\ndone",
	},
	{
		"if x # inline\nthen bar; fi",
		"if x; then # inline\n\tbar\nfi",
	},
	{
		"for i in a b # inline\ndo bar; done",
		"for i in a b; do # inline\n\tbar\ndone",
	},
	{
		"for i #a\n\tin 1; do #b\ndone",
		"for i in \\\n\t1; do #a\n\t#b\ndone",
	},
	{
		"foo() # inline\n{\n\tbar\n}",
		"foo() { # inline\n\tbar\n}",
	},
	{
		"foo() #before\n(\n\tbar #inline\n)",
		"foo() ( #before\n\tbar #inline\n)",
	},
	{
		"foo() (#before\n\tbar #inline\n)",
		"foo() ( #before\n\tbar #inline\n)",
	},
	{
		"foo()\n#before-1\n(#before-2\n\tbar #inline\n)",
		"foo() ( #before-1\n\t#before-2\n\tbar #inline\n)",
	},
	{
		"(#before\n\tbar #inline\n)",
		"( #before\n\tbar #inline\n)",
	},
	{
		"(\n#before\n\tbar #inline\n)",
		"(\n\t#before\n\tbar #inline\n)",
	},
	{
		"foo=$(#before\n\tbar #inline\n)",
		"foo=$( #before\n\tbar #inline\n)",
	},
	{
		"foo=`#before\nbar`",
		"foo=$( #before\n\tbar\n)",
	},
	samePrint("if foo; then\n\tbar\n\t# comment\nfi"),
	samePrint("if foo; then\n\tbar\n# else commented out\nfi"),
	samePrint("if foo; then\n\tx\nelse\n\tbar\n\t# comment\nfi"),
	samePrint("if foo; then\n\tx\n#comment\nelse\n\ty\nfi"),
	samePrint("if foo; then\n\tx\n\t#comment\nelse\n\ty\nfi"),
	{
		"if foo; then\n\tx\n#a\n\t#b\n\t#c\nelse\n\ty\nfi",
		"if foo; then\n\tx\n\t#a\n\t#b\n\t#c\nelse\n\ty\nfi",
	},
	samePrint("if foo; then\n\tx\nelse #comment\n\ty\nfi"),
	samePrint("if foo; then\n\tx\n#comment\nelif bar; then\n\ty\nfi"),
	samePrint("if foo; then\n\tx\n\t#comment\nelif bar; then\n\ty\nfi"),
	samePrint("case i in\nx)\n\ta\n\t;;\n#comment\ny) ;;\nesac"),
	samePrint("case i in\nx)\n\ta\n\t;;\n\t#comment\ny) ;;\nesac"),
	{
		"case i in\nx)\n\ta\n\t;;\n\t#a\n#b\n\t#c\ny) ;;\nesac",
		"case i in\nx)\n\ta\n\t;;\n\t#a\n\t#b\n\t#c\ny) ;;\nesac",
	},
	samePrint("'foo\tbar'\n'foooo\tbar'"),
	samePrint("\"foo\tbar\"\n\"foooo\tbar\""),
	samePrint("foo\\\tbar\nfoooo\\\tbar"),
	samePrint("#foo\tbar\n#foooo\tbar"),
	{
		"array=('one'\n\t\t# 'two'\n\t\t'three')",
		"array=('one'\n\t# 'two'\n\t'three')",
	},
	samePrint("#comment\n>redir"),
	{
		">redir \\\n\tfoo",
		">redir foo",
	},
	samePrint("$(declare)"),
	{
		"`declare`",
		"$(declare)",
	},
	{
		"(\n(foo >redir))",
		"(\n\t(foo >redir)\n)",
	},
	{
		"( (foo) )",
		"( (foo))",
	},
	{
		"( (foo); bar )",
		"(\n\t(foo)\n\tbar\n)",
	},
	{
		"( ((foo++)) )",
		"( ((foo++)))",
	},
	{
		"( ((foo++)); bar )",
		"(\n\t((foo++))\n\tbar\n)",
	},
	samePrint("(\n\t((foo++))\n)"),
	samePrint("(foo && bar)"),
	samePrint(`$foo#bar ${foo}#bar 'foo'#bar "foo"#bar`),
	// TODO: support cases with command substitutions as well
	// {
	// 	"`foo`#bar",
	// 	"$(foo)#bar",
	// },
	// samePrint(`$("foo"#bar)#bar`),
}

func TestPrintWeirdFormat(t *testing.T) {
	t.Parallel()
	parser := NewParser(KeepComments(true))
	printer := NewPrinter()
	for i, tc := range printTests {
		t.Run(fmt.Sprintf("#%03d", i), func(t *testing.T) {
			printTest(t, parser, printer, tc.in, tc.want)
		})
		t.Run(fmt.Sprintf("#%03d-nl", i), func(t *testing.T) {
			printTest(t, parser, printer, "\n"+tc.in+"\n", tc.want)
		})
		t.Run(fmt.Sprintf("#%03d-redo", i), func(t *testing.T) {
			printTest(t, parser, printer, tc.want, tc.want)
		})
	}
}

func parsePath(tb testing.TB, path string) *File {
	f, err := os.Open(path)
	if err != nil {
		tb.Fatal(err)
	}
	defer f.Close()
	prog, err := NewParser(KeepComments(true)).Parse(f, "")
	if err != nil {
		tb.Fatal(err)
	}
	return prog
}

const canonicalPath = "canonical.sh"

func TestPrintMultiline(t *testing.T) {
	t.Parallel()
	prog := parsePath(t, canonicalPath)
	got, err := strPrint(NewPrinter(), prog)
	if err != nil {
		t.Fatal(err)
	}

	wantBs, err := os.ReadFile(canonicalPath)
	if err != nil {
		t.Fatal(err)
	}

	// If we're on Windows and it was set up to automatically replace LF
	// with CRLF, that might make this test fail. Just ignore \r characters.
	want := strings.ReplaceAll(string(wantBs), "\r", "")
	got = strings.ReplaceAll(got, "\r", "")
	if got != want {
		t.Fatalf("Print mismatch in canonical.sh")
	}
}

func TestPrintSpaces(t *testing.T) {
	t.Parallel()
	spaceFormats := [...]struct {
		spaces   uint
		in, want string
	}{
		{
			0,
			"{\nfoo \\\nbar\n}",
			"{\n\tfoo \\\n\t\tbar\n}",
		},
		{
			2,
			"{\nfoo \\\nbar\n}",
			"{\n  foo \\\n    bar\n}",
		},
		{
			4,
			"{\nfoo \\\nbar\n}",
			"{\n    foo \\\n        bar\n}",
		},
		{
			2,
			"if foo; then # inline1\nbar # inline2\n# withfi\nfi",
			"if foo; then # inline1\n  bar        # inline2\n# withfi\nfi",
		},
		{
			2,
			"array=('one'\n    # 'two'\n    'three')",
			"array=('one'\n  # 'two'\n  'three')",
		},
	}

	parser := NewParser(KeepComments(true))
	for _, tc := range spaceFormats {
		t.Run("", func(t *testing.T) {
			printer := NewPrinter(Indent(tc.spaces))
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

var errBadWriter = fmt.Errorf("write: expected error")

type badWriter struct{}

func (b badWriter) Write(p []byte) (int, error) { return 0, errBadWriter }

func TestWriteErr(t *testing.T) {
	t.Parallel()
	f := &File{Stmts: []*Stmt{
		{
			Redirs: []*Redirect{{
				Op:   RdrOut,
				Word: litWord("foo"),
			}},
			Cmd: &Subshell{},
		},
	}}
	err := NewPrinter().Print(badWriter{}, f)
	if err == nil {
		t.Fatalf("Expected error with bad writer")
	}
	if err != errBadWriter {
		t.Fatalf("Error mismatch with bad writer:\nwant: %v\ngot:  %v",
			errBadWriter, err)
	}
}

func TestPrintBinaryNextLine(t *testing.T) {
	t.Parallel()
	tests := [...]printCase{
		{
			"foo <<EOF &&\nl1\nEOF\nbar",
			"foo <<EOF && bar\nl1\nEOF",
		},
		samePrint("a \\\n\t&& b"),
		samePrint("a \\\n\t&& b\nc"),
		{
			"{\n(a \\\n&& b)\nc\n}",
			"{\n\t(a \\\n\t\t&& b)\n\tc\n}",
		},
		{
			"a && b \\\n&& c",
			"a && b \\\n\t&& c",
		},
		{
			"a \\\n&& $(b) && c \\\n&& d",
			"a \\\n\t&& $(b) && c \\\n\t&& d",
		},
		{
			"a \\\n&& b\nc \\\n&& d",
			"a \\\n\t&& b\nc \\\n\t&& d",
		},
		{
			"a | {\nb \\\n| c\n}",
			"a | {\n\tb \\\n\t\t| c\n}",
		},
		{
			"a \\\n\t&& if foo; then\nbar\nfi",
			"a \\\n\t&& if foo; then\n\t\tbar\n\tfi",
		},
		{
			"if foo \\\n&& bar\nthen\nbar\nfi",
			"if foo \\\n\t&& bar; then\n\tbar\nfi",
		},
		{
			"a |\nb |\nc",
			"a \\\n\t| b \\\n\t| c",
		},
		{
			"foo |\n# misplaced\nbar",
			"foo \\\n\t|\n\t# misplaced\n\tbar",
		},
		samePrint("{\n\tfoo\n\t#a\n\tbar\n} | etc"),
		{
			"foo &&\n#a1\n#a2\n$(bar)",
			"foo \\\n\t&&\n\t#a1\n\t#a2\n\t$(bar)",
		},
		{
			"{\n\tfoo\n\t#a\n} |\n# misplaced\nbar",
			"{\n\tfoo\n\t#a\n} \\\n\t|\n\t# misplaced\n\tbar",
		},
		samePrint("foo | bar\n#after"),
		{
			"a |\nb | #c2\nc",
			"a \\\n\t| b \\\n\t|\n\t#c2\n\tc",
		},
		samePrint("a \\\n\t&"),
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter(BinaryNextLine(true))
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

func TestPrintSwitchCaseIndent(t *testing.T) {
	t.Parallel()
	tests := [...]printCase{
		{
			"case $i in\n1)\nfoo\n;;\nesac",
			"case $i in\n\t1)\n\t\tfoo\n\t\t;;\nesac",
		},
		{
			"case $i in\n1)\na\n;;\n2)\nb\n;;\nesac",
			"case $i in\n\t1)\n\t\ta\n\t\t;;\n\t2)\n\t\tb\n\t\t;;\nesac",
		},
		samePrint("case $i in\n\t#foo\nesac"),
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter(SwitchCaseIndent(true))
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

func TestPrintFunctionNextLine(t *testing.T) {
	t.Parallel()
	tests := [...]printCase{
		{
			"foo() { bar; }",
			"foo()\n{\n\tbar\n}",
		},
		{
			"foo()\n{ bar; }",
			"foo()\n{\n\tbar\n}",
		},
		{
			"foo()\n\n{\n\n\tbar\n}",
			"foo()\n{\n\n\tbar\n}",
		},
		{
			"function foo {\n\tbar\n}",
			"function foo\n{\n\tbar\n}",
		},
		{
			"function foo() {\n\tbar\n}",
			"function foo()\n{\n\tbar\n}",
		},
		{
			"{ foo() { bar; }; }",
			"{\n\tfoo()\n\t{\n\t\tbar\n\t}\n}",
		},
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter(FunctionNextLine(true))
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

func TestPrintSpaceRedirects(t *testing.T) {
	t.Parallel()
	tests := [...]printCase{
		samePrint("echo foo bar > f"),
		samePrint("echo > f foo bar"),
		samePrint("echo >(cmd)"),
		samePrint("echo > >(cmd)"),
		samePrint("<< EOF\nfoo\nEOF"),
		samePrint("<<- EOF\n\t$(< foo)\nEOF"),
		samePrint("echo 2> f"),
		samePrint("echo foo bar >&1"),
		samePrint("echo 2<&1 foo bar"),
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter(SpaceRedirects(true))
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

func TestPrintKeepPadding(t *testing.T) {
	t.Parallel()
	tests := [...]printCase{
		samePrint("echo foo bar"),
		samePrint("echo  foo   bar"),
		samePrint("a=b  c=d   bar"),
		samePrint("echo foo    >bar"),
		samePrint("echo foo    2>bar"),
		samePrint("{ foo;  }"),
		samePrint("a()   { foo; }"),
		samePrint("a   && b"),
		samePrint("a   | b"),
		samePrint("a |  b"),
		samePrint("{  a b c; }"),
		samePrint("foo    # x\nbaaar  # y"),
		samePrint("{ { a; }; }"),
		samePrint("{  a;  }"),
		samePrint("(  a   )"),
		samePrint("'foo\nbar'   # x"),
		{"\tfoo", "foo"},
		{"  if foo; then bar; fi", "if   foo; then bar; fi"},
		samePrint("echo '★'  || true"),
		{
			"1234 || { x; y; }",
			"1234 || {\n\tx\n\ty\n}",
		},
		{
			"array=('one'\n\t\t# 'two'\n\t\t'three')",
			"array=('one'\n\t# 'two'\n\t'three')",
		},
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter(KeepPadding(true))
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			// ensure that Reset does properly reset colCounter
			printer.WriteByte('x')
			printer.Reset(nil)
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

func TestPrintKeepPaddingSpaces(t *testing.T) {
	t.Parallel()
	tests := [...]printCase{
		samePrint("array=('one'\n        # 'two'\n        'three')"),
		samePrint("    abc=123"),
		samePrint("foo \\\n  bar \\\n    baz"),
		samePrint("{\n  foo\n    bar\n}"),
		samePrint("# foo\n  # bar"),
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter(KeepPadding(true), Indent(2))
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

func TestPrintMinify(t *testing.T) {
	t.Parallel()
	tests := [...]printCase{
		samePrint("echo foo bar $a $(b)"),
		{
			"#comment",
			"",
		},
		{
			"foo #comment",
			"foo",
		},
		{
			"foo\n\nbar",
			"foo\nbar",
		},
		{
			"foo &",
			"foo&",
		},
		samePrint("foo >bar 2>baz <etc"),
		{
			"{\n\tfoo\n}",
			"{\nfoo\n}",
		},
		{
			"(\n\ta\n)\n(\n\tb\n\tc\n)",
			"(a)\n(b\nc)",
		},
		{
			"$(\n\ta\n)\n$(\n\tb\n\tc\n)",
			"$(a)\n$(b\nc)",
		},
		{
			"f() { x; }",
			"f(){ x;}",
		},
		{
			"((1 + 2))",
			"((1+2))",
		},
		{
			"echo $a ${b} ${c}-d ${e}f ${g}_h",
			"echo $a $b $c-d ${e}f ${g}_h",
		},
		{
			"echo ${0} ${3} ${10} ${22}",
			"echo $0 $3 ${10} ${22}",
		},
		{
			"case $a in\nx) c ;;\ny | z)\n\td\n\t;;\nesac",
			"case $a in\nx)c;;\ny|z)d\nesac",
		},
		{
			"a && b | c",
			"a&&b|c",
		},
		{
			"a &&\n\tb |\n\tc",
			"a&&b|c",
		},
		{
			"${0/${a}\\\n}",
			"${0/$a/}",
		},
		{
			"#!/bin/sh\necho 1\n#!/bin/sh\necho 2",
			"#!/bin/sh\necho 1\necho 2",
		},
		samePrint("foo >bar 2>baz <etc"),
		samePrint("<<-EOF\n$(a|b)\nEOF"),
		{
			"a=$(\n\tcat <<'EOF'\n  hello\nEOF\n)",
			"a=$(cat <<'EOF'\n  hello\nEOF\n)",
		},
		{
			"(\n\tcat <<EOF\n hello\nEOF\n)",
			"(cat <<EOF\n hello\nEOF\n)",
		},
		samePrint("diff -y <(cat <<EOF\n1\n2\n3\nEOF\n) <(cat <<EOF\n1\n4\n3\nEOF\n)"),
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter(Minify(true))
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

func TestPrintSingleLine(t *testing.T) {
	t.Parallel()
	tests := [...]printCase{
		samePrint("echo foo bar $a $(b)"),
		samePrint("foo #comment"),
		{
			"foo\n\nbar",
			"foo; bar",
		},
		samePrint("foo &"),
		samePrint("foo >bar 2>baz <etc"),
		{
			"{\n\tfoo\n}",
			"{ foo; }",
		},
		{
			"(\n\ta\n)\n(\n\tb\n\tc\n)",
			"(a); (b; c)",
		},
		{
			"$(\n\ta\n)\n$(\n\tb\n\tc\n)",
			"$(a); $(b; c)",
		},
		samePrint("f() { x; }"),
		samePrint("((1 + 2))"),
		samePrint("echo $a ${b} ${c}-d ${e}f ${g}_h"),
		samePrint("echo ${0} ${3} ${10} ${22}"),
		{
			"case $a in\nx)c;;\ny|z)d\nesac",
			"case $a in x) c ;; y | z) d ;; esac",
		},
		samePrint("a && b | c"),
		{
			"a &&\n\tb |\n\tc",
			"a && b | c",
		},
		{
			"if\nfoo\nthen\nbar\nfi",
			"if foo; then bar; fi",
		},
		{
			"a \\\n >b",
			"a >b",
		},
		samePrint("foo >bar 2>baz <etc"),
		samePrint("<<-EOF\n\t$(a | b)\nEOF"),
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter(SingleLine(true))
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			printTest(t, parser, printer, tc.in, tc.want)
		})
	}
}

func TestPrintOptionsNotBroken(t *testing.T) {
	t.Parallel()
	parserBash := NewParser(KeepComments(true))
	parserPosix := NewParser(KeepComments(true), Variant(LangPOSIX))
	parserMirBSD := NewParser(KeepComments(true), Variant(LangMirBSDKorn))
	parserBats := NewParser(KeepComments(true), Variant(LangBats))

	// e.g. comments and heredocs require newlines
	singleLineException := regexp.MustCompile(`#|<<|'|"`)
	checkSingleLine := func(t *testing.T, got string) {
		if singleLineException.MatchString(got) {
			return
		}
		got = strings.TrimSuffix(got, "\n") // trailing newline is expected
		if strings.Contains(got, "\n") {
			t.Fatalf("unexpected newline with SingleLine: %q", got)
		}
	}

	for _, opts := range []struct {
		name string
		list []PrinterOption
	}{
		{"Minify", []PrinterOption{Minify(true)}},
		{"SingleLine", []PrinterOption{SingleLine(true)}},
	} {
		printer := NewPrinter(opts.list...)
		for _, tc := range append(fileTests, fileTestsNoPrint...) {
			t.Run("File"+opts.name, func(t *testing.T) {
				parser := parserPosix
				if tc.Bats != nil {
					parser = parserBats
				} else if tc.Bash != nil {
					parser = parserBash
				} else if tc.MirBSDKorn != nil {
					parser = parserMirBSD
				}
				in := tc.Strs[0]
				prog, err := parser.Parse(strings.NewReader(in), "")
				if err != nil {
					t.Fatal(err)
				}
				got, err := strPrint(printer, prog)
				if err != nil {
					t.Fatal(err)
				}
				if opts.name == "SingleLine" {
					checkSingleLine(t, got)
				}
				_, err = parser.Parse(strings.NewReader(got), "")
				if err != nil {
					t.Fatalf("program was broken: %v\noriginal:\n%s\nfinal:\n%s", err, in, got)
				}
			})
		}
		for _, tc := range printTests {
			t.Run("Print"+opts.name, func(t *testing.T) {
				prog, err := parserBash.Parse(strings.NewReader(tc.in), "")
				if err != nil {
					t.Fatal(err)
				}
				got, err := strPrint(printer, prog)
				if err != nil {
					t.Fatal(err)
				}
				if opts.name == "SingleLine" {
					checkSingleLine(t, got)
				}
				_, err = parserBash.Parse(strings.NewReader(got), "")
				if err != nil {
					t.Fatalf("program was broken: %v\noriginal:\n%s\nfinal:\n%s", err, tc.in, got)
				}
			})
		}
	}
}

func printTest(t *testing.T, parser *Parser, printer *Printer, in, want string) {
	t.Helper()
	prog, err := parser.Parse(strings.NewReader(in), "")
	if err != nil {
		t.Fatalf("parsing got an error: %s:\n%s", err, in)
	}
	origWant := want
	want += "\n"
	got, err := strPrint(printer, prog)
	if err != nil {
		t.Fatal(err)
	}
	if got != want {
		t.Fatalf("Print mismatch:\nin:\n%q\nwant:\n%q\ngot:\n%q", in, want, got)
	}

	// With the original "want" output string,
	// make sure that it's idempotent when formatted again.
	// Note that we don't want the added newline,
	// as that can change the meaning of trailing backslashes.
	progAgain, err := parser.Parse(strings.NewReader(origWant), "")
	if err != nil {
		t.Fatalf("Result is not valid shell:\n%s", want)
	}
	gotAgain, err := strPrint(printer, progAgain)
	if err != nil {
		t.Fatal(err)
	}
	if gotAgain != want {
		t.Fatalf("Re-print mismatch:\nin:\n%q\nwant:\n%q\ngot:\n%q", in, want, gotAgain)
	}
}

func TestPrintNodeTypes(t *testing.T) {
	t.Parallel()

	multiline, err := NewParser().Parse(strings.NewReader(`
		echo foo
	`), "")
	if err != nil {
		t.Fatal(err)
	}

	tests := [...]struct {
		in      Node
		want    string
		wantErr bool
	}{
		{
			in:   &File{Stmts: litStmts("foo")},
			want: "foo\n",
		},
		{
			in:   &File{Stmts: litStmts("foo", "bar")},
			want: "foo\nbar\n",
		},
		{
			in:   litStmt("foo", "bar"),
			want: "foo bar",
		},
		{
			in:   litCall("foo", "bar"),
			want: "foo bar",
		},
		{
			in:   litWord("foo"),
			want: "foo",
		},
		{
			in:   lit("foo"),
			want: "foo",
		},
		{
			in:   sglQuoted("foo"),
			want: "'foo'",
		},
		{
			in:      &Comment{},
			wantErr: true,
		},
		{
			in:   multiline.Stmts[0],
			want: "echo foo",
		},
		{
			in:   multiline.Stmts[0].Cmd,
			want: "echo foo",
		},
		{
			in:   multiline.Stmts[0].Cmd.(*CallExpr).Args[0],
			want: "echo",
		},
		{
			in:   multiline.Stmts[0].Cmd.(*CallExpr).Args[0].Parts[0],
			want: "echo",
		},
	}
	printer := NewPrinter()
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			got, err := strPrint(printer, tc.in)
			if err == nil && tc.wantErr {
				t.Fatalf("wanted an error but found none")
			} else if err != nil && !tc.wantErr {
				t.Fatalf("didn't want an error but got %v", err)
			}
			if got != tc.want {
				t.Fatalf("Print mismatch:\nwant:\n%s\ngot:\n%s",
					tc.want, got)
			}
		})
	}
}

func TestPrintManyStmts(t *testing.T) {
	t.Parallel()
	tests := [...]struct {
		in, want string
	}{
		{"foo; bar", "foo\nbar\n"},
		{"foo\nbar", "foo\nbar\n"},
		{"\n\nfoo\nbar\n\n", "foo\nbar\n"},
		{"foo\nbar <<EOF\nbody\nEOF\n", "foo\nbar <<EOF\nbody\nEOF\n"},
		{"foo\nbar # inline", "foo\nbar # inline\n"},
		{"# comment before\nfoo bar", "# comment before\nfoo bar\n"},
	}
	parser := NewParser(KeepComments(true))
	printer := NewPrinter()
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			f, err := parser.Parse(strings.NewReader(tc.in), "")
			if err != nil {
				t.Fatal(err)
			}
			var buf bytes.Buffer
			for _, stmt := range f.Stmts {
				printer.Print(&buf, stmt)
				buf.WriteByte('\n')
			}
			got := buf.String()
			if got != tc.want {
				t.Fatalf("Print mismatch:\nwant:\n%s\ngot:\n%s",
					tc.want, got)
			}
		})
	}
}

func TestKeepPaddingRepeated(t *testing.T) {
	t.Parallel()
	parser := NewParser()
	printer := NewPrinter()

	// Enable the KeepPadding option twice. This used to crash, since the
	// option made an invalid type assertion the second time.
	KeepPadding(true)(printer)
	KeepPadding(true)(printer)

	// Ensure the option is enabled.
	printTest(t, parser, printer, "foo  bar", "foo  bar")

	// Disable the option, and ensure it's disabled.
	KeepPadding(false)(printer)
	printTest(t, parser, printer, "foo  bar", "foo bar")
}
07070100000059000081A4000000000000000000000001686AE5C0000014C9000000000000000000000000000000000000001A00000000sh-3.12.0/syntax/quote.go// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"fmt"
	"strings"
	"unicode"
	"unicode/utf8"
)

type QuoteError struct {
	ByteOffset int
	Message    string
}

func (e QuoteError) Error() string {
	return fmt.Sprintf("cannot quote character at byte %d: %s", e.ByteOffset, e.Message)
}

const (
	quoteErrNull  = "shell strings cannot contain null bytes"
	quoteErrPOSIX = "POSIX shell lacks escape sequences"
	quoteErrRange = "rune out of range"
	quoteErrMksh  = "mksh cannot escape codepoints above 16 bits"
)

// Quote returns a quoted version of the input string,
// so that the quoted version is expanded or interpreted
// as the original string in the given language variant.
//
// Quoting is necessary when using arbitrary literal strings
// as words in a shell script or command.
// Without quoting, one can run into syntax errors,
// as well as the possibility of running unintended code.
//
// An error is returned when a string cannot be quoted for a variant.
// For instance, POSIX lacks escape sequences for non-printable characters,
// and no language variant can represent a string containing null bytes.
// In such cases, the returned error type will be *QuoteError.
//
// The quoting strategy is chosen on a best-effort basis,
// to minimize the amount of extra bytes necessary.
//
// Some strings do not require any quoting and are returned unchanged.
// Those strings can be directly surrounded in single quotes as well.
func Quote(s string, lang LangVariant) (string, error) {
	if s == "" {
		// Special case; an empty string must always be quoted,
		// as otherwise it expands to zero fields.
		return "''", nil
	}
	shellChars := false
	nonPrintable := false
	offs := 0
	for rem := s; len(rem) > 0; {
		r, size := utf8.DecodeRuneInString(rem)
		switch r {
		// Like regOps; token characters.
		case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`',
			// Whitespace; might result in multiple fields.
			' ', '\t', '\r', '\n',
			// Escape sequences would be expanded.
			'\\',
			// Would start a comment unless quoted.
			'#',
			// Might result in brace expansion.
			'{',
			// Might result in tilde expansion.
			'~',
			// Might result in globbing.
			'*', '?', '[',
			// Might result in an assignment.
			'=':
			shellChars = true
		case '\x00':
			return "", &QuoteError{ByteOffset: offs, Message: quoteErrNull}
		}
		if r == utf8.RuneError || !unicode.IsPrint(r) {
			if lang == LangPOSIX {
				return "", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX}
			}
			nonPrintable = true
		}
		rem = rem[size:]
		offs += size
	}
	if !shellChars && !nonPrintable && !IsKeyword(s) {
		// Nothing to quote; avoid allocating.
		return s, nil
	}

	// Single quotes are usually best,
	// as they don't require any escaping of characters.
	// If we have any invalid utf8 or non-printable runes,
	// use $'' so that we can escape them.
	// Note that we can't use double quotes for those.
	var b strings.Builder
	if nonPrintable {
		b.WriteString("$'")
		lastRequoteIfHex := false
		offs := 0
		for rem := s; len(rem) > 0; {
			nextRequoteIfHex := false
			r, size := utf8.DecodeRuneInString(rem)
			switch {
			case r == '\'', r == '\\':
				b.WriteByte('\\')
				b.WriteRune(r)
			case unicode.IsPrint(r) && r != utf8.RuneError:
				if lastRequoteIfHex && isHex(r) {
					b.WriteString("'$'")
				}
				b.WriteRune(r)
			case r == '\a':
				b.WriteString(`\a`)
			case r == '\b':
				b.WriteString(`\b`)
			case r == '\f':
				b.WriteString(`\f`)
			case r == '\n':
				b.WriteString(`\n`)
			case r == '\r':
				b.WriteString(`\r`)
			case r == '\t':
				b.WriteString(`\t`)
			case r == '\v':
				b.WriteString(`\v`)
			case r < utf8.RuneSelf, r == utf8.RuneError && size == 1:
				// \xXX, fixed at two hexadecimal characters.
				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 {
					nextRequoteIfHex = true
				}
			case r > utf8.MaxRune:
				// Not a valid Unicode code point?
				return "", &QuoteError{ByteOffset: offs, Message: quoteErrRange}
			case lang == LangMirBSDKorn && r > 0xFFFD:
				// From the CAVEATS section in R59's man page:
				//
				// mksh currently uses OPTU-16 internally, which is the same as
				// UTF-8 and CESU-8 with 0000..FFFD being valid codepoints.
				return "", &QuoteError{ByteOffset: offs, Message: quoteErrMksh}
			case r < 0x10000:
				// \uXXXX, fixed at four hexadecimal characters.
				fmt.Fprintf(&b, "\\u%04x", r)
			default:
				// \UXXXXXXXX, fixed at eight hexadecimal characters.
				fmt.Fprintf(&b, "\\U%08x", r)
			}
			rem = rem[size:]
			lastRequoteIfHex = nextRequoteIfHex
			offs += size
		}
		b.WriteString("'")
		return b.String(), nil
	}

	// Single quotes without any need for escaping.
	if !strings.Contains(s, "'") {
		return "'" + s + "'", nil
	}

	// The string contains single quotes,
	// so fall back to double quotes.
	b.WriteByte('"')
	for _, r := range s {
		switch r {
		case '"', '\\', '`', '$':
			b.WriteByte('\\')
		}
		b.WriteRune(r)
	}
	b.WriteByte('"')
	return b.String(), nil
}

func isHex(r rune) bool {
	return (r >= '0' && r <= '9') ||
		(r >= 'a' && r <= 'f') ||
		(r >= 'A' && r <= 'F')
}
0707010000005A000081A4000000000000000000000001686AE5C000000543000000000000000000000000000000000000001F00000000sh-3.12.0/syntax/quote_test.go// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"testing"

	"github.com/go-quicktest/qt"
)

func TestQuote(t *testing.T) {
	t.Parallel()
	tests := [...]struct {
		str  string
		lang LangVariant
		want any
	}{
		{"", LangBash, `''`},
		{"\a", LangBash, `$'\a'`},
		{"\b", LangBash, `$'\b'`},
		{"\f", LangBash, `$'\f'`},
		{"\n", LangBash, `$'\n'`},
		{"\r", LangBash, `$'\r'`},
		{"\t", LangBash, `$'\t'`},
		{"\v", LangBash, `$'\v'`},
		{"null\x00", LangBash, &QuoteError{4, quoteErrNull}},
		{"posix\x1b", LangPOSIX, &QuoteError{5, quoteErrPOSIX}},
		{"posix\n", LangPOSIX, &QuoteError{5, quoteErrPOSIX}},
		{"mksh16\U00086199", LangMirBSDKorn, &QuoteError{6, quoteErrMksh}},
		{"\x1b\x1caaa", LangBash, `$'\x1b\x1caaa'`},
		{"\x1b\x1caaa", LangMirBSDKorn, `$'\x1b\x1c'$'aaa'`},
		{"\xff\x00", LangBash, &QuoteError{1, quoteErrNull}},
	}

	for _, test := range tests {
		t.Run("", func(t *testing.T) {
			t.Parallel()

			got, gotErr := Quote(test.str, test.lang)
			switch want := test.want.(type) {
			case string:
				qt.Assert(t, qt.Equals(got, want))
				qt.Assert(t, qt.IsNil(gotErr))
			case *QuoteError:
				qt.Assert(t, qt.Equals(got, ""))
				qt.Assert(t, qt.DeepEquals(gotErr, error(want)))
			default:
				t.Fatalf("unexpected type: %T", want)
			}
		})
	}
}
0707010000005B000081A4000000000000000000000001686AE5C000000729000000000000000000000000000000000000002600000000sh-3.12.0/syntax/quotestate_string.go// 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) + ")"
}
0707010000005C000081A4000000000000000000000001686AE5C000001633000000000000000000000000000000000000001D00000000sh-3.12.0/syntax/simplify.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

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"
func Simplify(n Node) bool {
	s := simplifier{}
	Walk(n, s.visit)
	return s.modified
}

type simplifier struct {
	modified bool
}

func (s *simplifier) visit(node Node) bool {
	switch node := node.(type) {
	case *Assign:
		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:
		node.Index = s.removeParensArithm(node.Index)
		// don't inline params - same as above.

		if node.Slice == nil {
			break
		}
		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:
		node.X = s.removeParensArithm(node.X)
		node.X = s.inlineSimpleParams(node.X)
	case *ArithmCmd:
		node.X = s.removeParensArithm(node.X)
		node.X = s.inlineSimpleParams(node.X)
	case *ParenArithm:
		node.X = s.removeParensArithm(node.X)
		node.X = s.inlineSimpleParams(node.X)
	case *BinaryArithm:
		node.X = s.inlineSimpleParams(node.X)
		node.Y = s.inlineSimpleParams(node.Y)
	case *CmdSubst:
		node.Stmts = s.inlineSubshell(node.Stmts)
	case *Subshell:
		node.Stmts = s.inlineSubshell(node.Stmts)
	case *Word:
		node.Parts = s.simplifyWord(node.Parts)
	case *TestClause:
		node.X = s.removeParensTest(node.X)
		node.X = s.removeNegateTest(node.X)
	case *ParenTest:
		node.X = s.removeParensTest(node.X)
		node.X = s.removeNegateTest(node.X)
	case *BinaryTest:
		node.X = s.unquoteParams(node.X)
		node.X = s.removeNegateTest(node.X)
		if node.Op == TsMatchShort {
			s.modified = true
			node.Op = TsMatch
		}
		switch node.Op {
		case TsMatch, TsNoMatch:
			// unquoting enables globbing
		default:
			node.Y = s.unquoteParams(node.Y)
		}
		node.Y = s.removeNegateTest(node.Y)
	case *UnaryTest:
		node.X = s.unquoteParams(node.X)
	}
	return true
}

func (s *simplifier) simplifyWord(wps []WordPart) []WordPart {
parts:
	for i, wp := range wps {
		dq, _ := wp.(*DblQuoted)
		if dq == nil || len(dq.Parts) != 1 {
			break
		}
		lit, _ := dq.Parts[0].(*Lit)
		if lit == nil {
			break
		}
		var sb strings.Builder
		escaped := false
		for _, r := range lit.Value {
			switch r {
			case '\\':
				escaped = !escaped
				if escaped {
					continue
				}
			case '\'':
				continue parts
			case '$', '"', '`':
				escaped = false
			default:
				if escaped {
					continue parts
				}
				escaped = false
			}
			sb.WriteRune(r)
		}
		newVal := sb.String()
		if newVal == lit.Value {
			break
		}
		s.modified = true
		wps[i] = &SglQuoted{
			Left:   dq.Pos(),
			Right:  dq.End(),
			Dollar: dq.Dollar,
			Value:  newVal,
		}
	}
	return wps
}

func (s *simplifier) removeParensArithm(x ArithmExpr) ArithmExpr {
	for {
		par, _ := x.(*ParenArithm)
		if par == nil {
			return x
		}
		s.modified = true
		x = par.X
	}
}

func (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr {
	w, _ := x.(*Word)
	if w == nil || len(w.Parts) != 1 {
		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.
		return x
	}
	if pe.Excl || pe.Length || pe.Width || pe.Slice != nil ||
		pe.Repl != nil || pe.Exp != nil || pe.Index != nil {
		// A complex parameter expansion can't be simplified.
		//
		// Note that index expressions can't generally be simplified
		// either. It's fine to turn ${a[0]} into a[0], but others like
		// a[*] are invalid in many shells including Bash.
		return x
	}
	s.modified = true
	return &Word{Parts: []WordPart{pe.Param}}
}

func (s *simplifier) inlineSubshell(stmts []*Stmt) []*Stmt {
	for len(stmts) == 1 {
		st := stmts[0]
		if st.Negated || st.Background || st.Coprocess ||
			len(st.Redirs) > 0 {
			break
		}
		sub, _ := st.Cmd.(*Subshell)
		if sub == nil {
			break
		}
		s.modified = true
		stmts = sub.Stmts
	}
	return stmts
}

func (s *simplifier) unquoteParams(x TestExpr) TestExpr {
	w, _ := x.(*Word)
	if w == nil || len(w.Parts) != 1 {
		return x
	}
	dq, _ := w.Parts[0].(*DblQuoted)
	if dq == nil || len(dq.Parts) != 1 {
		return x
	}
	if _, ok := dq.Parts[0].(*ParamExp); !ok {
		return x
	}
	s.modified = true
	w.Parts = dq.Parts
	return w
}

func (s *simplifier) removeParensTest(x TestExpr) TestExpr {
	for {
		par, _ := x.(*ParenTest)
		if par == nil {
			return x
		}
		s.modified = true
		x = par.X
	}
}

func (s *simplifier) removeNegateTest(x TestExpr) TestExpr {
	u, _ := x.(*UnaryTest)
	if u == nil || u.Op != TsNot {
		return x
	}
	switch y := u.X.(type) {
	case *UnaryTest:
		switch y.Op {
		case TsEmpStr:
			y.Op = TsNempStr
			s.modified = true
			return y
		case TsNempStr:
			y.Op = TsEmpStr
			s.modified = true
			return y
		case TsNot:
			s.modified = true
			return y.X
		}
	case *BinaryTest:
		switch y.Op {
		case TsMatch:
			y.Op = TsNoMatch
			s.modified = true
			return y
		case TsNoMatch:
			y.Op = TsMatch
			s.modified = true
			return y
		}
	}
	return x
}
0707010000005D000081A4000000000000000000000001686AE5C000000963000000000000000000000000000000000000002200000000sh-3.12.0/syntax/simplify_test.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"bytes"
	"strings"
	"testing"
)

type simplifyTest struct {
	in, want string
}

func noSimple(in string) simplifyTest {
	return simplifyTest{in: in, want: in}
}

var simplifyTests = [...]simplifyTest{
	// arithmetic exprs
	{"$((a + ((b - c))))", "$((a + (b - c)))"},
	{"$((a + (((b - c)))))", "$((a + (b - c)))"},
	{"$(((b - c)))", "$((b - c))"},
	{"(((b - c)))", "((b - c))"},
	{"${foo[(1)]}", "${foo[1]}"},
	{"${foo:(1):(2)}", "${foo:1:2}"},
	{"a[(1)]=2", "a[1]=2"},
	{"$(($a + ${b}))", "$((a + b))"},
	noSimple("$((${!a} + ${#b}))"),
	noSimple("a[$b]=2"),
	noSimple("${a[$b]}"),
	noSimple("${a[@]}"),
	noSimple("((${a[@]}))"),
	noSimple("((${a[*]}))"),
	noSimple("((${a[0]}))"),
	noSimple("(($3 == $#))"),

	// test exprs
	{`[[ "$foo" == "bar" ]]`, `[[ $foo == "bar" ]]`},
	{`[[ (-z "$foo") ]]`, `[[ -z $foo ]]`},
	{`[[ "a b" > "$c" ]]`, `[[ "a b" > $c ]]`},
	{`[[ ! -n $foo ]]`, `[[ -z $foo ]]`},
	{`[[ ! ! -e a && ! -z $b ]]`, `[[ -e a && -n $b ]]`},
	{`[[ (! a == b) || (! c != d) ]]`, `[[ (a != b) || (c == d) ]]`},
	noSimple(`[[ -n a$b && -n $c ]]`),
	noSimple(`[[ ! -e foo ]]`),
	noSimple(`[[ foo == bar ]]`),
	{`[[ foo = bar ]]`, `[[ foo == bar ]]`},

	// stmts
	{"$( (sts))", "$(sts)"},
	{"( ( (sts)))", "(sts)"},
	noSimple("( (sts) >f)"),
	noSimple("(\n\tx\n\t(sts)\n)"),

	// strings
	noSimple(`"foo"`),
	noSimple(`"foo$bar"`),
	noSimple(`"$bar"`),
	noSimple(`"f'o\\o"`),
	noSimple(`"fo\'o"`),
	noSimple(`"fo\\'o"`),
	noSimple(`"fo\no"`),
	{`"fo\$o"`, `'fo$o'`},
	{`"fo\"o"`, `'fo"o'`},
	{"\"fo\\`o\"", "'fo`o'"},
	noSimple(`fo"o"bar`),
	noSimple(`foo""bar`),
}

func TestSimplify(t *testing.T) {
	t.Parallel()
	parser := NewParser()
	printer := NewPrinter()
	for _, tc := range simplifyTests {
		t.Run("", func(t *testing.T) {
			prog, err := parser.Parse(strings.NewReader(tc.in), "")
			if err != nil {
				t.Fatal(err)
			}
			simplified := Simplify(prog)
			var buf bytes.Buffer
			printer.Print(&buf, prog)
			want := tc.want + "\n"
			if got := buf.String(); got != want {
				t.Fatalf("Simplify mismatch of %q\nwant: %q\ngot:  %q",
					tc.in, want, got)
			}
			if simplified && tc.in == tc.want {
				t.Fatalf("returned true but did not simplify")
			} else if !simplified && tc.in != tc.want {
				t.Fatalf("returned false but did simplify")
			}
		})
	}
}
0707010000005E000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001A00000000sh-3.12.0/syntax/testdata0707010000005F000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001F00000000sh-3.12.0/syntax/testdata/fuzz07070100000060000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000002E00000000sh-3.12.0/syntax/testdata/fuzz/FuzzParsePrint07070100000061000081A4000000000000000000000001686AE5C0000000A4000000000000000000000000000000000000003F00000000sh-3.12.0/syntax/testdata/fuzz/FuzzParsePrint/293db3718a4ab7a5go test fuzz v1
string("A=0(")
byte('\x00')
bool(true)
bool(false)
byte('\x00')
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
07070100000062000081A4000000000000000000000001686AE5C0000000AB000000000000000000000000000000000000003F00000000sh-3.12.0/syntax/testdata/fuzz/FuzzParsePrint/6d0dc226922dc40cgo test fuzz v1
string("`\\$\\\\000")
byte('\x02')
bool(true)
bool(false)
byte('\x00')
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
07070100000063000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000002900000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote07070100000064000081A4000000000000000000000001686AE5C00000002E000000000000000000000000000000000000006A00000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote/23cf0175e40438e8033b11cdd1441a2d2893a99144c4ac0f2b5f4caa113c9eddgo test fuzz v1
string("\uffff")
byte('\x02')
07070100000065000081A4000000000000000000000001686AE5C00000002D000000000000000000000000000000000000006A00000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote/25f36feab4af00bc4dfc3cf56da02b842b62ba8c5ac44862b5b3b776a0d519b4go test fuzz v1
string("\xb3c")
byte('\x02')
07070100000066000081A4000000000000000000000001686AE5C00000002D000000000000000000000000000000000000006A00000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote/2788bd30d386289e06a1024a030ad5ab7f363c703bea8a5d035de174491029bfgo test fuzz v1
string("\x0fC")
byte('\x00')
07070100000067000081A4000000000000000000000001686AE5C00000002F000000000000000000000000000000000000006A00000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote/39d5fdf93d52b2cd50fb9582b27c82d159de0575623865538ced2a7780499fa6go test fuzz v1
string("\u05f5A")
byte('\x00')
07070100000068000081A4000000000000000000000001686AE5C000000032000000000000000000000000000000000000006A00000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote/6fcce067200fb8ae6d4c2b1b7c1f55d3f7e4b38f4ee4f05e50e496a7c399f2d8go test fuzz v1
string("\U00086199")
byte('\x02')
07070100000069000081A4000000000000000000000001686AE5C00000002C000000000000000000000000000000000000006A00000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote/b26cd471412059c6ab6aa27b6153d42d2d00cbb00ad11d3cd88a192a7dfd2cdfgo test fuzz v1
string("\xb6")
byte('\x01')
0707010000006A000081A4000000000000000000000001686AE5C00000002D000000000000000000000000000000000000006A00000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote/df6b5d69da50c7d58ca13f6dde15e2a7224a53ce7bd72a02d49893e580b6775bgo test fuzz v1
string("\x050")
byte('\x02')
0707010000006B000081A4000000000000000000000001686AE5C000000033000000000000000000000000000000000000006A00000000sh-3.12.0/syntax/testdata/fuzz/FuzzQuote/ea14da9b0299f4463c20659e2a51808fef8d5fb0de6324f0de64153511d4b1f8go test fuzz v1
string("\U000600a04")
byte('\x00')
0707010000006C000081A4000000000000000000000001686AE5C000000F14000000000000000000000000000000000000002100000000sh-3.12.0/syntax/token_string.go// Code generated by "stringer -type token -linecomment -trimprefix _"; 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[illegalTok-0]
	_ = x[_EOF-1]
	_ = x[_Newl-2]
	_ = 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]
}

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?(*(+(@(!("

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}

func (i token) String() string {
	if i >= token(len(_token_index)-1) {
		return "token(" + strconv.FormatInt(int64(i), 10) + ")"
	}
	return _token_name[_token_index[i]:_token_index[i+1]]
}
0707010000006D000081A4000000000000000000000001686AE5C00000268F000000000000000000000000000000000000001B00000000sh-3.12.0/syntax/tokens.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

//go:generate stringer -type token -linecomment -trimprefix _

type token uint32

// The list of all possible tokens.
const (
	illegalTok token = iota

	_EOF
	_Newl
	_Lit
	_LitWord
	_LitRedir

	sglQuote // '
	dblQuote // "
	bckQuote // `

	and    // &
	andAnd // &&
	orOr   // ||
	or     // |
	orAnd  // |&

	dollar       // $
	dollSglQuote // $'
	dollDblQuote // $"
	dollBrace    // ${
	dollBrack    // $[
	dollParen    // $(
	dollDblParen // $((
	leftBrack    // [
	dblLeftBrack // [[
	leftParen    // (
	dblLeftParen // ((

	rightBrace    // }
	rightBrack    // ]
	rightParen    // )
	dblRightParen // ))
	semicolon     // ;

	dblSemicolon // ;;
	semiAnd      // ;&
	dblSemiAnd   // ;;&
	semiOr       // ;|

	exclMark // !
	tilde    // ~
	addAdd   // ++
	subSub   // --
	star     // *
	power    // **
	equal    // ==
	nequal   // !=
	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 // >(

	plus     // +
	colPlus  // :+
	minus    // -
	colMinus // :-
	quest    // ?
	colQuest // :?
	assgn    // =
	colAssgn // :=
	perc     // %
	dblPerc  // %%
	hash     // #
	dblHash  // ##
	caret    // ^
	dblCaret // ^^
	comma    // ,
	dblComma // ,,
	at       // @
	slash    // /
	dblSlash // //
	colon    // :

	tsExists  // -e
	tsRegFile // -f
	tsDirect  // -d
	tsCharSp  // -c
	tsBlckSp  // -b
	tsNmPipe  // -p
	tsSocket  // -S
	tsSmbLink // -L
	tsSticky  // -k
	tsGIDSet  // -g
	tsUIDSet  // -u
	tsGrpOwn  // -G
	tsUsrOwn  // -O
	tsModif   // -N
	tsRead    // -r
	tsWrite   // -w
	tsExec    // -x
	tsNoEmpty // -s
	tsFdTerm  // -t
	tsEmpStr  // -z
	tsNempStr // -n
	tsOptSet  // -o
	tsVarSet  // -v
	tsRefVar  // -R

	tsReMatch // =~
	tsNewer   // -nt
	tsOlder   // -ot
	tsDevIno  // -ef
	tsEql     // -eq
	tsNeq     // -ne
	tsLeq     // -le
	tsGeq     // -ge
	tsLss     // -lt
	tsGtr     // -gt

	globQuest // ?(
	globStar  // *(
	globPlus  // +(
	globAt    // @(
	globExcl  // !(
)

type RedirOperator token

const (
	RdrOut   = RedirOperator(rdrOut) + iota // >
	AppOut                                  // >>
	RdrIn                                   // <
	RdrInOut                                // <>
	DplIn                                   // <&
	DplOut                                  // >&
	ClbOut                                  // >|
	Hdoc                                    // <<
	DashHdoc                                // <<-
	WordHdoc                                // <<<
	RdrAll                                  // &>
	AppAll                                  // &>>
)

type ProcOperator token

const (
	CmdIn  = ProcOperator(cmdIn) + iota // <(
	CmdOut                              // >(
)

type GlobOperator token

const (
	GlobZeroOrOne  = GlobOperator(globQuest) + iota // ?(
	GlobZeroOrMore                                  // *(
	GlobOneOrMore                                   // +(
	GlobOne                                         // @(
	GlobExcept                                      // !(
)

type BinCmdOperator token

const (
	AndStmt = BinCmdOperator(andAnd) + iota // &&
	OrStmt                                  // ||
	Pipe                                    // |
	PipeAll                                 // |&
)

type CaseOperator token

const (
	Break       = CaseOperator(dblSemicolon) + iota // ;;
	Fallthrough                                     // ;&
	Resume                                          // ;;&
	ResumeKorn                                      // ;|
)

type ParNamesOperator token

const (
	NamesPrefix      = ParNamesOperator(star) // *
	NamesPrefixWords = ParNamesOperator(at)   // @
)

type ParExpOperator token

const (
	AlternateUnset       = ParExpOperator(plus) + iota // +
	AlternateUnsetOrNull                               // :+
	DefaultUnset                                       // -
	DefaultUnsetOrNull                                 // :-
	ErrorUnset                                         // ?
	ErrorUnsetOrNull                                   // :?
	AssignUnset                                        // =
	AssignUnsetOrNull                                  // :=
	RemSmallSuffix                                     // %
	RemLargeSuffix                                     // %%
	RemSmallPrefix                                     // #
	RemLargePrefix                                     // ##
	UpperFirst                                         // ^
	UpperAll                                           // ^^
	LowerFirst                                         // ,
	LowerAll                                           // ,,
	OtherParamOps                                      // @
)

type UnAritOperator token

const (
	Not         = UnAritOperator(exclMark) + iota // !
	BitNegation                                   // ~
	Inc                                           // ++
	Dec                                           // --
	Plus        = UnAritOperator(plus)            // +
	Minus       = UnAritOperator(minus)           // -
)

type BinAritOperator token

const (
	Add = BinAritOperator(plus)   // +
	Sub = BinAritOperator(minus)  // -
	Mul = BinAritOperator(star)   // *
	Quo = BinAritOperator(slash)  // /
	Rem = BinAritOperator(perc)   // %
	Pow = BinAritOperator(power)  // **
	Eql = BinAritOperator(equal)  // ==
	Gtr = BinAritOperator(rdrOut) // >
	Lss = BinAritOperator(rdrIn)  // <
	Neq = BinAritOperator(nequal) // !=
	Leq = BinAritOperator(lequal) // <=
	Geq = BinAritOperator(gequal) // >=
	And = BinAritOperator(and)    // &
	Or  = BinAritOperator(or)     // |
	Xor = BinAritOperator(caret)  // ^
	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) // >>=
)

type UnTestOperator token

const (
	TsExists  = UnTestOperator(tsExists) + iota // -e
	TsRegFile                                   // -f
	TsDirect                                    // -d
	TsCharSp                                    // -c
	TsBlckSp                                    // -b
	TsNmPipe                                    // -p
	TsSocket                                    // -S
	TsSmbLink                                   // -L
	TsSticky                                    // -k
	TsGIDSet                                    // -g
	TsUIDSet                                    // -u
	TsGrpOwn                                    // -G
	TsUsrOwn                                    // -O
	TsModif                                     // -N
	TsRead                                      // -r
	TsWrite                                     // -w
	TsExec                                      // -x
	TsNoEmpty                                   // -s
	TsFdTerm                                    // -t
	TsEmpStr                                    // -z
	TsNempStr                                   // -n
	TsOptSet                                    // -o
	TsVarSet                                    // -v
	TsRefVar                                    // -R
	TsNot     = UnTestOperator(exclMark)        // !
	TsParen   = UnTestOperator(leftParen)       // (
)

type BinTestOperator token

const (
	TsReMatch    = BinTestOperator(tsReMatch) + iota // =~
	TsNewer                                          // -nt
	TsOlder                                          // -ot
	TsDevIno                                         // -ef
	TsEql                                            // -eq
	TsNeq                                            // -ne
	TsLeq                                            // -le
	TsGeq                                            // -ge
	TsLss                                            // -lt
	TsGtr                                            // -gt
	AndTest      = BinTestOperator(andAnd)           // &&
	OrTest       = BinTestOperator(orOr)             // ||
	TsMatchShort = BinTestOperator(assgn)            // =
	TsMatch      = BinTestOperator(equal)            // ==
	TsNoMatch    = BinTestOperator(nequal)           // !=
	TsBefore     = BinTestOperator(rdrIn)            // <
	TsAfter      = BinTestOperator(rdrOut)           // >
)

func (o RedirOperator) String() string    { return token(o).String() }
func (o ProcOperator) String() string     { return token(o).String() }
func (o GlobOperator) String() string     { return token(o).String() }
func (o BinCmdOperator) String() string   { return token(o).String() }
func (o CaseOperator) String() string     { return token(o).String() }
func (o ParNamesOperator) String() string { return token(o).String() }
func (o ParExpOperator) String() string   { return token(o).String() }
func (o UnAritOperator) String() string   { return token(o).String() }
func (o BinAritOperator) String() string  { return token(o).String() }
func (o UnTestOperator) String() string   { return token(o).String() }
func (o BinTestOperator) String() string  { return token(o).String() }
0707010000006E000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000001B00000000sh-3.12.0/syntax/typedjson0707010000006F000081A4000000000000000000000001686AE5C0000023B3000000000000000000000000000000000000002300000000sh-3.12.0/syntax/typedjson/json.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package typedjson allows encoding and decoding shell syntax trees as JSON.
// The decoding process needs to know what syntax node types to decode into,
// so the "typed JSON" requires "Type" keys in some syntax tree node objects:
//
//   - The root node
//   - Any node represented as an interface field in the parent Go type
//
// The types of all other nodes can be inferred from context alone.
//
// For the sake of efficiency and simplicity, the "Type" key
// described above must be first in each JSON object.
package typedjson

// TODO: encoding and decoding nodes other than File is untested.

import (
	"encoding/json"
	"fmt"
	"io"
	"reflect"

	"mvdan.cc/sh/v3/syntax"
)

// Encode is a shortcut for [EncodeOptions.Encode] with the default options.
func Encode(w io.Writer, node syntax.Node) error {
	return EncodeOptions{}.Encode(w, node)
}

// EncodeOptions allows configuring how syntax nodes are encoded.
type EncodeOptions struct {
	Indent string // e.g. "\t"

	// Allows us to add options later.
}

// Encode writes node to w in its typed JSON form,
// as described in the package documentation.
func (opts EncodeOptions) Encode(w io.Writer, node syntax.Node) error {
	val := reflect.ValueOf(node)
	encVal, tname := encodeValue(val)
	if tname == "" {
		panic("node did not contain a named type?")
	}
	encVal.Elem().Field(0).SetString(tname)
	enc := json.NewEncoder(w)
	if opts.Indent != "" {
		enc.SetIndent("", opts.Indent)
	}
	return enc.Encode(encVal.Interface())
}

func encodeValue(val reflect.Value) (reflect.Value, string) {
	switch val.Kind() {
	case reflect.Ptr:
		if val.IsNil() {
			break
		}
		return encodeValue(val.Elem())
	case reflect.Interface:
		if val.IsNil() {
			break
		}
		enc, tname := encodeValue(val.Elem())
		if tname == "" {
			panic("interface did not contain a named type?")
		}
		enc.Elem().Field(0).SetString(tname)
		return enc, ""
	case reflect.Struct:
		// Construct a new struct with an optional Type, Pos and End,
		// and then all the visible fields which aren't positions.
		typ := val.Type()
		fields := []reflect.StructField{typeField, posField, endField}
		for i := range typ.NumField() {
			field := typ.Field(i)
			typ := anyType
			if field.Type == posType {
				typ = exportedPosType
			}
			fields = append(fields, reflect.StructField{
				Name: field.Name,
				Type: typ,
				Tag:  `json:",omitempty"`,
			})
		}
		encTyp := reflect.StructOf(fields)
		enc := reflect.New(encTyp).Elem()

		// Node methods are defined on struct pointer receivers.
		if node, _ := val.Addr().Interface().(syntax.Node); node != nil {
			encodePos(enc.Field(1), node.Pos()) // posField
			encodePos(enc.Field(2), node.End()) // endField
		}
		// Do the rest of the fields.
		for i := 3; i < encTyp.NumField(); i++ {
			ftyp := encTyp.Field(i)
			fval := val.FieldByName(ftyp.Name)
			if ftyp.Type == exportedPosType {
				encodePos(enc.Field(i), fval.Interface().(syntax.Pos))
			} else {
				encElem, _ := encodeValue(fval)
				if encElem.IsValid() {
					enc.Field(i).Set(encElem)
				}
			}
		}

		// Addr helps prevent an allocation as we use any fields.
		return enc.Addr(), typ.Name()
	case reflect.Slice:
		n := val.Len()
		if n == 0 {
			break
		}
		enc := reflect.MakeSlice(anySliceType, n, n)
		for i := range n {
			elem := val.Index(i)
			encElem, _ := encodeValue(elem)
			enc.Index(i).Set(encElem)
		}
		return enc, ""
	case reflect.Bool:
		if val.Bool() {
			return val, ""
		}
	case reflect.String:
		if val.String() != "" {
			return val, ""
		}
	case reflect.Uint32:
		if val.Uint() != 0 {
			return val, ""
		}
	default:
		panic(val.Kind().String())
	}
	return noValue, ""
}

var (
	noValue reflect.Value

	anyType         = reflect.TypeFor[any]()
	anySliceType    = reflect.TypeFor[[]any]()
	posType         = reflect.TypeFor[syntax.Pos]()
	exportedPosType = reflect.TypeFor[*exportedPos]()

	// TODO(v4): derived fields like Type, Pos, and End should have clearly
	// different names to prevent confusion. For example: _type, _pos, _end.
	typeField = reflect.StructField{
		Name: "Type",
		Type: reflect.TypeFor[string](),
		Tag:  `json:",omitempty"`,
	}
	posField = reflect.StructField{
		Name: "Pos",
		Type: exportedPosType,
		Tag:  `json:",omitempty"`,
	}
	endField = reflect.StructField{
		Name: "End",
		Type: exportedPosType,
		Tag:  `json:",omitempty"`,
	}
)

type exportedPos struct {
	Offset, Line, Col uint
}

func encodePos(encPtr reflect.Value, val syntax.Pos) {
	// TODO: perhaps we should encode recovered positions, as that is still useful information.
	if !val.IsValid() {
		return
	}
	enc := reflect.New(exportedPosType.Elem())
	encPtr.Set(enc)
	enc = enc.Elem()

	enc.Field(0).SetUint(uint64(val.Offset()))
	enc.Field(1).SetUint(uint64(val.Line()))
	enc.Field(2).SetUint(uint64(val.Col()))
}

func decodePos(val reflect.Value, enc map[string]any) {
	offset := uint(enc["Offset"].(float64))
	line := uint(enc["Line"].(float64))
	column := uint(enc["Col"].(float64))
	val.Set(reflect.ValueOf(syntax.NewPos(offset, line, column)))
}

// Decode is a shortcut for [DecodeOptions.Decode] with the default options.
func Decode(r io.Reader) (syntax.Node, error) {
	return DecodeOptions{}.Decode(r)
}

// DecodeOptions allows configuring how syntax nodes are encoded.
type DecodeOptions struct {
	// Empty for now; allows us to add options later.
}

// Decode writes node to w in its typed JSON form,
// as described in the package documentation.
func (opts DecodeOptions) Decode(r io.Reader) (syntax.Node, error) {
	var enc any
	if err := json.NewDecoder(r).Decode(&enc); err != nil {
		return nil, err
	}
	node := new(syntax.Node)
	if err := decodeValue(reflect.ValueOf(node).Elem(), enc); err != nil {
		return nil, err
	}
	return *node, nil
}

var nodeByName = map[string]reflect.Type{
	"File": reflect.TypeFor[syntax.File](),
	"Word": reflect.TypeFor[syntax.Word](),

	"Lit":       reflect.TypeFor[syntax.Lit](),
	"SglQuoted": reflect.TypeFor[syntax.SglQuoted](),
	"DblQuoted": reflect.TypeFor[syntax.DblQuoted](),
	"ParamExp":  reflect.TypeFor[syntax.ParamExp](),
	"CmdSubst":  reflect.TypeFor[syntax.CmdSubst](),
	"CallExpr":  reflect.TypeFor[syntax.CallExpr](),
	"ArithmExp": reflect.TypeFor[syntax.ArithmExp](),
	"ProcSubst": reflect.TypeFor[syntax.ProcSubst](),
	"ExtGlob":   reflect.TypeFor[syntax.ExtGlob](),
	"BraceExp":  reflect.TypeFor[syntax.BraceExp](),

	"ArithmCmd":    reflect.TypeFor[syntax.ArithmCmd](),
	"BinaryCmd":    reflect.TypeFor[syntax.BinaryCmd](),
	"IfClause":     reflect.TypeFor[syntax.IfClause](),
	"ForClause":    reflect.TypeFor[syntax.ForClause](),
	"WhileClause":  reflect.TypeFor[syntax.WhileClause](),
	"CaseClause":   reflect.TypeFor[syntax.CaseClause](),
	"Block":        reflect.TypeFor[syntax.Block](),
	"Subshell":     reflect.TypeFor[syntax.Subshell](),
	"FuncDecl":     reflect.TypeFor[syntax.FuncDecl](),
	"TestClause":   reflect.TypeFor[syntax.TestClause](),
	"DeclClause":   reflect.TypeFor[syntax.DeclClause](),
	"LetClause":    reflect.TypeFor[syntax.LetClause](),
	"TimeClause":   reflect.TypeFor[syntax.TimeClause](),
	"CoprocClause": reflect.TypeFor[syntax.CoprocClause](),
	"TestDecl":     reflect.TypeFor[syntax.TestDecl](),

	"UnaryArithm":  reflect.TypeFor[syntax.UnaryArithm](),
	"BinaryArithm": reflect.TypeFor[syntax.BinaryArithm](),
	"ParenArithm":  reflect.TypeFor[syntax.ParenArithm](),

	"UnaryTest":  reflect.TypeFor[syntax.UnaryTest](),
	"BinaryTest": reflect.TypeFor[syntax.BinaryTest](),
	"ParenTest":  reflect.TypeFor[syntax.ParenTest](),

	"WordIter":   reflect.TypeFor[syntax.WordIter](),
	"CStyleLoop": reflect.TypeFor[syntax.CStyleLoop](),
}

func decodeValue(val reflect.Value, enc any) error {
	switch enc := enc.(type) {
	case map[string]any:
		if val.Kind() == reflect.Ptr && val.IsNil() {
			val.Set(reflect.New(val.Type().Elem()))
		}
		if typeName, _ := enc["Type"].(string); typeName != "" {
			typ := nodeByName[typeName]
			if typ == nil {
				return fmt.Errorf("unknown type: %q", typeName)
			}
			val.Set(reflect.New(typ))
		}
		for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
			val = val.Elem()
		}
		for name, fv := range enc {
			fval := val.FieldByName(name)
			switch name {
			case "Type", "Pos", "End":
				// Type is already used above. Pos and End came from method calls.
				continue
			}
			if !fval.IsValid() {
				return fmt.Errorf("unknown field for %s: %q", val.Type(), name)
			}
			if fval.Type() == posType {
				// TODO: don't panic on bad input
				decodePos(fval, fv.(map[string]any))
				continue
			}
			if err := decodeValue(fval, fv); err != nil {
				return err
			}
		}
	case []any:
		for _, encElem := range enc {
			elem := reflect.New(val.Type().Elem()).Elem()
			if err := decodeValue(elem, encElem); err != nil {
				return err
			}
			val.Set(reflect.Append(val, elem))
		}
	case float64:
		// Tokens and thus operators are uint32, but encoding/json defaults to float64.
		// TODO: reject invalid operators.
		u := uint64(enc)
		val.SetUint(u)
	default:
		if enc != nil {
			val.Set(reflect.ValueOf(enc))
		}
	}
	return nil
}
07070100000070000081A4000000000000000000000001686AE5C00000091E000000000000000000000000000000000000002800000000sh-3.12.0/syntax/typedjson/json_test.go// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package typedjson_test

import (
	"bytes"
	"flag"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/go-quicktest/qt"

	"mvdan.cc/sh/v3/syntax"
	"mvdan.cc/sh/v3/syntax/typedjson"
)

var update = flag.Bool("u", false, "update output files")

func TestRoundtrip(t *testing.T) {
	t.Parallel()

	dir := filepath.Join("testdata", "roundtrip")
	shellPaths, err := filepath.Glob(filepath.Join(dir, "*.sh"))
	qt.Assert(t, qt.IsNil(err))
	for _, shellPath := range shellPaths {
		name := strings.TrimSuffix(filepath.Base(shellPath), ".sh")
		jsonPath := filepath.Join(dir, name+".json")
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			shellInput, err := os.ReadFile(shellPath)
			qt.Assert(t, qt.IsNil(err))
			jsonInput, err := os.ReadFile(jsonPath)
			if !*update { // allow it to not exist
				qt.Assert(t, qt.IsNil(err))
			}
			sb := new(strings.Builder)

			// Parse the shell source and check that it is well formatted.
			parser := syntax.NewParser(syntax.KeepComments(true))
			node, err := parser.Parse(bytes.NewReader(shellInput), "")
			qt.Assert(t, qt.IsNil(err))

			printer := syntax.NewPrinter()
			sb.Reset()
			err = printer.Print(sb, node)
			qt.Assert(t, qt.IsNil(err))
			qt.Assert(t, qt.Equals(sb.String(), string(shellInput)))

			// Validate writing the pretty JSON.
			sb.Reset()
			encOpts := typedjson.EncodeOptions{Indent: "\t"}
			err = encOpts.Encode(sb, node)
			qt.Assert(t, qt.IsNil(err))
			got := sb.String()
			if *update {
				err := os.WriteFile(jsonPath, []byte(got), 0o666)
				qt.Assert(t, qt.IsNil(err))
			} else {
				qt.Assert(t, qt.Equals(got, string(jsonInput)))
			}

			// Ensure we don't use the originally parsed node again.
			node = nil

			// Validate reading the pretty JSON and check that it formats the same.
			node2, err := typedjson.Decode(bytes.NewReader(jsonInput))
			qt.Assert(t, qt.IsNil(err))

			sb.Reset()
			err = printer.Print(sb, node2)
			qt.Assert(t, qt.IsNil(err))
			qt.Assert(t, qt.Equals(sb.String(), string(shellInput)))

			// Validate that emitting the JSON again produces the same result.
			sb.Reset()
			err = encOpts.Encode(sb, node2)
			qt.Assert(t, qt.IsNil(err))
			got = sb.String()
			qt.Assert(t, qt.Equals(got, string(jsonInput)))
		})
	}
}
07070100000071000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000002400000000sh-3.12.0/syntax/typedjson/testdata07070100000072000041ED000000000000000000000002686AE5C000000000000000000000000000000000000000000000002E00000000sh-3.12.0/syntax/typedjson/testdata/roundtrip07070100000073000081A4000000000000000000000001686AE5C00000FE43000000000000000000000000000000000000003800000000sh-3.12.0/syntax/typedjson/testdata/roundtrip/file.json{
	"Type": "File",
	"Pos": {
		"Offset": 0,
		"Line": 1,
		"Col": 1
	},
	"End": {
		"Offset": 368,
		"Line": 30,
		"Col": 4
	},
	"Stmts": [
		{
			"Pos": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			},
			"End": {
				"Offset": 3,
				"Line": 1,
				"Col": 4
			},
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 0,
					"Line": 1,
					"Col": 1
				},
				"End": {
					"Offset": 3,
					"Line": 1,
					"Col": 4
				},
				"Args": [
					{
						"Pos": {
							"Offset": 0,
							"Line": 1,
							"Col": 1
						},
						"End": {
							"Offset": 3,
							"Line": 1,
							"Col": 4
						},
						"Parts": [
							{
								"Type": "Lit",
								"Pos": {
									"Offset": 0,
									"Line": 1,
									"Col": 1
								},
								"End": {
									"Offset": 3,
									"Line": 1,
									"Col": 4
								},
								"ValuePos": {
									"Offset": 0,
									"Line": 1,
									"Col": 1
								},
								"ValueEnd": {
									"Offset": 3,
									"Line": 1,
									"Col": 4
								},
								"Value": "foo"
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 4,
				"Line": 2,
				"Col": 1
			},
			"End": {
				"Offset": 9,
				"Line": 2,
				"Col": 6
			},
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 6,
					"Line": 2,
					"Col": 3
				},
				"End": {
					"Offset": 9,
					"Line": 2,
					"Col": 6
				},
				"Args": [
					{
						"Pos": {
							"Offset": 6,
							"Line": 2,
							"Col": 3
						},
						"End": {
							"Offset": 9,
							"Line": 2,
							"Col": 6
						},
						"Parts": [
							{
								"Type": "Lit",
								"Pos": {
									"Offset": 6,
									"Line": 2,
									"Col": 3
								},
								"End": {
									"Offset": 9,
									"Line": 2,
									"Col": 6
								},
								"ValuePos": {
									"Offset": 6,
									"Line": 2,
									"Col": 3
								},
								"ValueEnd": {
									"Offset": 9,
									"Line": 2,
									"Col": 6
								},
								"Value": "foo"
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 4,
				"Line": 2,
				"Col": 1
			},
			"Negated": true
		},
		{
			"Pos": {
				"Offset": 10,
				"Line": 3,
				"Col": 1
			},
			"End": {
				"Offset": 15,
				"Line": 3,
				"Col": 6
			},
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 10,
					"Line": 3,
					"Col": 1
				},
				"End": {
					"Offset": 13,
					"Line": 3,
					"Col": 4
				},
				"Args": [
					{
						"Pos": {
							"Offset": 10,
							"Line": 3,
							"Col": 1
						},
						"End": {
							"Offset": 13,
							"Line": 3,
							"Col": 4
						},
						"Parts": [
							{
								"Type": "Lit",
								"Pos": {
									"Offset": 10,
									"Line": 3,
									"Col": 1
								},
								"End": {
									"Offset": 13,
									"Line": 3,
									"Col": 4
								},
								"ValuePos": {
									"Offset": 10,
									"Line": 3,
									"Col": 1
								},
								"ValueEnd": {
									"Offset": 13,
									"Line": 3,
									"Col": 4
								},
								"Value": "foo"
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 10,
				"Line": 3,
				"Col": 1
			},
			"Semicolon": {
				"Offset": 14,
				"Line": 3,
				"Col": 5
			},
			"Background": true
		},
		{
			"Pos": {
				"Offset": 16,
				"Line": 4,
				"Col": 1
			},
			"End": {
				"Offset": 27,
				"Line": 4,
				"Col": 12
			},
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 16,
					"Line": 4,
					"Col": 1
				},
				"End": {
					"Offset": 27,
					"Line": 4,
					"Col": 12
				},
				"Args": [
					{
						"Pos": {
							"Offset": 16,
							"Line": 4,
							"Col": 1
						},
						"End": {
							"Offset": 21,
							"Line": 4,
							"Col": 6
						},
						"Parts": [
							{
								"Type": "SglQuoted",
								"Pos": {
									"Offset": 16,
									"Line": 4,
									"Col": 1
								},
								"End": {
									"Offset": 21,
									"Line": 4,
									"Col": 6
								},
								"Left": {
									"Offset": 16,
									"Line": 4,
									"Col": 1
								},
								"Right": {
									"Offset": 20,
									"Line": 4,
									"Col": 5
								},
								"Value": "foo"
							}
						]
					},
					{
						"Pos": {
							"Offset": 22,
							"Line": 4,
							"Col": 7
						},
						"End": {
							"Offset": 27,
							"Line": 4,
							"Col": 12
						},
						"Parts": [
							{
								"Type": "DblQuoted",
								"Pos": {
									"Offset": 22,
									"Line": 4,
									"Col": 7
								},
								"End": {
									"Offset": 27,
									"Line": 4,
									"Col": 12
								},
								"Left": {
									"Offset": 22,
									"Line": 4,
									"Col": 7
								},
								"Right": {
									"Offset": 26,
									"Line": 4,
									"Col": 11
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 23,
											"Line": 4,
											"Col": 8
										},
										"End": {
											"Offset": 26,
											"Line": 4,
											"Col": 11
										},
										"ValuePos": {
											"Offset": 23,
											"Line": 4,
											"Col": 8
										},
										"ValueEnd": {
											"Offset": 26,
											"Line": 4,
											"Col": 11
										},
										"Value": "bar"
									}
								]
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 16,
				"Line": 4,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 28,
				"Line": 5,
				"Col": 1
			},
			"End": {
				"Offset": 50,
				"Line": 5,
				"Col": 23
			},
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 28,
					"Line": 5,
					"Col": 1
				},
				"End": {
					"Offset": 50,
					"Line": 5,
					"Col": 23
				},
				"Args": [
					{
						"Pos": {
							"Offset": 28,
							"Line": 5,
							"Col": 1
						},
						"End": {
							"Offset": 34,
							"Line": 5,
							"Col": 7
						},
						"Parts": [
							{
								"Type": "ParamExp",
								"Pos": {
									"Offset": 28,
									"Line": 5,
									"Col": 1
								},
								"End": {
									"Offset": 34,
									"Line": 5,
									"Col": 7
								},
								"Dollar": {
									"Offset": 28,
									"Line": 5,
									"Col": 1
								},
								"Rbrace": {
									"Offset": 33,
									"Line": 5,
									"Col": 6
								},
								"Param": {
									"Pos": {
										"Offset": 30,
										"Line": 5,
										"Col": 3
									},
									"End": {
										"Offset": 33,
										"Line": 5,
										"Col": 6
									},
									"ValuePos": {
										"Offset": 30,
										"Line": 5,
										"Col": 3
									},
									"ValueEnd": {
										"Offset": 33,
										"Line": 5,
										"Col": 6
									},
									"Value": "foo"
								}
							}
						]
					},
					{
						"Pos": {
							"Offset": 35,
							"Line": 5,
							"Col": 8
						},
						"End": {
							"Offset": 41,
							"Line": 5,
							"Col": 14
						},
						"Parts": [
							{
								"Type": "CmdSubst",
								"Pos": {
									"Offset": 35,
									"Line": 5,
									"Col": 8
								},
								"End": {
									"Offset": 41,
									"Line": 5,
									"Col": 14
								},
								"Left": {
									"Offset": 35,
									"Line": 5,
									"Col": 8
								},
								"Right": {
									"Offset": 40,
									"Line": 5,
									"Col": 13
								},
								"Stmts": [
									{
										"Pos": {
											"Offset": 37,
											"Line": 5,
											"Col": 10
										},
										"End": {
											"Offset": 40,
											"Line": 5,
											"Col": 13
										},
										"Cmd": {
											"Type": "CallExpr",
											"Pos": {
												"Offset": 37,
												"Line": 5,
												"Col": 10
											},
											"End": {
												"Offset": 40,
												"Line": 5,
												"Col": 13
											},
											"Args": [
												{
													"Pos": {
														"Offset": 37,
														"Line": 5,
														"Col": 10
													},
													"End": {
														"Offset": 40,
														"Line": 5,
														"Col": 13
													},
													"Parts": [
														{
															"Type": "Lit",
															"Pos": {
																"Offset": 37,
																"Line": 5,
																"Col": 10
															},
															"End": {
																"Offset": 40,
																"Line": 5,
																"Col": 13
															},
															"ValuePos": {
																"Offset": 37,
																"Line": 5,
																"Col": 10
															},
															"ValueEnd": {
																"Offset": 40,
																"Line": 5,
																"Col": 13
															},
															"Value": "bar"
														}
													]
												}
											]
										},
										"Position": {
											"Offset": 37,
											"Line": 5,
											"Col": 10
										}
									}
								]
							}
						]
					},
					{
						"Pos": {
							"Offset": 42,
							"Line": 5,
							"Col": 15
						},
						"End": {
							"Offset": 50,
							"Line": 5,
							"Col": 23
						},
						"Parts": [
							{
								"Type": "ArithmExp",
								"Pos": {
									"Offset": 42,
									"Line": 5,
									"Col": 15
								},
								"End": {
									"Offset": 50,
									"Line": 5,
									"Col": 23
								},
								"Left": {
									"Offset": 42,
									"Line": 5,
									"Col": 15
								},
								"Right": {
									"Offset": 48,
									"Line": 5,
									"Col": 21
								},
								"X": {
									"Type": "Word",
									"Pos": {
										"Offset": 45,
										"Line": 5,
										"Col": 18
									},
									"End": {
										"Offset": 48,
										"Line": 5,
										"Col": 21
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 45,
												"Line": 5,
												"Col": 18
											},
											"End": {
												"Offset": 48,
												"Line": 5,
												"Col": 21
											},
											"ValuePos": {
												"Offset": 45,
												"Line": 5,
												"Col": 18
											},
											"ValueEnd": {
												"Offset": 48,
												"Line": 5,
												"Col": 21
											},
											"Value": "baz"
										}
									]
								}
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 28,
				"Line": 5,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 51,
				"Line": 6,
				"Col": 1
			},
			"End": {
				"Offset": 67,
				"Line": 6,
				"Col": 17
			},
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 51,
					"Line": 6,
					"Col": 1
				},
				"End": {
					"Offset": 67,
					"Line": 6,
					"Col": 17
				},
				"Args": [
					{
						"Pos": {
							"Offset": 51,
							"Line": 6,
							"Col": 1
						},
						"End": {
							"Offset": 57,
							"Line": 6,
							"Col": 7
						},
						"Parts": [
							{
								"Type": "ExtGlob",
								"Pos": {
									"Offset": 51,
									"Line": 6,
									"Col": 1
								},
								"End": {
									"Offset": 57,
									"Line": 6,
									"Col": 7
								},
								"OpPos": {
									"Offset": 51,
									"Line": 6,
									"Col": 1
								},
								"Op": 125,
								"Pattern": {
									"Pos": {
										"Offset": 53,
										"Line": 6,
										"Col": 3
									},
									"End": {
										"Offset": 56,
										"Line": 6,
										"Col": 6
									},
									"ValuePos": {
										"Offset": 53,
										"Line": 6,
										"Col": 3
									},
									"ValueEnd": {
										"Offset": 56,
										"Line": 6,
										"Col": 6
									},
									"Value": "foo"
								}
							}
						]
					},
					{
						"Pos": {
							"Offset": 58,
							"Line": 6,
							"Col": 8
						},
						"End": {
							"Offset": 67,
							"Line": 6,
							"Col": 17
						},
						"Parts": [
							{
								"Type": "Lit",
								"Pos": {
									"Offset": 58,
									"Line": 6,
									"Col": 8
								},
								"End": {
									"Offset": 67,
									"Line": 6,
									"Col": 17
								},
								"ValuePos": {
									"Offset": 58,
									"Line": 6,
									"Col": 8
								},
								"ValueEnd": {
									"Offset": 67,
									"Line": 6,
									"Col": 17
								},
								"Value": "{bar,baz}"
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 51,
				"Line": 6,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 69,
				"Line": 8,
				"Col": 1
			},
			"End": {
				"Offset": 86,
				"Line": 8,
				"Col": 18
			},
			"Cmd": {
				"Type": "BinaryCmd",
				"Pos": {
					"Offset": 69,
					"Line": 8,
					"Col": 1
				},
				"End": {
					"Offset": 86,
					"Line": 8,
					"Col": 18
				},
				"OpPos": {
					"Offset": 80,
					"Line": 8,
					"Col": 12
				},
				"Op": 11,
				"X": {
					"Pos": {
						"Offset": 69,
						"Line": 8,
						"Col": 1
					},
					"End": {
						"Offset": 79,
						"Line": 8,
						"Col": 11
					},
					"Cmd": {
						"Type": "BinaryCmd",
						"Pos": {
							"Offset": 69,
							"Line": 8,
							"Col": 1
						},
						"End": {
							"Offset": 79,
							"Line": 8,
							"Col": 11
						},
						"OpPos": {
							"Offset": 73,
							"Line": 8,
							"Col": 5
						},
						"Op": 10,
						"X": {
							"Pos": {
								"Offset": 69,
								"Line": 8,
								"Col": 1
							},
							"End": {
								"Offset": 72,
								"Line": 8,
								"Col": 4
							},
							"Cmd": {
								"Type": "CallExpr",
								"Pos": {
									"Offset": 69,
									"Line": 8,
									"Col": 1
								},
								"End": {
									"Offset": 72,
									"Line": 8,
									"Col": 4
								},
								"Args": [
									{
										"Pos": {
											"Offset": 69,
											"Line": 8,
											"Col": 1
										},
										"End": {
											"Offset": 72,
											"Line": 8,
											"Col": 4
										},
										"Parts": [
											{
												"Type": "Lit",
												"Pos": {
													"Offset": 69,
													"Line": 8,
													"Col": 1
												},
												"End": {
													"Offset": 72,
													"Line": 8,
													"Col": 4
												},
												"ValuePos": {
													"Offset": 69,
													"Line": 8,
													"Col": 1
												},
												"ValueEnd": {
													"Offset": 72,
													"Line": 8,
													"Col": 4
												},
												"Value": "foo"
											}
										]
									}
								]
							},
							"Position": {
								"Offset": 69,
								"Line": 8,
								"Col": 1
							}
						},
						"Y": {
							"Pos": {
								"Offset": 76,
								"Line": 8,
								"Col": 8
							},
							"End": {
								"Offset": 79,
								"Line": 8,
								"Col": 11
							},
							"Cmd": {
								"Type": "CallExpr",
								"Pos": {
									"Offset": 76,
									"Line": 8,
									"Col": 8
								},
								"End": {
									"Offset": 79,
									"Line": 8,
									"Col": 11
								},
								"Args": [
									{
										"Pos": {
											"Offset": 76,
											"Line": 8,
											"Col": 8
										},
										"End": {
											"Offset": 79,
											"Line": 8,
											"Col": 11
										},
										"Parts": [
											{
												"Type": "Lit",
												"Pos": {
													"Offset": 76,
													"Line": 8,
													"Col": 8
												},
												"End": {
													"Offset": 79,
													"Line": 8,
													"Col": 11
												},
												"ValuePos": {
													"Offset": 76,
													"Line": 8,
													"Col": 8
												},
												"ValueEnd": {
													"Offset": 79,
													"Line": 8,
													"Col": 11
												},
												"Value": "bar"
											}
										]
									}
								]
							},
							"Position": {
								"Offset": 76,
								"Line": 8,
								"Col": 8
							}
						}
					},
					"Position": {
						"Offset": 69,
						"Line": 8,
						"Col": 1
					}
				},
				"Y": {
					"Pos": {
						"Offset": 83,
						"Line": 8,
						"Col": 15
					},
					"End": {
						"Offset": 86,
						"Line": 8,
						"Col": 18
					},
					"Cmd": {
						"Type": "CallExpr",
						"Pos": {
							"Offset": 83,
							"Line": 8,
							"Col": 15
						},
						"End": {
							"Offset": 86,
							"Line": 8,
							"Col": 18
						},
						"Args": [
							{
								"Pos": {
									"Offset": 83,
									"Line": 8,
									"Col": 15
								},
								"End": {
									"Offset": 86,
									"Line": 8,
									"Col": 18
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 83,
											"Line": 8,
											"Col": 15
										},
										"End": {
											"Offset": 86,
											"Line": 8,
											"Col": 18
										},
										"ValuePos": {
											"Offset": 83,
											"Line": 8,
											"Col": 15
										},
										"ValueEnd": {
											"Offset": 86,
											"Line": 8,
											"Col": 18
										},
										"Value": "baz"
									}
								]
							}
						]
					},
					"Position": {
						"Offset": 83,
						"Line": 8,
						"Col": 15
					}
				}
			},
			"Position": {
				"Offset": 69,
				"Line": 8,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 87,
				"Line": 9,
				"Col": 1
			},
			"End": {
				"Offset": 103,
				"Line": 9,
				"Col": 17
			},
			"Cmd": {
				"Type": "BinaryCmd",
				"Pos": {
					"Offset": 87,
					"Line": 9,
					"Col": 1
				},
				"End": {
					"Offset": 103,
					"Line": 9,
					"Col": 17
				},
				"OpPos": {
					"Offset": 97,
					"Line": 9,
					"Col": 11
				},
				"Op": 13,
				"X": {
					"Pos": {
						"Offset": 87,
						"Line": 9,
						"Col": 1
					},
					"End": {
						"Offset": 96,
						"Line": 9,
						"Col": 10
					},
					"Cmd": {
						"Type": "BinaryCmd",
						"Pos": {
							"Offset": 87,
							"Line": 9,
							"Col": 1
						},
						"End": {
							"Offset": 96,
							"Line": 9,
							"Col": 10
						},
						"OpPos": {
							"Offset": 91,
							"Line": 9,
							"Col": 5
						},
						"Op": 12,
						"X": {
							"Pos": {
								"Offset": 87,
								"Line": 9,
								"Col": 1
							},
							"End": {
								"Offset": 90,
								"Line": 9,
								"Col": 4
							},
							"Cmd": {
								"Type": "CallExpr",
								"Pos": {
									"Offset": 87,
									"Line": 9,
									"Col": 1
								},
								"End": {
									"Offset": 90,
									"Line": 9,
									"Col": 4
								},
								"Args": [
									{
										"Pos": {
											"Offset": 87,
											"Line": 9,
											"Col": 1
										},
										"End": {
											"Offset": 90,
											"Line": 9,
											"Col": 4
										},
										"Parts": [
											{
												"Type": "Lit",
												"Pos": {
													"Offset": 87,
													"Line": 9,
													"Col": 1
												},
												"End": {
													"Offset": 90,
													"Line": 9,
													"Col": 4
												},
												"ValuePos": {
													"Offset": 87,
													"Line": 9,
													"Col": 1
												},
												"ValueEnd": {
													"Offset": 90,
													"Line": 9,
													"Col": 4
												},
												"Value": "foo"
											}
										]
									}
								]
							},
							"Position": {
								"Offset": 87,
								"Line": 9,
								"Col": 1
							}
						},
						"Y": {
							"Pos": {
								"Offset": 93,
								"Line": 9,
								"Col": 7
							},
							"End": {
								"Offset": 96,
								"Line": 9,
								"Col": 10
							},
							"Cmd": {
								"Type": "CallExpr",
								"Pos": {
									"Offset": 93,
									"Line": 9,
									"Col": 7
								},
								"End": {
									"Offset": 96,
									"Line": 9,
									"Col": 10
								},
								"Args": [
									{
										"Pos": {
											"Offset": 93,
											"Line": 9,
											"Col": 7
										},
										"End": {
											"Offset": 96,
											"Line": 9,
											"Col": 10
										},
										"Parts": [
											{
												"Type": "Lit",
												"Pos": {
													"Offset": 93,
													"Line": 9,
													"Col": 7
												},
												"End": {
													"Offset": 96,
													"Line": 9,
													"Col": 10
												},
												"ValuePos": {
													"Offset": 93,
													"Line": 9,
													"Col": 7
												},
												"ValueEnd": {
													"Offset": 96,
													"Line": 9,
													"Col": 10
												},
												"Value": "bar"
											}
										]
									}
								]
							},
							"Position": {
								"Offset": 93,
								"Line": 9,
								"Col": 7
							}
						}
					},
					"Position": {
						"Offset": 87,
						"Line": 9,
						"Col": 1
					}
				},
				"Y": {
					"Pos": {
						"Offset": 100,
						"Line": 9,
						"Col": 14
					},
					"End": {
						"Offset": 103,
						"Line": 9,
						"Col": 17
					},
					"Cmd": {
						"Type": "CallExpr",
						"Pos": {
							"Offset": 100,
							"Line": 9,
							"Col": 14
						},
						"End": {
							"Offset": 103,
							"Line": 9,
							"Col": 17
						},
						"Args": [
							{
								"Pos": {
									"Offset": 100,
									"Line": 9,
									"Col": 14
								},
								"End": {
									"Offset": 103,
									"Line": 9,
									"Col": 17
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 100,
											"Line": 9,
											"Col": 14
										},
										"End": {
											"Offset": 103,
											"Line": 9,
											"Col": 17
										},
										"ValuePos": {
											"Offset": 100,
											"Line": 9,
											"Col": 14
										},
										"ValueEnd": {
											"Offset": 103,
											"Line": 9,
											"Col": 17
										},
										"Value": "baz"
									}
								]
							}
						]
					},
					"Position": {
						"Offset": 100,
						"Line": 9,
						"Col": 14
					}
				}
			},
			"Position": {
				"Offset": 87,
				"Line": 9,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 105,
				"Line": 11,
				"Col": 1
			},
			"End": {
				"Offset": 125,
				"Line": 11,
				"Col": 21
			},
			"Cmd": {
				"Type": "IfClause",
				"Pos": {
					"Offset": 105,
					"Line": 11,
					"Col": 1
				},
				"End": {
					"Offset": 125,
					"Line": 11,
					"Col": 21
				},
				"Position": {
					"Offset": 105,
					"Line": 11,
					"Col": 1
				},
				"ThenPos": {
					"Offset": 113,
					"Line": 11,
					"Col": 9
				},
				"FiPos": {
					"Offset": 123,
					"Line": 11,
					"Col": 19
				},
				"Cond": [
					{
						"Pos": {
							"Offset": 108,
							"Line": 11,
							"Col": 4
						},
						"End": {
							"Offset": 112,
							"Line": 11,
							"Col": 8
						},
						"Cmd": {
							"Type": "CallExpr",
							"Pos": {
								"Offset": 108,
								"Line": 11,
								"Col": 4
							},
							"End": {
								"Offset": 111,
								"Line": 11,
								"Col": 7
							},
							"Args": [
								{
									"Pos": {
										"Offset": 108,
										"Line": 11,
										"Col": 4
									},
									"End": {
										"Offset": 111,
										"Line": 11,
										"Col": 7
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 108,
												"Line": 11,
												"Col": 4
											},
											"End": {
												"Offset": 111,
												"Line": 11,
												"Col": 7
											},
											"ValuePos": {
												"Offset": 108,
												"Line": 11,
												"Col": 4
											},
											"ValueEnd": {
												"Offset": 111,
												"Line": 11,
												"Col": 7
											},
											"Value": "foo"
										}
									]
								}
							]
						},
						"Position": {
							"Offset": 108,
							"Line": 11,
							"Col": 4
						},
						"Semicolon": {
							"Offset": 111,
							"Line": 11,
							"Col": 7
						}
					}
				],
				"Then": [
					{
						"Pos": {
							"Offset": 118,
							"Line": 11,
							"Col": 14
						},
						"End": {
							"Offset": 122,
							"Line": 11,
							"Col": 18
						},
						"Cmd": {
							"Type": "CallExpr",
							"Pos": {
								"Offset": 118,
								"Line": 11,
								"Col": 14
							},
							"End": {
								"Offset": 121,
								"Line": 11,
								"Col": 17
							},
							"Args": [
								{
									"Pos": {
										"Offset": 118,
										"Line": 11,
										"Col": 14
									},
									"End": {
										"Offset": 121,
										"Line": 11,
										"Col": 17
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 118,
												"Line": 11,
												"Col": 14
											},
											"End": {
												"Offset": 121,
												"Line": 11,
												"Col": 17
											},
											"ValuePos": {
												"Offset": 118,
												"Line": 11,
												"Col": 14
											},
											"ValueEnd": {
												"Offset": 121,
												"Line": 11,
												"Col": 17
											},
											"Value": "bar"
										}
									]
								}
							]
						},
						"Position": {
							"Offset": 118,
							"Line": 11,
							"Col": 14
						},
						"Semicolon": {
							"Offset": 121,
							"Line": 11,
							"Col": 17
						}
					}
				]
			},
			"Position": {
				"Offset": 105,
				"Line": 11,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 126,
				"Line": 12,
				"Col": 1
			},
			"End": {
				"Offset": 154,
				"Line": 12,
				"Col": 29
			},
			"Cmd": {
				"Type": "ForClause",
				"Pos": {
					"Offset": 126,
					"Line": 12,
					"Col": 1
				},
				"End": {
					"Offset": 154,
					"Line": 12,
					"Col": 29
				},
				"ForPos": {
					"Offset": 126,
					"Line": 12,
					"Col": 1
				},
				"DoPos": {
					"Offset": 142,
					"Line": 12,
					"Col": 17
				},
				"DonePos": {
					"Offset": 150,
					"Line": 12,
					"Col": 25
				},
				"Loop": {
					"Type": "WordIter",
					"Pos": {
						"Offset": 130,
						"Line": 12,
						"Col": 5
					},
					"End": {
						"Offset": 140,
						"Line": 12,
						"Col": 15
					},
					"Name": {
						"Pos": {
							"Offset": 130,
							"Line": 12,
							"Col": 5
						},
						"End": {
							"Offset": 131,
							"Line": 12,
							"Col": 6
						},
						"ValuePos": {
							"Offset": 130,
							"Line": 12,
							"Col": 5
						},
						"ValueEnd": {
							"Offset": 131,
							"Line": 12,
							"Col": 6
						},
						"Value": "i"
					},
					"InPos": {
						"Offset": 132,
						"Line": 12,
						"Col": 7
					},
					"Items": [
						{
							"Pos": {
								"Offset": 135,
								"Line": 12,
								"Col": 10
							},
							"End": {
								"Offset": 136,
								"Line": 12,
								"Col": 11
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 135,
										"Line": 12,
										"Col": 10
									},
									"End": {
										"Offset": 136,
										"Line": 12,
										"Col": 11
									},
									"ValuePos": {
										"Offset": 135,
										"Line": 12,
										"Col": 10
									},
									"ValueEnd": {
										"Offset": 136,
										"Line": 12,
										"Col": 11
									},
									"Value": "1"
								}
							]
						},
						{
							"Pos": {
								"Offset": 137,
								"Line": 12,
								"Col": 12
							},
							"End": {
								"Offset": 138,
								"Line": 12,
								"Col": 13
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 137,
										"Line": 12,
										"Col": 12
									},
									"End": {
										"Offset": 138,
										"Line": 12,
										"Col": 13
									},
									"ValuePos": {
										"Offset": 137,
										"Line": 12,
										"Col": 12
									},
									"ValueEnd": {
										"Offset": 138,
										"Line": 12,
										"Col": 13
									},
									"Value": "2"
								}
							]
						},
						{
							"Pos": {
								"Offset": 139,
								"Line": 12,
								"Col": 14
							},
							"End": {
								"Offset": 140,
								"Line": 12,
								"Col": 15
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 139,
										"Line": 12,
										"Col": 14
									},
									"End": {
										"Offset": 140,
										"Line": 12,
										"Col": 15
									},
									"ValuePos": {
										"Offset": 139,
										"Line": 12,
										"Col": 14
									},
									"ValueEnd": {
										"Offset": 140,
										"Line": 12,
										"Col": 15
									},
									"Value": "3"
								}
							]
						}
					]
				},
				"Do": [
					{
						"Pos": {
							"Offset": 145,
							"Line": 12,
							"Col": 20
						},
						"End": {
							"Offset": 149,
							"Line": 12,
							"Col": 24
						},
						"Cmd": {
							"Type": "CallExpr",
							"Pos": {
								"Offset": 145,
								"Line": 12,
								"Col": 20
							},
							"End": {
								"Offset": 148,
								"Line": 12,
								"Col": 23
							},
							"Args": [
								{
									"Pos": {
										"Offset": 145,
										"Line": 12,
										"Col": 20
									},
									"End": {
										"Offset": 148,
										"Line": 12,
										"Col": 23
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 145,
												"Line": 12,
												"Col": 20
											},
											"End": {
												"Offset": 148,
												"Line": 12,
												"Col": 23
											},
											"ValuePos": {
												"Offset": 145,
												"Line": 12,
												"Col": 20
											},
											"ValueEnd": {
												"Offset": 148,
												"Line": 12,
												"Col": 23
											},
											"Value": "bar"
										}
									]
								}
							]
						},
						"Position": {
							"Offset": 145,
							"Line": 12,
							"Col": 20
						},
						"Semicolon": {
							"Offset": 148,
							"Line": 12,
							"Col": 23
						}
					}
				]
			},
			"Position": {
				"Offset": 126,
				"Line": 12,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 155,
				"Line": 13,
				"Col": 1
			},
			"End": {
				"Offset": 194,
				"Line": 13,
				"Col": 40
			},
			"Cmd": {
				"Type": "ForClause",
				"Pos": {
					"Offset": 155,
					"Line": 13,
					"Col": 1
				},
				"End": {
					"Offset": 194,
					"Line": 13,
					"Col": 40
				},
				"ForPos": {
					"Offset": 155,
					"Line": 13,
					"Col": 1
				},
				"DoPos": {
					"Offset": 182,
					"Line": 13,
					"Col": 28
				},
				"DonePos": {
					"Offset": 190,
					"Line": 13,
					"Col": 36
				},
				"Loop": {
					"Type": "CStyleLoop",
					"Pos": {
						"Offset": 159,
						"Line": 13,
						"Col": 5
					},
					"End": {
						"Offset": 180,
						"Line": 13,
						"Col": 26
					},
					"Lparen": {
						"Offset": 159,
						"Line": 13,
						"Col": 5
					},
					"Rparen": {
						"Offset": 178,
						"Line": 13,
						"Col": 24
					},
					"Init": {
						"Type": "BinaryArithm",
						"Pos": {
							"Offset": 161,
							"Line": 13,
							"Col": 7
						},
						"End": {
							"Offset": 166,
							"Line": 13,
							"Col": 12
						},
						"OpPos": {
							"Offset": 163,
							"Line": 13,
							"Col": 9
						},
						"Op": 74,
						"X": {
							"Type": "Word",
							"Pos": {
								"Offset": 161,
								"Line": 13,
								"Col": 7
							},
							"End": {
								"Offset": 162,
								"Line": 13,
								"Col": 8
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 161,
										"Line": 13,
										"Col": 7
									},
									"End": {
										"Offset": 162,
										"Line": 13,
										"Col": 8
									},
									"ValuePos": {
										"Offset": 161,
										"Line": 13,
										"Col": 7
									},
									"ValueEnd": {
										"Offset": 162,
										"Line": 13,
										"Col": 8
									},
									"Value": "i"
								}
							]
						},
						"Y": {
							"Type": "Word",
							"Pos": {
								"Offset": 165,
								"Line": 13,
								"Col": 11
							},
							"End": {
								"Offset": 166,
								"Line": 13,
								"Col": 12
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 165,
										"Line": 13,
										"Col": 11
									},
									"End": {
										"Offset": 166,
										"Line": 13,
										"Col": 12
									},
									"ValuePos": {
										"Offset": 165,
										"Line": 13,
										"Col": 11
									},
									"ValueEnd": {
										"Offset": 166,
										"Line": 13,
										"Col": 12
									},
									"Value": "0"
								}
							]
						}
					},
					"Cond": {
						"Type": "BinaryArithm",
						"Pos": {
							"Offset": 168,
							"Line": 13,
							"Col": 14
						},
						"End": {
							"Offset": 173,
							"Line": 13,
							"Col": 19
						},
						"OpPos": {
							"Offset": 170,
							"Line": 13,
							"Col": 16
						},
						"Op": 56,
						"X": {
							"Type": "Word",
							"Pos": {
								"Offset": 168,
								"Line": 13,
								"Col": 14
							},
							"End": {
								"Offset": 169,
								"Line": 13,
								"Col": 15
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 168,
										"Line": 13,
										"Col": 14
									},
									"End": {
										"Offset": 169,
										"Line": 13,
										"Col": 15
									},
									"ValuePos": {
										"Offset": 168,
										"Line": 13,
										"Col": 14
									},
									"ValueEnd": {
										"Offset": 169,
										"Line": 13,
										"Col": 15
									},
									"Value": "i"
								}
							]
						},
						"Y": {
							"Type": "Word",
							"Pos": {
								"Offset": 172,
								"Line": 13,
								"Col": 18
							},
							"End": {
								"Offset": 173,
								"Line": 13,
								"Col": 19
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 172,
										"Line": 13,
										"Col": 18
									},
									"End": {
										"Offset": 173,
										"Line": 13,
										"Col": 19
									},
									"ValuePos": {
										"Offset": 172,
										"Line": 13,
										"Col": 18
									},
									"ValueEnd": {
										"Offset": 173,
										"Line": 13,
										"Col": 19
									},
									"Value": "3"
								}
							]
						}
					},
					"Post": {
						"Type": "UnaryArithm",
						"Pos": {
							"Offset": 175,
							"Line": 13,
							"Col": 21
						},
						"End": {
							"Offset": 178,
							"Line": 13,
							"Col": 24
						},
						"OpPos": {
							"Offset": 176,
							"Line": 13,
							"Col": 22
						},
						"Op": 36,
						"Post": true,
						"X": {
							"Type": "Word",
							"Pos": {
								"Offset": 175,
								"Line": 13,
								"Col": 21
							},
							"End": {
								"Offset": 176,
								"Line": 13,
								"Col": 22
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 175,
										"Line": 13,
										"Col": 21
									},
									"End": {
										"Offset": 176,
										"Line": 13,
										"Col": 22
									},
									"ValuePos": {
										"Offset": 175,
										"Line": 13,
										"Col": 21
									},
									"ValueEnd": {
										"Offset": 176,
										"Line": 13,
										"Col": 22
									},
									"Value": "i"
								}
							]
						}
					}
				},
				"Do": [
					{
						"Pos": {
							"Offset": 185,
							"Line": 13,
							"Col": 31
						},
						"End": {
							"Offset": 189,
							"Line": 13,
							"Col": 35
						},
						"Cmd": {
							"Type": "CallExpr",
							"Pos": {
								"Offset": 185,
								"Line": 13,
								"Col": 31
							},
							"End": {
								"Offset": 188,
								"Line": 13,
								"Col": 34
							},
							"Args": [
								{
									"Pos": {
										"Offset": 185,
										"Line": 13,
										"Col": 31
									},
									"End": {
										"Offset": 188,
										"Line": 13,
										"Col": 34
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 185,
												"Line": 13,
												"Col": 31
											},
											"End": {
												"Offset": 188,
												"Line": 13,
												"Col": 34
											},
											"ValuePos": {
												"Offset": 185,
												"Line": 13,
												"Col": 31
											},
											"ValueEnd": {
												"Offset": 188,
												"Line": 13,
												"Col": 34
											},
											"Value": "bar"
										}
									]
								}
							]
						},
						"Position": {
							"Offset": 185,
							"Line": 13,
							"Col": 31
						},
						"Semicolon": {
							"Offset": 188,
							"Line": 13,
							"Col": 34
						}
					}
				]
			},
			"Position": {
				"Offset": 155,
				"Line": 13,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 195,
				"Line": 14,
				"Col": 1
			},
			"End": {
				"Offset": 218,
				"Line": 14,
				"Col": 24
			},
			"Cmd": {
				"Type": "WhileClause",
				"Pos": {
					"Offset": 195,
					"Line": 14,
					"Col": 1
				},
				"End": {
					"Offset": 218,
					"Line": 14,
					"Col": 24
				},
				"WhilePos": {
					"Offset": 195,
					"Line": 14,
					"Col": 1
				},
				"DoPos": {
					"Offset": 206,
					"Line": 14,
					"Col": 12
				},
				"DonePos": {
					"Offset": 214,
					"Line": 14,
					"Col": 20
				},
				"Cond": [
					{
						"Pos": {
							"Offset": 201,
							"Line": 14,
							"Col": 7
						},
						"End": {
							"Offset": 205,
							"Line": 14,
							"Col": 11
						},
						"Cmd": {
							"Type": "CallExpr",
							"Pos": {
								"Offset": 201,
								"Line": 14,
								"Col": 7
							},
							"End": {
								"Offset": 204,
								"Line": 14,
								"Col": 10
							},
							"Args": [
								{
									"Pos": {
										"Offset": 201,
										"Line": 14,
										"Col": 7
									},
									"End": {
										"Offset": 204,
										"Line": 14,
										"Col": 10
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 201,
												"Line": 14,
												"Col": 7
											},
											"End": {
												"Offset": 204,
												"Line": 14,
												"Col": 10
											},
											"ValuePos": {
												"Offset": 201,
												"Line": 14,
												"Col": 7
											},
											"ValueEnd": {
												"Offset": 204,
												"Line": 14,
												"Col": 10
											},
											"Value": "foo"
										}
									]
								}
							]
						},
						"Position": {
							"Offset": 201,
							"Line": 14,
							"Col": 7
						},
						"Semicolon": {
							"Offset": 204,
							"Line": 14,
							"Col": 10
						}
					}
				],
				"Do": [
					{
						"Pos": {
							"Offset": 209,
							"Line": 14,
							"Col": 15
						},
						"End": {
							"Offset": 213,
							"Line": 14,
							"Col": 19
						},
						"Cmd": {
							"Type": "CallExpr",
							"Pos": {
								"Offset": 209,
								"Line": 14,
								"Col": 15
							},
							"End": {
								"Offset": 212,
								"Line": 14,
								"Col": 18
							},
							"Args": [
								{
									"Pos": {
										"Offset": 209,
										"Line": 14,
										"Col": 15
									},
									"End": {
										"Offset": 212,
										"Line": 14,
										"Col": 18
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 209,
												"Line": 14,
												"Col": 15
											},
											"End": {
												"Offset": 212,
												"Line": 14,
												"Col": 18
											},
											"ValuePos": {
												"Offset": 209,
												"Line": 14,
												"Col": 15
											},
											"ValueEnd": {
												"Offset": 212,
												"Line": 14,
												"Col": 18
											},
											"Value": "bar"
										}
									]
								}
							]
						},
						"Position": {
							"Offset": 209,
							"Line": 14,
							"Col": 15
						},
						"Semicolon": {
							"Offset": 212,
							"Line": 14,
							"Col": 18
						}
					}
				]
			},
			"Position": {
				"Offset": 195,
				"Line": 14,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 219,
				"Line": 15,
				"Col": 1
			},
			"End": {
				"Offset": 245,
				"Line": 15,
				"Col": 27
			},
			"Cmd": {
				"Type": "CaseClause",
				"Pos": {
					"Offset": 219,
					"Line": 15,
					"Col": 1
				},
				"End": {
					"Offset": 245,
					"Line": 15,
					"Col": 27
				},
				"Case": {
					"Offset": 219,
					"Line": 15,
					"Col": 1
				},
				"In": {
					"Offset": 226,
					"Line": 15,
					"Col": 8
				},
				"Esac": {
					"Offset": 241,
					"Line": 15,
					"Col": 23
				},
				"Word": {
					"Pos": {
						"Offset": 224,
						"Line": 15,
						"Col": 6
					},
					"End": {
						"Offset": 225,
						"Line": 15,
						"Col": 7
					},
					"Parts": [
						{
							"Type": "Lit",
							"Pos": {
								"Offset": 224,
								"Line": 15,
								"Col": 6
							},
							"End": {
								"Offset": 225,
								"Line": 15,
								"Col": 7
							},
							"ValuePos": {
								"Offset": 224,
								"Line": 15,
								"Col": 6
							},
							"ValueEnd": {
								"Offset": 225,
								"Line": 15,
								"Col": 7
							},
							"Value": "i"
						}
					]
				},
				"Items": [
					{
						"Pos": {
							"Offset": 229,
							"Line": 15,
							"Col": 11
						},
						"End": {
							"Offset": 240,
							"Line": 15,
							"Col": 22
						},
						"Op": 30,
						"OpPos": {
							"Offset": 238,
							"Line": 15,
							"Col": 20
						},
						"Patterns": [
							{
								"Pos": {
									"Offset": 229,
									"Line": 15,
									"Col": 11
								},
								"End": {
									"Offset": 232,
									"Line": 15,
									"Col": 14
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 229,
											"Line": 15,
											"Col": 11
										},
										"End": {
											"Offset": 232,
											"Line": 15,
											"Col": 14
										},
										"ValuePos": {
											"Offset": 229,
											"Line": 15,
											"Col": 11
										},
										"ValueEnd": {
											"Offset": 232,
											"Line": 15,
											"Col": 14
										},
										"Value": "foo"
									}
								]
							}
						],
						"Stmts": [
							{
								"Pos": {
									"Offset": 234,
									"Line": 15,
									"Col": 16
								},
								"End": {
									"Offset": 237,
									"Line": 15,
									"Col": 19
								},
								"Cmd": {
									"Type": "CallExpr",
									"Pos": {
										"Offset": 234,
										"Line": 15,
										"Col": 16
									},
									"End": {
										"Offset": 237,
										"Line": 15,
										"Col": 19
									},
									"Args": [
										{
											"Pos": {
												"Offset": 234,
												"Line": 15,
												"Col": 16
											},
											"End": {
												"Offset": 237,
												"Line": 15,
												"Col": 19
											},
											"Parts": [
												{
													"Type": "Lit",
													"Pos": {
														"Offset": 234,
														"Line": 15,
														"Col": 16
													},
													"End": {
														"Offset": 237,
														"Line": 15,
														"Col": 19
													},
													"ValuePos": {
														"Offset": 234,
														"Line": 15,
														"Col": 16
													},
													"ValueEnd": {
														"Offset": 237,
														"Line": 15,
														"Col": 19
													},
													"Value": "bar"
												}
											]
										}
									]
								},
								"Position": {
									"Offset": 234,
									"Line": 15,
									"Col": 16
								}
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 219,
				"Line": 15,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 247,
				"Line": 17,
				"Col": 1
			},
			"End": {
				"Offset": 255,
				"Line": 17,
				"Col": 9
			},
			"Cmd": {
				"Type": "Block",
				"Pos": {
					"Offset": 247,
					"Line": 17,
					"Col": 1
				},
				"End": {
					"Offset": 255,
					"Line": 17,
					"Col": 9
				},
				"Lbrace": {
					"Offset": 247,
					"Line": 17,
					"Col": 1
				},
				"Rbrace": {
					"Offset": 254,
					"Line": 17,
					"Col": 8
				},
				"Stmts": [
					{
						"Pos": {
							"Offset": 249,
							"Line": 17,
							"Col": 3
						},
						"End": {
							"Offset": 253,
							"Line": 17,
							"Col": 7
						},
						"Cmd": {
							"Type": "CallExpr",
							"Pos": {
								"Offset": 249,
								"Line": 17,
								"Col": 3
							},
							"End": {
								"Offset": 252,
								"Line": 17,
								"Col": 6
							},
							"Args": [
								{
									"Pos": {
										"Offset": 249,
										"Line": 17,
										"Col": 3
									},
									"End": {
										"Offset": 252,
										"Line": 17,
										"Col": 6
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 249,
												"Line": 17,
												"Col": 3
											},
											"End": {
												"Offset": 252,
												"Line": 17,
												"Col": 6
											},
											"ValuePos": {
												"Offset": 249,
												"Line": 17,
												"Col": 3
											},
											"ValueEnd": {
												"Offset": 252,
												"Line": 17,
												"Col": 6
											},
											"Value": "foo"
										}
									]
								}
							]
						},
						"Position": {
							"Offset": 249,
							"Line": 17,
							"Col": 3
						},
						"Semicolon": {
							"Offset": 252,
							"Line": 17,
							"Col": 6
						}
					}
				]
			},
			"Position": {
				"Offset": 247,
				"Line": 17,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 256,
				"Line": 18,
				"Col": 1
			},
			"End": {
				"Offset": 261,
				"Line": 18,
				"Col": 6
			},
			"Cmd": {
				"Type": "Subshell",
				"Pos": {
					"Offset": 256,
					"Line": 18,
					"Col": 1
				},
				"End": {
					"Offset": 261,
					"Line": 18,
					"Col": 6
				},
				"Lparen": {
					"Offset": 256,
					"Line": 18,
					"Col": 1
				},
				"Rparen": {
					"Offset": 260,
					"Line": 18,
					"Col": 5
				},
				"Stmts": [
					{
						"Pos": {
							"Offset": 257,
							"Line": 18,
							"Col": 2
						},
						"End": {
							"Offset": 260,
							"Line": 18,
							"Col": 5
						},
						"Cmd": {
							"Type": "CallExpr",
							"Pos": {
								"Offset": 257,
								"Line": 18,
								"Col": 2
							},
							"End": {
								"Offset": 260,
								"Line": 18,
								"Col": 5
							},
							"Args": [
								{
									"Pos": {
										"Offset": 257,
										"Line": 18,
										"Col": 2
									},
									"End": {
										"Offset": 260,
										"Line": 18,
										"Col": 5
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 257,
												"Line": 18,
												"Col": 2
											},
											"End": {
												"Offset": 260,
												"Line": 18,
												"Col": 5
											},
											"ValuePos": {
												"Offset": 257,
												"Line": 18,
												"Col": 2
											},
											"ValueEnd": {
												"Offset": 260,
												"Line": 18,
												"Col": 5
											},
											"Value": "foo"
										}
									]
								}
							]
						},
						"Position": {
							"Offset": 257,
							"Line": 18,
							"Col": 2
						}
					}
				]
			},
			"Position": {
				"Offset": 256,
				"Line": 18,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 262,
				"Line": 19,
				"Col": 1
			},
			"End": {
				"Offset": 276,
				"Line": 19,
				"Col": 15
			},
			"Cmd": {
				"Type": "FuncDecl",
				"Pos": {
					"Offset": 262,
					"Line": 19,
					"Col": 1
				},
				"End": {
					"Offset": 276,
					"Line": 19,
					"Col": 15
				},
				"Position": {
					"Offset": 262,
					"Line": 19,
					"Col": 1
				},
				"Parens": true,
				"Name": {
					"Pos": {
						"Offset": 262,
						"Line": 19,
						"Col": 1
					},
					"End": {
						"Offset": 265,
						"Line": 19,
						"Col": 4
					},
					"ValuePos": {
						"Offset": 262,
						"Line": 19,
						"Col": 1
					},
					"ValueEnd": {
						"Offset": 265,
						"Line": 19,
						"Col": 4
					},
					"Value": "foo"
				},
				"Body": {
					"Pos": {
						"Offset": 268,
						"Line": 19,
						"Col": 7
					},
					"End": {
						"Offset": 276,
						"Line": 19,
						"Col": 15
					},
					"Cmd": {
						"Type": "Block",
						"Pos": {
							"Offset": 268,
							"Line": 19,
							"Col": 7
						},
						"End": {
							"Offset": 276,
							"Line": 19,
							"Col": 15
						},
						"Lbrace": {
							"Offset": 268,
							"Line": 19,
							"Col": 7
						},
						"Rbrace": {
							"Offset": 275,
							"Line": 19,
							"Col": 14
						},
						"Stmts": [
							{
								"Pos": {
									"Offset": 270,
									"Line": 19,
									"Col": 9
								},
								"End": {
									"Offset": 274,
									"Line": 19,
									"Col": 13
								},
								"Cmd": {
									"Type": "CallExpr",
									"Pos": {
										"Offset": 270,
										"Line": 19,
										"Col": 9
									},
									"End": {
										"Offset": 273,
										"Line": 19,
										"Col": 12
									},
									"Args": [
										{
											"Pos": {
												"Offset": 270,
												"Line": 19,
												"Col": 9
											},
											"End": {
												"Offset": 273,
												"Line": 19,
												"Col": 12
											},
											"Parts": [
												{
													"Type": "Lit",
													"Pos": {
														"Offset": 270,
														"Line": 19,
														"Col": 9
													},
													"End": {
														"Offset": 273,
														"Line": 19,
														"Col": 12
													},
													"ValuePos": {
														"Offset": 270,
														"Line": 19,
														"Col": 9
													},
													"ValueEnd": {
														"Offset": 273,
														"Line": 19,
														"Col": 12
													},
													"Value": "bar"
												}
											]
										}
									]
								},
								"Position": {
									"Offset": 270,
									"Line": 19,
									"Col": 9
								},
								"Semicolon": {
									"Offset": 273,
									"Line": 19,
									"Col": 12
								}
							}
						]
					},
					"Position": {
						"Offset": 268,
						"Line": 19,
						"Col": 7
					}
				}
			},
			"Position": {
				"Offset": 262,
				"Line": 19,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 277,
				"Line": 20,
				"Col": 1
			},
			"End": {
				"Offset": 288,
				"Line": 20,
				"Col": 12
			},
			"Cmd": {
				"Type": "DeclClause",
				"Pos": {
					"Offset": 277,
					"Line": 20,
					"Col": 1
				},
				"End": {
					"Offset": 288,
					"Line": 20,
					"Col": 12
				},
				"Variant": {
					"Pos": {
						"Offset": 277,
						"Line": 20,
						"Col": 1
					},
					"End": {
						"Offset": 284,
						"Line": 20,
						"Col": 8
					},
					"ValuePos": {
						"Offset": 277,
						"Line": 20,
						"Col": 1
					},
					"ValueEnd": {
						"Offset": 284,
						"Line": 20,
						"Col": 8
					},
					"Value": "declare"
				},
				"Args": [
					{
						"Pos": {
							"Offset": 285,
							"Line": 20,
							"Col": 9
						},
						"End": {
							"Offset": 288,
							"Line": 20,
							"Col": 12
						},
						"Naked": true,
						"Name": {
							"Pos": {
								"Offset": 285,
								"Line": 20,
								"Col": 9
							},
							"End": {
								"Offset": 288,
								"Line": 20,
								"Col": 12
							},
							"ValuePos": {
								"Offset": 285,
								"Line": 20,
								"Col": 9
							},
							"ValueEnd": {
								"Offset": 288,
								"Line": 20,
								"Col": 12
							},
							"Value": "foo"
						}
					}
				]
			},
			"Position": {
				"Offset": 277,
				"Line": 20,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 289,
				"Line": 21,
				"Col": 1
			},
			"End": {
				"Offset": 304,
				"Line": 21,
				"Col": 16
			},
			"Cmd": {
				"Type": "LetClause",
				"Pos": {
					"Offset": 289,
					"Line": 21,
					"Col": 1
				},
				"End": {
					"Offset": 304,
					"Line": 21,
					"Col": 16
				},
				"Let": {
					"Offset": 289,
					"Line": 21,
					"Col": 1
				},
				"Exprs": [
					{
						"Type": "BinaryArithm",
						"Pos": {
							"Offset": 293,
							"Line": 21,
							"Col": 5
						},
						"End": {
							"Offset": 304,
							"Line": 21,
							"Col": 16
						},
						"OpPos": {
							"Offset": 296,
							"Line": 21,
							"Col": 8
						},
						"Op": 74,
						"X": {
							"Type": "Word",
							"Pos": {
								"Offset": 293,
								"Line": 21,
								"Col": 5
							},
							"End": {
								"Offset": 296,
								"Line": 21,
								"Col": 8
							},
							"Parts": [
								{
									"Type": "Lit",
									"Pos": {
										"Offset": 293,
										"Line": 21,
										"Col": 5
									},
									"End": {
										"Offset": 296,
										"Line": 21,
										"Col": 8
									},
									"ValuePos": {
										"Offset": 293,
										"Line": 21,
										"Col": 5
									},
									"ValueEnd": {
										"Offset": 296,
										"Line": 21,
										"Col": 8
									},
									"Value": "foo"
								}
							]
						},
						"Y": {
							"Type": "BinaryArithm",
							"Pos": {
								"Offset": 297,
								"Line": 21,
								"Col": 9
							},
							"End": {
								"Offset": 304,
								"Line": 21,
								"Col": 16
							},
							"OpPos": {
								"Offset": 302,
								"Line": 21,
								"Col": 14
							},
							"Op": 68,
							"X": {
								"Type": "ParenArithm",
								"Pos": {
									"Offset": 297,
									"Line": 21,
									"Col": 9
								},
								"End": {
									"Offset": 302,
									"Line": 21,
									"Col": 14
								},
								"Lparen": {
									"Offset": 297,
									"Line": 21,
									"Col": 9
								},
								"Rparen": {
									"Offset": 301,
									"Line": 21,
									"Col": 13
								},
								"X": {
									"Type": "Word",
									"Pos": {
										"Offset": 298,
										"Line": 21,
										"Col": 10
									},
									"End": {
										"Offset": 301,
										"Line": 21,
										"Col": 13
									},
									"Parts": [
										{
											"Type": "Lit",
											"Pos": {
												"Offset": 298,
												"Line": 21,
												"Col": 10
											},
											"End": {
												"Offset": 301,
												"Line": 21,
												"Col": 13
											},
											"ValuePos": {
												"Offset": 298,
												"Line": 21,
												"Col": 10
											},
											"ValueEnd": {
												"Offset": 301,
												"Line": 21,
												"Col": 13
											},
											"Value": "bar"
										}
									]
								}
							},
							"Y": {
								"Type": "Word",
								"Pos": {
									"Offset": 303,
									"Line": 21,
									"Col": 15
								},
								"End": {
									"Offset": 304,
									"Line": 21,
									"Col": 16
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 303,
											"Line": 21,
											"Col": 15
										},
										"End": {
											"Offset": 304,
											"Line": 21,
											"Col": 16
										},
										"ValuePos": {
											"Offset": 303,
											"Line": 21,
											"Col": 15
										},
										"ValueEnd": {
											"Offset": 304,
											"Line": 21,
											"Col": 16
										},
										"Value": "3"
									}
								]
							}
						}
					}
				]
			},
			"Position": {
				"Offset": 289,
				"Line": 21,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 305,
				"Line": 22,
				"Col": 1
			},
			"End": {
				"Offset": 313,
				"Line": 22,
				"Col": 9
			},
			"Cmd": {
				"Type": "TimeClause",
				"Pos": {
					"Offset": 305,
					"Line": 22,
					"Col": 1
				},
				"End": {
					"Offset": 313,
					"Line": 22,
					"Col": 9
				},
				"Time": {
					"Offset": 305,
					"Line": 22,
					"Col": 1
				},
				"Stmt": {
					"Pos": {
						"Offset": 310,
						"Line": 22,
						"Col": 6
					},
					"End": {
						"Offset": 313,
						"Line": 22,
						"Col": 9
					},
					"Cmd": {
						"Type": "CallExpr",
						"Pos": {
							"Offset": 310,
							"Line": 22,
							"Col": 6
						},
						"End": {
							"Offset": 313,
							"Line": 22,
							"Col": 9
						},
						"Args": [
							{
								"Pos": {
									"Offset": 310,
									"Line": 22,
									"Col": 6
								},
								"End": {
									"Offset": 313,
									"Line": 22,
									"Col": 9
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 310,
											"Line": 22,
											"Col": 6
										},
										"End": {
											"Offset": 313,
											"Line": 22,
											"Col": 9
										},
										"ValuePos": {
											"Offset": 310,
											"Line": 22,
											"Col": 6
										},
										"ValueEnd": {
											"Offset": 313,
											"Line": 22,
											"Col": 9
										},
										"Value": "foo"
									}
								]
							}
						]
					},
					"Position": {
						"Offset": 310,
						"Line": 22,
						"Col": 6
					}
				}
			},
			"Position": {
				"Offset": 305,
				"Line": 22,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 314,
				"Line": 23,
				"Col": 1
			},
			"End": {
				"Offset": 324,
				"Line": 23,
				"Col": 11
			},
			"Cmd": {
				"Type": "CoprocClause",
				"Pos": {
					"Offset": 314,
					"Line": 23,
					"Col": 1
				},
				"End": {
					"Offset": 324,
					"Line": 23,
					"Col": 11
				},
				"Coproc": {
					"Offset": 314,
					"Line": 23,
					"Col": 1
				},
				"Stmt": {
					"Pos": {
						"Offset": 321,
						"Line": 23,
						"Col": 8
					},
					"End": {
						"Offset": 324,
						"Line": 23,
						"Col": 11
					},
					"Cmd": {
						"Type": "CallExpr",
						"Pos": {
							"Offset": 321,
							"Line": 23,
							"Col": 8
						},
						"End": {
							"Offset": 324,
							"Line": 23,
							"Col": 11
						},
						"Args": [
							{
								"Pos": {
									"Offset": 321,
									"Line": 23,
									"Col": 8
								},
								"End": {
									"Offset": 324,
									"Line": 23,
									"Col": 11
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 321,
											"Line": 23,
											"Col": 8
										},
										"End": {
											"Offset": 324,
											"Line": 23,
											"Col": 11
										},
										"ValuePos": {
											"Offset": 321,
											"Line": 23,
											"Col": 8
										},
										"ValueEnd": {
											"Offset": 324,
											"Line": 23,
											"Col": 11
										},
										"Value": "foo"
									}
								]
							}
						]
					},
					"Position": {
						"Offset": 321,
						"Line": 23,
						"Col": 8
					}
				}
			},
			"Position": {
				"Offset": 314,
				"Line": 23,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 325,
				"Line": 24,
				"Col": 1
			},
			"End": {
				"Offset": 330,
				"Line": 24,
				"Col": 6
			},
			"Cmd": {
				"Type": "ArithmCmd",
				"Pos": {
					"Offset": 325,
					"Line": 24,
					"Col": 1
				},
				"End": {
					"Offset": 330,
					"Line": 24,
					"Col": 6
				},
				"Left": {
					"Offset": 325,
					"Line": 24,
					"Col": 1
				},
				"Right": {
					"Offset": 328,
					"Line": 24,
					"Col": 4
				},
				"X": {
					"Type": "Word",
					"Pos": {
						"Offset": 327,
						"Line": 24,
						"Col": 3
					},
					"End": {
						"Offset": 328,
						"Line": 24,
						"Col": 4
					},
					"Parts": [
						{
							"Type": "Lit",
							"Pos": {
								"Offset": 327,
								"Line": 24,
								"Col": 3
							},
							"End": {
								"Offset": 328,
								"Line": 24,
								"Col": 4
							},
							"ValuePos": {
								"Offset": 327,
								"Line": 24,
								"Col": 3
							},
							"ValueEnd": {
								"Offset": 328,
								"Line": 24,
								"Col": 4
							},
							"Value": "2"
						}
					]
				}
			},
			"Position": {
				"Offset": 325,
				"Line": 24,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 332,
				"Line": 26,
				"Col": 1
			},
			"End": {
				"Offset": 352,
				"Line": 26,
				"Col": 21
			},
			"Cmd": {
				"Type": "TestClause",
				"Pos": {
					"Offset": 332,
					"Line": 26,
					"Col": 1
				},
				"End": {
					"Offset": 352,
					"Line": 26,
					"Col": 21
				},
				"Left": {
					"Offset": 332,
					"Line": 26,
					"Col": 1
				},
				"Right": {
					"Offset": 350,
					"Line": 26,
					"Col": 19
				},
				"X": {
					"Type": "UnaryTest",
					"Pos": {
						"Offset": 335,
						"Line": 26,
						"Col": 4
					},
					"End": {
						"Offset": 349,
						"Line": 26,
						"Col": 18
					},
					"OpPos": {
						"Offset": 335,
						"Line": 26,
						"Col": 4
					},
					"Op": 34,
					"X": {
						"Type": "ParenTest",
						"Pos": {
							"Offset": 337,
							"Line": 26,
							"Col": 6
						},
						"End": {
							"Offset": 349,
							"Line": 26,
							"Col": 18
						},
						"Lparen": {
							"Offset": 337,
							"Line": 26,
							"Col": 6
						},
						"Rparen": {
							"Offset": 348,
							"Line": 26,
							"Col": 17
						},
						"X": {
							"Type": "BinaryTest",
							"Pos": {
								"Offset": 338,
								"Line": 26,
								"Col": 7
							},
							"End": {
								"Offset": 348,
								"Line": 26,
								"Col": 17
							},
							"OpPos": {
								"Offset": 342,
								"Line": 26,
								"Col": 11
							},
							"Op": 10,
							"X": {
								"Type": "Word",
								"Pos": {
									"Offset": 338,
									"Line": 26,
									"Col": 7
								},
								"End": {
									"Offset": 341,
									"Line": 26,
									"Col": 10
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 338,
											"Line": 26,
											"Col": 7
										},
										"End": {
											"Offset": 341,
											"Line": 26,
											"Col": 10
										},
										"ValuePos": {
											"Offset": 338,
											"Line": 26,
											"Col": 7
										},
										"ValueEnd": {
											"Offset": 341,
											"Line": 26,
											"Col": 10
										},
										"Value": "foo"
									}
								]
							},
							"Y": {
								"Type": "Word",
								"Pos": {
									"Offset": 345,
									"Line": 26,
									"Col": 14
								},
								"End": {
									"Offset": 348,
									"Line": 26,
									"Col": 17
								},
								"Parts": [
									{
										"Type": "Lit",
										"Pos": {
											"Offset": 345,
											"Line": 26,
											"Col": 14
										},
										"End": {
											"Offset": 348,
											"Line": 26,
											"Col": 17
										},
										"ValuePos": {
											"Offset": 345,
											"Line": 26,
											"Col": 14
										},
										"ValueEnd": {
											"Offset": 348,
											"Line": 26,
											"Col": 17
										},
										"Value": "bar"
									}
								]
							}
						}
					}
				}
			},
			"Position": {
				"Offset": 332,
				"Line": 26,
				"Col": 1
			}
		},
		{
			"Pos": {
				"Offset": 365,
				"Line": 30,
				"Col": 1
			},
			"End": {
				"Offset": 368,
				"Line": 30,
				"Col": 4
			},
			"Comments": [
				{
					"Pos": {
						"Offset": 354,
						"Line": 28,
						"Col": 1
					},
					"End": {
						"Offset": 363,
						"Line": 28,
						"Col": 10
					},
					"Hash": {
						"Offset": 354,
						"Line": 28,
						"Col": 1
					},
					"Text": " comment"
				}
			],
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 365,
					"Line": 30,
					"Col": 1
				},
				"End": {
					"Offset": 368,
					"Line": 30,
					"Col": 4
				},
				"Args": [
					{
						"Pos": {
							"Offset": 365,
							"Line": 30,
							"Col": 1
						},
						"End": {
							"Offset": 368,
							"Line": 30,
							"Col": 4
						},
						"Parts": [
							{
								"Type": "ProcSubst",
								"Pos": {
									"Offset": 365,
									"Line": 30,
									"Col": 1
								},
								"End": {
									"Offset": 368,
									"Line": 30,
									"Col": 4
								},
								"OpPos": {
									"Offset": 365,
									"Line": 30,
									"Col": 1
								},
								"Rparen": {
									"Offset": 367,
									"Line": 30,
									"Col": 3
								},
								"Op": 66
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 365,
				"Line": 30,
				"Col": 1
			}
		}
	]
}
07070100000074000081A4000000000000000000000001686AE5C000000171000000000000000000000000000000000000003600000000sh-3.12.0/syntax/typedjson/testdata/roundtrip/file.shfoo
! foo
foo &
'foo' "bar"
${foo} $(bar) $((baz))
@(foo) {bar,baz}

foo && bar || baz
foo | bar |& baz

if foo; then bar; fi
for i in 1 2 3; do bar; done
for ((i = 0; i < 3; i++)); do bar; done
while foo; do bar; done
case i in foo) bar ;; esac

{ foo; }
(foo)
foo() { bar; }
declare foo
let foo=(bar)+3
time foo
coproc foo
((2))

[[ ! (foo && bar) ]]

# comment

<()
07070100000075000081A4000000000000000000000001686AE5C00000175A000000000000000000000000000000000000001900000000sh-3.12.0/syntax/walk.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"fmt"
	"io"
	"reflect"
)

// 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
// f(nil).
func Walk(node Node, f func(Node) bool) {
	if !f(node) {
		return
	}

	switch node := node.(type) {
	case *File:
		walkList(node.Stmts, f)
		walkComments(node.Last, f)
	case *Comment:
	case *Stmt:
		for _, c := range node.Comments {
			if !node.End().After(c.Pos()) {
				defer Walk(&c, f)
				break
			}
			Walk(&c, f)
		}
		if node.Cmd != nil {
			Walk(node.Cmd, f)
		}
		walkList(node.Redirs, f)
	case *Assign:
		walkNilable(node.Name, f)
		walkNilable(node.Value, f)
		walkNilable(node.Index, f)
		walkNilable(node.Array, f)
	case *Redirect:
		walkNilable(node.N, f)
		Walk(node.Word, f)
		walkNilable(node.Hdoc, f)
	case *CallExpr:
		walkList(node.Assigns, f)
		walkList(node.Args, f)
	case *Subshell:
		walkList(node.Stmts, f)
		walkComments(node.Last, f)
	case *Block:
		walkList(node.Stmts, f)
		walkComments(node.Last, f)
	case *IfClause:
		walkList(node.Cond, f)
		walkComments(node.CondLast, f)
		walkList(node.Then, f)
		walkComments(node.ThenLast, f)
		walkNilable(node.Else, f)
	case *WhileClause:
		walkList(node.Cond, f)
		walkComments(node.CondLast, f)
		walkList(node.Do, f)
		walkComments(node.DoLast, f)
	case *ForClause:
		Walk(node.Loop, f)
		walkList(node.Do, f)
		walkComments(node.DoLast, f)
	case *WordIter:
		Walk(node.Name, f)
		walkList(node.Items, f)
	case *CStyleLoop:
		walkNilable(node.Init, f)
		walkNilable(node.Cond, f)
		walkNilable(node.Post, f)
	case *BinaryCmd:
		Walk(node.X, f)
		Walk(node.Y, f)
	case *FuncDecl:
		Walk(node.Name, f)
		Walk(node.Body, f)
	case *Word:
		walkList(node.Parts, f)
	case *Lit:
	case *SglQuoted:
	case *DblQuoted:
		walkList(node.Parts, f)
	case *CmdSubst:
		walkList(node.Stmts, f)
		walkComments(node.Last, f)
	case *ParamExp:
		Walk(node.Param, f)
		walkNilable(node.Index, f)
		if node.Repl != nil {
			walkNilable(node.Repl.Orig, f)
			walkNilable(node.Repl.With, f)
		}
		if node.Exp != nil {
			walkNilable(node.Exp.Word, f)
		}
	case *ArithmExp:
		Walk(node.X, f)
	case *ArithmCmd:
		Walk(node.X, f)
	case *BinaryArithm:
		Walk(node.X, f)
		Walk(node.Y, f)
	case *BinaryTest:
		Walk(node.X, f)
		Walk(node.Y, f)
	case *UnaryArithm:
		Walk(node.X, f)
	case *UnaryTest:
		Walk(node.X, f)
	case *ParenArithm:
		Walk(node.X, f)
	case *ParenTest:
		Walk(node.X, f)
	case *CaseClause:
		Walk(node.Word, f)
		walkList(node.Items, f)
		walkComments(node.Last, f)
	case *CaseItem:
		for _, c := range node.Comments {
			if c.Pos().After(node.Pos()) {
				defer Walk(&c, f)
				break
			}
			Walk(&c, f)
		}
		walkList(node.Patterns, f)
		walkList(node.Stmts, f)
		walkComments(node.Last, f)
	case *TestClause:
		Walk(node.X, f)
	case *DeclClause:
		walkList(node.Args, f)
	case *ArrayExpr:
		walkList(node.Elems, f)
		walkComments(node.Last, f)
	case *ArrayElem:
		for _, c := range node.Comments {
			if c.Pos().After(node.Pos()) {
				defer Walk(&c, f)
				break
			}
			Walk(&c, f)
		}
		walkNilable(node.Index, f)
		walkNilable(node.Value, f)
	case *ExtGlob:
		Walk(node.Pattern, f)
	case *ProcSubst:
		walkList(node.Stmts, f)
		walkComments(node.Last, f)
	case *TimeClause:
		walkNilable(node.Stmt, f)
	case *CoprocClause:
		walkNilable(node.Name, f)
		Walk(node.Stmt, f)
	case *LetClause:
		walkList(node.Exprs, f)
	case *TestDecl:
		Walk(node.Description, f)
		Walk(node.Body, f)
	default:
		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
}

type debugPrinter struct {
	out   io.Writer
	level int
	err   error
}

func (p *debugPrinter) printf(format string, args ...any) {
	_, err := fmt.Fprintf(p.out, format, args...)
	if err != nil && p.err == nil {
		p.err = err
	}
}

func (p *debugPrinter) newline() {
	p.printf("\n")
	for range p.level {
		p.printf(".  ")
	}
}

func (p *debugPrinter) print(x reflect.Value) {
	switch x.Kind() {
	case reflect.Interface:
		if x.IsNil() {
			p.printf("nil")
			return
		}
		p.print(x.Elem())
	case reflect.Ptr:
		if x.IsNil() {
			p.printf("nil")
			return
		}
		p.printf("*")
		p.print(x.Elem())
	case reflect.Slice:
		p.printf("%s (len = %d) {", x.Type(), x.Len())
		if x.Len() > 0 {
			p.level++
			p.newline()
			for i := range x.Len() {
				p.printf("%d: ", i)
				p.print(x.Index(i))
				if i == x.Len()-1 {
					p.level--
				}
				p.newline()
			}
		}
		p.printf("}")

	case reflect.Struct:
		if v, ok := x.Interface().(Pos); ok {
			if v.IsRecovered() {
				p.printf("<recovered>")
				return
			}
			p.printf("%v:%v", v.Line(), v.Col())
			return
		}
		t := x.Type()
		p.printf("%s {", t)
		p.level++
		p.newline()
		for i := range t.NumField() {
			p.printf("%s: ", t.Field(i).Name)
			p.print(x.Field(i))
			if i == x.NumField()-1 {
				p.level--
			}
			p.newline()
		}
		p.printf("}")
	default:
		if s, ok := x.Interface().(fmt.Stringer); ok && !x.IsZero() {
			p.printf("%#v (%s)", x.Interface(), s)
		} else {
			p.printf("%#v", x.Interface())
		}
	}
}
07070100000076000081A4000000000000000000000001686AE5C000000B94000000000000000000000000000000000000001E00000000sh-3.12.0/syntax/walk_test.go// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	"reflect"
	"strings"
	"testing"
)

func TestWalk(t *testing.T) {
	t.Parallel()
	seen := map[string]bool{
		"*syntax.File":         false,
		"*syntax.Comment":      false,
		"*syntax.Stmt":         false,
		"*syntax.Assign":       false,
		"*syntax.Redirect":     false,
		"*syntax.CallExpr":     false,
		"*syntax.Subshell":     false,
		"*syntax.Block":        false,
		"*syntax.IfClause":     false,
		"*syntax.WhileClause":  false,
		"*syntax.ForClause":    false,
		"*syntax.WordIter":     false,
		"*syntax.CStyleLoop":   false,
		"*syntax.BinaryCmd":    false,
		"*syntax.FuncDecl":     false,
		"*syntax.Word":         false,
		"*syntax.Lit":          false,
		"*syntax.SglQuoted":    false,
		"*syntax.DblQuoted":    false,
		"*syntax.CmdSubst":     false,
		"*syntax.ParamExp":     false,
		"*syntax.ArithmExp":    false,
		"*syntax.ArithmCmd":    false,
		"*syntax.BinaryArithm": false,
		"*syntax.UnaryArithm":  false,
		"*syntax.ParenArithm":  false,
		"*syntax.CaseClause":   false,
		"*syntax.CaseItem":     false,
		"*syntax.TestClause":   false,
		"*syntax.BinaryTest":   false,
		"*syntax.UnaryTest":    false,
		"*syntax.ParenTest":    false,
		"*syntax.DeclClause":   false,
		"*syntax.ArrayExpr":    false,
		"*syntax.ArrayElem":    false,
		"*syntax.ExtGlob":      false,
		"*syntax.ProcSubst":    false,
		"*syntax.TimeClause":   false,
		"*syntax.CoprocClause": false,
		"*syntax.LetClause":    false,
	}
	parser := NewParser(KeepComments(true))
	var allStrs []string
	for _, c := range fileTests {
		allStrs = append(allStrs, c.Strs[0])
	}
	for _, c := range printTests {
		allStrs = append(allStrs, c.in)
	}
	for _, in := range allStrs {
		t.Run("", func(t *testing.T) {
			prog, err := parser.Parse(strings.NewReader(in), "")
			if err != nil {
				// good enough for now, as the bash
				// parser ignoring errors covers what we
				// need.
				return
			}
			lastOffs := uint(0)
			Walk(prog, func(node Node) bool {
				if node == nil {
					return false
				}
				tstr := reflect.TypeOf(node).String()
				if _, ok := seen[tstr]; !ok {
					t.Errorf("unexpected type: %s", tstr)
				} else {
					seen[tstr] = true
				}
				switch node.(type) {
				case *Lit:
					return false
				case *Comment:
				default:
					return true
				}
				offs := node.Pos().Offset()
				if offs >= lastOffs {
					lastOffs = offs
				} else {
					t.Errorf("comment offset goes back")
				}
				return true
			})
		})
	}
	for tstr, tseen := range seen {
		if !tseen {
			t.Errorf("type not seen: %s", tstr)
		}
	}
}

type newNode struct{}

func (newNode) Pos() Pos { return Pos{} }
func (newNode) End() Pos { return Pos{} }

func TestWalkUnexpectedType(t *testing.T) {
	t.Parallel()
	defer func() {
		if r := recover(); r == nil {
			t.Errorf("did not panic")
		}
	}()
	Walk(newNode{}, func(node Node) bool {
		return true
	})
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!1869 blocks
openSUSE Build Service is sponsored by