[
  {
    "path": ".gitattributes",
    "content": "# To prevent CRLF breakages on Windows for fragile files, like testdata.\n* -text\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: mvdan\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "on: [push, pull_request]\nname: Test\njobs:\n  test:\n    strategy:\n      matrix:\n        go-version: [1.25.x, 1.26.x]\n        os: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 10\n    steps:\n    - uses: actions/checkout@v5\n    - uses: actions/setup-go@v5\n      with:\n        go-version: ${{ matrix.go-version }}\n        cache: false\n\n    - run: go test ./...\n    - run: cd moreinterp && go test ./...\n\n    - run: go test -race ./...\n      if: matrix.os == 'ubuntu-latest'\n    - run: GOARCH=386 go test -count=1 ./...\n      if: matrix.os == 'ubuntu-latest'\n    - name: confirm tests with Bash 5.2\n      run: |\n        go install mvdan.cc/dockexec@latest\n        CGO_ENABLED=0 go test -run TestRunnerRunConfirm -exec 'dockexec bash:5.2' ./interp\n      if: matrix.os == 'ubuntu-latest'\n\n    # Test that we can build for platforms that we can't currently test on.\n    - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.26.x'\n      run: |\n        GOOS=plan9 GOARCH=amd64 go build ./...\n        GOOS=js GOARCH=wasm go build ./...\n\n    # Static checks from this point forward. Only run on one Go version and on\n    # Linux, since it's the fastest platform, and the tools behave the same.\n    - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.26.x'\n      run: diff <(echo -n) <(gofmt -s -d .)\n    - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.26.x'\n      run: go vet ./...\n\n  test-linux-alpine:\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    steps:\n    - uses: actions/checkout@v5\n    - name: Test as root, without cgo, and with busybox\n      run: docker run -v=\"$PWD:/pwd\" -w=/pwd --user=1000 -e=GOCACHE=/tmp -e=CGO_ENABLED=0 golang:1.26.0-alpine go test ./...\n\n  docker:\n    name: Build and test Docker images\n    # Only deploy if previous stages pass.\n    needs: [test, test-linux-alpine]\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    services:\n      registry:\n        image: registry:2\n        ports:\n          - 5000:5000\n        # this is needed because we restart the docker daemon for experimental\n        # support\n        options: \"--restart always\"\n    env:\n      # Export environment variables for all stages.\n      DOCKER_USER: ${{ secrets.DOCKER_USER }}\n      DOCKER_DEPLOY_IMAGES: false\n      DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}\n      DOCKER_REPO: shfmt\n      # We use all platforms for which FROM images in our Dockerfile are\n      # available.\n      DOCKER_PLATFORMS: >\n        linux/386\n        linux/amd64\n        linux/arm/v7\n        linux/arm64/v8\n        linux/ppc64le\n\n      # linux/s390x TODO: reenable when we figure out its weird errors when\n      # fetching dependencies, including:\n      #\n      # zip: checksum error\n      # Get \"https://proxy.golang.org/...\": local error: tls: bad record MAC\n      # Get \"https://proxy.golang.org/...\": local error: tls: unexpected message\n      # Get \"https://proxy.golang.org/...\": x509: certificate signed by unknown authority\n    steps:\n    - uses: actions/checkout@v5\n      with:\n        fetch-depth: 0 # also fetch tags for 'git describe'\n    # Enable docker daemon experimental support (for 'pull --platform').\n    - name: Enable experimental support\n      run: |\n        config='/etc/docker/daemon.json'\n        if [[ -e \"$config\" ]]; then\n          sudo sed -i -e 's/{/{ \"experimental\": true, /' \"$config\"\n        else\n          echo '{ \"experimental\": true }' | sudo tee \"$config\"\n        fi\n        sudo systemctl restart docker\n    - uses: docker/setup-qemu-action@v3\n    - uses: docker/setup-buildx-action@v3\n      with:\n        driver-opts: network=host\n    - name: Set up env vars\n      run: |\n        set -vx\n        # Export environment variable for later stages.\n        if echo \"$GITHUB_REF\" | grep -q '^refs/heads/master$'; then\n          # Pushes to the master branch deploy 'latest'.\n          echo \"TAG=latest\" >> $GITHUB_ENV\n        elif echo \"$GITHUB_REF\" | grep -q '^refs/heads/docker-push-test$'; then\n          # Pushes to the test branch deploy 'latest-test'.\n          echo \"TAG=latest-test\" >> $GITHUB_ENV\n        elif echo \"$GITHUB_REF\" | grep -q '^refs/tags/'; then\n          # Pushes to a git tag use it as the docker tag.\n          echo \"TAG=${GITHUB_REF/refs\\/tags\\//}\" >> $GITHUB_ENV\n        else\n          # Otherwise, we build and test the image locally, but we don't push it.\n          echo \"TAG=${GITHUB_SHA::8}\" >> $GITHUB_ENV\n        fi\n        echo \"DOCKER_BASE=test/${{ env.DOCKER_REPO }}\" >> $GITHUB_ENV\n        echo \"DOCKER_BUILD_PLATFORMS=${DOCKER_PLATFORMS// /,}\" >> $GITHUB_ENV\n    - name: Build and push to local registry\n      uses: docker/build-push-action@v5\n      with:\n        provenance: false # temporarily work around https://github.com/containers/skopeo/issues/1874\n        context: .\n        file: ./cmd/shfmt/Dockerfile\n        platforms: ${{ env.DOCKER_BUILD_PLATFORMS }}\n        push: true\n        tags: localhost:5000/${{ env.DOCKER_BASE }}:${{ env.TAG }}\n    - name: Build and push to local registry (alpine)\n      uses: docker/build-push-action@v5\n      with:\n        provenance: false # temporarily work around https://github.com/containers/skopeo/issues/1874\n        context: .\n        file: ./cmd/shfmt/Dockerfile\n        platforms: ${{ env.DOCKER_BUILD_PLATFORMS }}\n        push: true\n        tags: localhost:5000/${{ env.DOCKER_BASE }}:${{ env.TAG }}-alpine\n        target: alpine\n    - name: Test multi-arch Docker images locally\n      run: |\n        for platform in $DOCKER_PLATFORMS; do\n          for ext in '' '-alpine'; do\n            image=\"localhost:5000/${DOCKER_BASE}:${TAG}${ext}\"\n            msg=\"Testing docker image $image on platform $platform\"\n            line=\"${msg//?/=}\"\n            printf \"\\n${line}\\n${msg}\\n${line}\\n\"\n            docker pull -q --platform \"$platform\" \"$image\"\n            if [[ -n \"$ext\" ]]; then\n              echo -n \"Image architecture: \"\n              docker run --rm --entrypoint /bin/sh \"$image\" -c 'uname -m'\n            fi\n            version=$(docker run --rm \"$image\" --version)\n            echo \"shfmt version: $version\"\n            if [[ $TAG != \"latest\" ]] &&\n              [[ $TAG != \"latest-test\" ]] &&\n              [[ $TAG != \"$version\" ]] &&\n              ! echo \"$version\" | grep -q \"$TAG\"; then\n              echo \"Version mismatch: shfmt $version tagged as $TAG\"\n              exit 1\n            fi\n            docker run --rm -v \"$PWD:/mnt\" -w '/mnt' \"$image\" -d cmd/shfmt/docker-entrypoint.sh\n          done\n        done\n    - name: Check GitHub settings\n      if: >\n        github.event_name == 'push' &&\n        github.repository == 'mvdan/sh' &&\n        (github.ref == 'refs/heads/master' ||\n        github.ref == 'refs/heads/docker-push-test' ||\n        startsWith(github.ref, 'refs/tags/'))\n      run: |\n        missing=()\n        [[ -n \"${{ secrets.DOCKER_USER }}\" ]] || missing+=(DOCKER_USER)\n        [[ -n \"${{ secrets.DOCKER_TOKEN }}\" ]] || missing+=(DOCKER_TOKEN)\n        for i in \"${missing[@]}\"; do\n          echo \"Missing github secret: $i\"\n        done\n        (( ${#missing[@]} == 0 )) || exit 1\n        echo \"DOCKER_DEPLOY_IMAGES=true\" >> $GITHUB_ENV\n    - name: Login to DockerHub\n      if: ${{ env.DOCKER_DEPLOY_IMAGES == 'true' }}\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKER_USER }}\n        password: ${{ secrets.DOCKER_TOKEN }}\n    - name: Push images to DockerHub\n      if: ${{ env.DOCKER_DEPLOY_IMAGES == 'true' }}\n      run: |\n        for ext in '' '-alpine'; do\n          image_src=\"${DOCKER_BASE}:${TAG}${ext}\"\n\n          image_dsts=(\"${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }}:${TAG}${ext}\")\n          if echo $TAG | grep -q '^v3\\.[0-9]\\+\\.[0-9]\\+$'; then\n            image_dsts+=(\"${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }}:v3${ext}\")\n          elif [[ $TAG == latest-test ]]; then\n            image_dsts+=(\"${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }}:v3-test${ext}\")\n          fi\n\n          # Show what we're doing.\n          msg=\"Copy multi-arch docker images to DockerHub ($image_src with ${#image_dsts[@]} destinations)\"\n          line=\"${msg//?/=}\"\n          printf \"\\n${line}\\n${msg}\\n${line}\\n\"\n\n          for image_dst in \"${image_dsts[@]}\"; do\n            skopeo copy --all --src-tls-verify=0 docker://localhost:5000/$image_src docker://docker.io/$image_dst\n          done\n        done\n    - name: Update DockerHub description\n      if: ${{ env.DOCKER_DEPLOY_IMAGES == 'true' }}\n      uses: peter-evans/dockerhub-description@v4\n      with:\n        username: ${{ secrets.DOCKER_USER }}\n        password: ${{ secrets.DOCKER_TOKEN }}\n        repository: ${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }}\n        readme-filepath: README.md\n"
  },
  {
    "path": ".gitignore",
    "content": "*.a\n*.zip\n\n# Don't store any of this in the master branch.\nsuppressions/\ncrashers/\ncorpus/\nvendor/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [3.12.0] - 2025-07-06\n\n- The `mvdan-sh` JS package is discontinued in favor of `sh-syntax` - #1145\n- **cmd/shfmt**\n  - Support the \"simplify\" and \"minify\" flags via EditorConfig - #819\n  - Do not allow `--write` to replace non-regular files - #843\n- **interp**\n  - Add `IsBuiltin` to check if a command name is a shell built-in - #1164\n  - Add `HandlerContext.Builtin` to allow `ExecHandlerFunc` to call built-ins\n  - Initial support for `$!` and `wait PID` - #221\n  - Return non-fatal `ExecHandlerFunc` errors via the `Runner.Run` API\n  - Add `HandlerContext.Pos` to provide handlers with source positions\n  - Deprecate `NewExitStatus` and `IsExitStatus` in favor of `ExitStatus`\n  - Fix `wait` to always return the status of the last given job\n  - Copy all env vars for background subshells to avoid data races\n  - Support reading random numbers via `$RANDOM` and `$SRANDOM`\n  - Set `$BASH_REMATCH` when matching regular expressions via `=~`\n  - Support modifying local vars from the parent calling function\n- **expand**\n  - Adjust which backslash sequences are expanded in here-docs - #1138\n  - Tweak tilde expansions to match Bash semantics\n- **pattern**\n  - Remove the flawed and broken `Braces` mode; use `syntax.SplitBraces` instead\n  - Tweak `**` to only act as \"globstar\" when alone as a path element - #1149\n  - Tweak `*` and `**` to not match leading dots in basenames\n  - Add a `NoGlobStar` mode to match the POSIX semantics\n- **fileutil**\n  - Treat all non-regular files as definitely not shell scripts - #1089\n\n## [3.11.0] - 2025-03-05\n\nThis release drops support for Go 1.22 and includes many enhancements.\n\n- **cmd/shfmt**\n  - Support `-l=0` and `-f=0` to split filenames with null bytes - #1096\n- **syntax**\n  - New iterator API: `Parser.WordsSeq`\n  - Fix `Parser.Incomplete` and `IsIncomplete` to work well with `Parser.Words` - #937\n  - Initial support for parsing incomplete shell via `RecoverErrors`\n  - Expand `LangError` to include which language was used when parsing\n- **interp**\n  - Refactor setting variables to fix array declaration edge cases - #1108\n  - Fix `test` read/write/exec operators to work correctly on directories - #1116\n  - Replace the `cancelreader` dependency with `os.File.SetReadDeadline`\n  - Avoid waiting for process substitutions, matching Bash\n  - Skip `OpenHandler` when opening named pipes for process substitutions - #1120\n  - Use `TMPDIR` if set via `Env` to create temporary files such as named pipes\n- **expand**\n  - New iterator API: `FieldsSeq`\n  - Correctly handle repeated backslashes in double quotes - #1106\n  - Don't expand backslashes inside here-documents - #1070\n  - Replace the `Unset` kind with a new `Variable.Set` boolean field\n\nConsider [becoming a sponsor](https://github.com/sponsors/mvdan) if you benefit from the work that went into this release!\n\n## [3.10.0] - 2024-10-20\n\n- **cmd/shfmt**\n  - Report the correct language variant in parser error messages - #1102\n  - Move `--filename` out of the parser options category - #1079\n- **syntax**\n  - Parse all CRLF line endings as LF, including inside heredocs - #1088\n  - Count skipped backslashes inside backticks in position column numbers - #1098\n  - Count skipped null bytes in position column numbers for consistency\n- **interp**\n  - Fix a regression in `v3.9.0` which broke redirecting files to stdin - #1099\n  - Fix a regression in `v3.9.0` where `HandlerContext.Stdin` was never nil\n  - Add an `Interactive` option to be used by interactive shells - #1100\n  - Support closing stdin, stdout, and stderr via redirections like `<&-`\n\nConsider [becoming a sponsor](https://github.com/sponsors/mvdan) if you benefit from the work that went into this release!\n\n## [3.9.0] - 2024-08-16\n\nThis release drops support for Go 1.21 and includes many fixes.\n\n- **cmd/shfmt**\n  - Switch the diff implementation to remove one dependency\n- **syntax**\n  - Protect against overflows in position offset integers\n- **interp**\n  - Use `os.Pipe` for stdin to prevent draining by subprocesses - #1085\n  - Support cancelling reads in builtins when stdin is a file - #1066\n  - Support the `nocaseglob` bash option - #1073\n  - Support the Bash 5.2 `@k` parameter expansion operator\n  - Support the `test -O` and `test -G` operators on non-Windows - #1080\n  - Support the `read -s` builtin flag - #1063\n- **expand**\n  - Add support for case insensitive globbing - #1073\n  - Don't panic when pattern words are nil - #1076\n\nA special thanks to @theclapp for their contributors to this release!\n\nConsider [becoming a sponsor](https://github.com/sponsors/mvdan) if you benefit from the work that went into this release!\n\n## [3.8.0] - 2024-02-11\n\nThis release drops support for Go 1.19 and 1.20 and includes many\nfeatures and bugfixes, such as improving EditorConfig support in `shfmt`.\n\n- **cmd/shfmt**\n  - Support EditorConfig language sections such as `[[shell]]` - #664\n  - Add `--apply-ignore` for tools and editors - #1037\n- **syntax**\n  - Allow formatting redirects before all command argumetnts - #942\n  - Support brace expansions with uppercase letters - #1042\n  - Unescape backquotes in single quotes within backquotes - #1041\n  - Better error when using `function` in POSIX mode - #993\n  - Better column numbers for escapes inside backquotes - #1028\n- **interp**\n  - Support parentheses in classic test commands - #1036\n  - Determine access to a directory via `unix.Access` - #1033\n  - Support subshells with `FuncEnviron` as `Env` - #1043\n  - Add support for `fs.DirEntry` via `ReadDirHandler2`\n- **expand**\n  - Add support for `fs.DirEntry` via `ReadDir2`\n  - Support zero-padding in brace expansions - #1042\n\n## [3.7.0] - 2023-06-18\n\n- **syntax**\n  - Correctly parse `$foo#bar` as a single word - #1003\n  - Make `&>` redirect operators an error in POSIX mode - #991\n  - Avoid producing invalid shell when minifying some heredocs - #923\n  - Revert the simplification of `${foo:-}` into `${foo-}` - #970\n- **interp**\n  - Add `ExecHandlers` to support layering multiple middlewares - #964\n  - Add initial support for the `select` clause - #969\n  - Support combining the `errexit` and `pipefail` options - #870\n  - Set `EUID` just like `UID` - #958\n  - Replace panics on unimplemented builtins with errors - #999\n  - Tweak build tags to support building for `js/wasm` - #983\n- **syntax/typedjson**\n  - Avoid `reflect.Value.MethodByName` to reduce binary sizes - #961\n\n## [3.6.0] - 2022-12-11\n\nThis release drops support for Go 1.17 and includes many features and fixes.\n\n- **cmd/shfmt**\n  - Implement `--from-json` as the reverse of `--to-json` - [#900]\n  - Improve the quality of the `--to-json` output - [#900]\n  - Provide detected language when erroring with `-ln=auto` - [#803]\n- **syntax**\n  - Don't require peeking two bytes after `echo *` - [#835]\n  - Simplify `${name:-}` to the equivalent `${name-}` - [#849]\n  - Don't print trailing whitespaces on nested subshells - [#814]\n  - Don't print extra newlines in some case clauses - [#779]\n  - Don't indent comments preceding case clause items - [#917]\n  - Allow escaped newlines before unquoted words again - [#873]\n  - Parse a redirections edge case without spaces - [#879]\n  - Give a helpful error when `<<<` is used in POSIX mode - [#881]\n  - Forbid `${!foo*}` and `${!foo@}` in mksh mode - [#929]\n  - Batch allocations less aggressively in the parser\n- **syntax/typedjson**\n  - Expose `--from-json` and `--to-json` as Go APIs - [#885]\n- **expand**\n  - Improve support for expanding array keys and values - [#884]\n  - Don't panic on unsupported syntax nodes - [#841]\n  - Don't panic on division by zero - [#892]\n  - Properly expand unquoted parameters with spaces - [#886]\n  - Trim spaces when converting strings to integers - [#928]\n- **interp**\n  - Add initial implementation for `mapfile` and `readarray` - [#863]\n  - Improve matching patterns against multiple lines - [#866]\n  - Support `%b` in the `printf` builtin - [#955]\n  - Display all Bash options in `shopt` - [#877]\n- **pattern**\n  - Add `EntireString` to match the entire string using `^$` - [#866]\n\n## [3.5.1] - 2022-05-23\n\n- **cmd/shfmt**\n  - Fix the Docker publishing script bug which broke 3.5.0 - [#860]\n- **interp**\n  - Support multi-line strings when pattern matching in `[[` - [#861]\n  - Invalid glob words are no longer removed with `nullglob` - [#862]\n- **pattern**\n  - `Regexp` now returns the typed error `SyntaxError` - [#862]\n\n## [3.5.0] - 2022-05-11\n\nThis release drops support for Go 1.16 and includes many new features.\n\n- **cmd/shfmt**\n  - Switch to `-ln=auto` by default to detect the shell language\n  - Add support for long flags, like `--indent` for `-i`\n- **syntax**\n  - Allow extglob wildcards as function names like `@() { ... }`\n  - Add support for heredocs surrounded by backquotes\n  - Add support for backquoted inline comments\n  - Add `NewPos` to create `Pos` values externally\n  - Support escaped newlines with CRLF line endings\n  - `Minify` no longer omits a leading shebang comment\n  - Avoid printing escaped newlines in non-quoted words\n  - Fix some printer edge cases where comments weren't properly spaced\n- **fileutil**\n  - Add `Shebang` to extract the shell language from a `#!` line\n- **expand**\n  - Reimplement globstar `**` globbing for correctness\n  - Replace `os.Stat` as the last direct use of the filesystem\n- **interp**\n  - Add `CallHandler` to intercept all interpreted `CallExpr` nodes\n  - Add `ReadDirHandler` to intercept glob expansion filesystem reads\n  - Add `StatHandler` to intercept `os.Stat` and `os.Lstat` calls\n  - Always surface exit codes from command substitutions\n  - Add initial and incomplete support for `set -x`\n  - Add support for `cd -` as `cd \"$OLDPWD\"`\n  - Avoid panic on `set - args`\n\n## [3.4.3] - 2022-02-19\n\n- **cmd/shfmt**\n  - New Docker `v3` tag to track the latest stable version\n  - Don't duplicate errors when walking directories\n- **interp**\n  - Properly handle empty paths in the `test` builtin\n  - Allow unsetting global vars from inside a function again\n  - Use `%w` to wrap errors in `Dir`\n\n## [3.4.2] - 2021-12-24\n\n- The tests no longer assume what locales are installed\n- **interp**\n  - Keep `PATH` list separators OS-specific to fix a recent regression\n  - Avoid negative elapsed durations in the `time` builtin\n\n## [3.4.1] - 2021-11-23\n\n- **syntax**\n  - Don't return an empty string on empty input to `Quote`\n- **expand**\n  - Properly sort in `ListEnviron` to avoid common prefix issues\n- **interp**\n  - `export` used in functions now affects the global scope\n  - Support looking for scripts in `$PATH` in `source`\n  - Properly slice arrays in parameter expansions\n\n## [3.4.0] - 2021-10-01\n\nThis release drops support for Go 1.15,\nwhich allows the code to start benefitting from `io/fs`.\n\n- **cmd/shfmt**\n  - Walks directories ~10% faster thanks to `filepath.WalkDir`\n- **syntax**\n  - Add `Quote` to mirror `strconv.Quote` for shell syntax\n  - Skip null characters when parsing, just like Bash\n  - Rewrite fuzzers with Go 1.18's native fuzzing\n- **fileutil**\n  - Add `CouldBeScript2` using `io/fs.DirEntry`\n- **expand**\n  - Skip or stop at null characters, just like Bash\n- **interp**\n  - Set `GID` just like `UID`\n  - Add support for `read -p`\n  - Add support for `pwd` flags\n  - Create random FIFOs for process substitutions more robustly\n  - Avoid leaking an open file when interpreting `$(<file)`\n\n## [3.3.1] - 2021-08-01\n\n- **syntax**\n  - Don't convert `&` in a separate line into `;`\n  - Fix a `BinaryNextLine` edge case idempotency bug\n  - Never start printing a command with an escaped newline\n- **interp**\n  - Support calling `Runner.Reset` before `Runner.Run`\n  - Obey `set -e` for failed redirections\n\n## [3.3.0] - 2021-05-17\n\n- **cmd/shfmt**\n  - Document the `FORCE_COLOR` env var to always use colors in diffs\n- **syntax**\n  - Add the printer `SingleLine` option to avoid printing newlines\n  - Positions now use more bits for line numbers than column numbers\n  - Test operators like `&&` and `||` no longer escape newlines\n  - Properly handle closing backquotes in a few edge cases\n  - Properly handle trailing escaped newlines in heredocs\n- **interp**\n  - Redesigned variable scoping to fix a number of edge cases\n  - Refactor `set -o nounset` support to fix many edge cases\n  - Deprecate `LookPath` in favor of `LookPathDir`\n  - Array element words are now expanded correctly\n  - Add support for `trap` with error and exit signals\n  - Add support for `shopt -s nullglob`\n  - Add support for `type -p`\n\n## [3.2.4] - 2021-03-08\n\n- **cmd/shfmt**\n  - Don't stop handling arguments when one results in a failure\n- **expand**\n  - Don't panic when a backslash is followed by EOF\n\n## [3.2.2] - 2021-01-29\n\n- **syntax**\n  - Avoid comment position panic in the printer\n\n## [3.2.1] - 2020-12-02\n\n- **syntax**\n  - Fix an endless loop when parsing single quotes in parameter expansions\n  - Properly print assignments using escaped newlines\n  - Print inline heredoc comments in the right place\n- **interp**\n  - Always expand `~` in Bash test expressions\n- **expand**\n  - Don't panic on out of bounds array index expansions\n\n## [3.2.0] - 2020-10-29\n\n- **cmd/shfmt**\n  - Add a man page via [scdoc](https://sr.ht/~sircmpwn/scdoc/); see [shfmt.1.scd](cmd/shfmt/shfmt.1.scd)\n  - Add `-filename` to give a name to standard input\n- **syntax**\n  - Add initial support for [Bats](https://github.com/bats-core/bats-core)\n  - Protect line and column position numbers against overflows\n  - Rewrite arithmetic parsing to fix operator precedence\n  - Don't add parentheses to `function f {...}` declarations for ksh support\n  - `KeepPadding` now obeys extra indentation when using space indentation\n  - Properly tokenize `((` within test expressions\n  - Properly tokenize single quotes within parameter expansions\n  - Obey print options inside `<<-` heredocs\n  - Don't simplify indexed parameter expansions in arithmetic expressions\n  - Improve parsing errors for missing test expressions\n  - `LangVariant` now implements [flag.Value](https://pkg.go.dev/flag#Value)\n- **interp**\n  - Avoid panic on C-style loops which omit expressions\n  - `$@` and `$*` always exist, so `\"$@\"` can expand to zero words\n\n## [3.1.2] - 2020-06-26\n\n- **syntax**\n  - Fix brace indentation when using `FunctionNextLine`\n  - Support indirect parameter expansions with transformations\n  - Stop heredoc bodies only when the entire line matches\n- **interp**\n  - Make the tests pass on 32-bit platforms\n\n## [3.1.1] - 2020-05-04\n\n- **cmd/shfmt**\n  - Recognise `function_next_line` in EditorConfig files\n- **syntax**\n  - Don't ignore escaped newlines at the end of heredoc bodies\n  - Improve support for parsing regexes in test expressions\n  - Count columns for `KeepPadding` in bytes, to better support unicode\n  - Never let `KeepPadding` add spaces right after indentation\n- **interp**\n  - Hide unset variables when executing programs\n\n## [3.1.0] - 2020-04-07\n\n- Redesigned Docker images, including buildx and an Alpine variant\n- **cmd/shfmt**\n  - Replace source files atomically when possible\n  - Support `ignore = true` in an EditorConfig to skip directories\n  - Add `-fn` to place function opening braces on the next line\n  - Improve behavior of `-f` when given non-directories\n  - Docker images and `go get` installs now embed good version information\n- **syntax**\n  - Add support for nested here-documents\n  - Allow parsing for loops with braces, present in mksh and Bash\n  - Expand `CaseClause` to describe its `in` token\n  - Allow empty lines in Bash arrays in the printer\n  - Support disabling `KeepPadding`\n  - Avoid mis-printing some programs involving `&`\n- **interp**\n  - Add initial support for Bash process substitutions\n  - Add initial support for aliases\n  - Fix an edge case where the status code would not be reset\n  - The exit status code can now reflect being stopped by a signal\n  - `test -t` now uses the interpreter's stdin/stdout/stderr files\n- **expand**\n  - Improve the interaction of `@` and `*` with quotes and `IFS`\n\n## [3.0.2] - 2020-02-22\n\n- **syntax**\n  - Don't indent after escaped newlines in heredocs\n  - Don't parse `*[i]=x` as a valid assignment\n- **interp**\n  - Prevent subshells from defining funcs in the parent shells\n- **expand**\n  - Parameters to `Fields` no longer get braces expanded in-place\n\n## [3.0.1] - 2020-01-11\n\n- **cmd/shfmt**\n  - Fix an edge case where walking directories could panic\n- **syntax**\n  - Only do a trailing read in `Parser.Stmts` if we have open heredocs\n  - Ensure comments are never folded into heredocs\n  - Properly tokenize `)` after a `=~` test regexp\n  - Stop parsing a comment at an escaped newline\n- **expand**\n  - `\"$@\"` now expands to zero fields when there are zero parameters\n\n## [3.0.0] - 2019-12-16\n\nThis is the first stable release as a proper module, now under\n`mvdan.cc/sh/v3/...`. Go 1.12 or later is supported.\n\nA large number of changes have been done since the last feature release a year\nago. All users are encouraged to update. Below are the major highlights.\n\n- **cmd/shfmt**\n  - Support for [EditorConfig](https://editorconfig.org/) files\n  - Drop the dependency on `diff` for the `-d` flag, now using pure Go\n- **syntax**\n  - Overhaul escaped newlines, now represented as `WordPart` positions\n  - Improve some operator type names, to consistently convey meaning\n  - Completely remove `StmtList`\n  - Redesign `IfClause`, making its \"else\" another `IfClause` node\n  - Redesign `DeclClause` to remove its broken `Opts` field\n  - Brace expression parsing is now done with a `BraceExp` word part\n  - Improve comment alignment in `Printer` via a post-process step\n  - Add support for the `~` bitwise negation operator\n  - Record the use of deprecated tokens in the syntax tree\n- **interp**\n  - Improve the module API as \"handlers\", to reduce confusion with Go modules\n  - Split `LookPath` out of `ExecHandler` to allow custom behavior\n  - `Run` now returns `nil` instead of `ShellExitStatus(0)`\n  - `OpenDevImpls` is removed; see `ExampleOpenHandler` for an alternative\n- **expand**\n  - Redesign `Variable` to reduce allocations\n  - Add support for more escape sequences\n  - Make `Config` a bit more powerful via `func` fields\n  - Rework brace expansion via the new `BraceExp` word part\n- **pattern**\n  - New package for shell pattern matching, extracted from `syntax`\n  - Add support for multiple modes, including filenames and braces\n\nSpecial thanks to Konstantin Kulikov for his contribution to this release.\n\n## [2.6.4] - 2019-03-10\n\n- **syntax**\n  - Support array elements without values, like `declare -A x=([index]=)`\n  - Parse `for i; do ...` uniquely, as it's short for `for i in \"$@\"`\n  - Add missing error on unclosed nested backquotes\n- **expand**\n  - Don't expand tildes twice, fixing `echo ~` on Windows\n- **interp**\n  - Fix the use of `Params` as an option to `New`\n  - Support lowercase Windows volume names in `$PATH`\n\n## [2.6.3] - 2019-01-19\n\n- **expand**\n  - Support globs with path prefixes and suffixes, like `./foo/*/`\n  - Don't error when skipping non-directories in glob walks\n\n## [2.6.2] - 2018-12-08\n\n- **syntax**\n  - Avoid premature reads in `Parser.Interactive` when parsing Unicode bytes\n  - Fix parsing of certain Bash test expression involving newlines\n  - `Redirect.End` now takes the `Hdoc` field into account\n  - `ValidName` now returns `false` for an empty string\n- **expand**\n  - Environment variables on Windows are case insensitive again\n- **interp**\n  - Don't crash on `declare $unset=foo`\n  - Fix a regression where executed programs would receive a broken environment\n\nNote that the published Docker image was changed to set `shfmt` as the\nentrypoint, so previous uses with arguments like `docker run mvdan/shfmt:v2.6.1\nshfmt --version` should now be `docker run mvdan/shfmt:v2.6.2 --version`.\n\n## [2.6.1] - 2018-11-17\n\n- **syntax**\n  - Fix `Parser.Incomplete` with some incomplete literals\n  - Fix parsing of Bash regex tests in some edge cases\n- **interp**\n  - Add support for `$(<file)` special command substitutions\n\n## [2.6.0] - 2018-11-10\n\nThis is the biggest v2 release to date. It's now possible to write an\ninteractive shell, and it's easier and safer to perform shell expansions.\n\nThis will be the last major v2 version, to allow converting the project to a Go\nmodule in v3.\n\n- Go 1.10 or later required to build\n- **syntax**\n  - Add `Parser.Interactive` to implement an interactive shell\n  - Add `Parser.Document` to parse a single here-document body\n  - Add `Parser.Words` to incrementally parse separate words\n  - Add the `Word.Lit` helper method\n  - Support custom indentation in `<<-` heredoc bodies\n- **interp**\n  - Stabilize API and add some examples\n  - Introduce a constructor, and redesign `Runner.Reset`\n  - Move the context from a field to function parameters\n  - Remove `Runner.Stmt` in favor of `Run` with `ShellExitStatus`\n- **shell**\n  - Stabilize API and add some examples\n  - Add `Expand`, as a more powerful `os.Expand`\n  - Add `Fields`, similar to the old `Runner.Fields`\n  - `Source*` functions now take a context\n  - `Source*` functions no longer try to sandbox\n- **expand**\n  - New package, split from `interp`\n  - Allows performing shell expansions in a controlled way\n  - Redesigned `Environ` and `Variable` moved from `interp`\n\n## [2.5.1] - 2018-08-03\n\n- **syntax**\n  - Fix a regression where semicolons would disappear within switch cases\n\n## [2.5.0] - 2018-07-13\n\n- **syntax**\n  - Add support for Bash's `{varname}<` redirects\n  - Add `SpaceRedirects` to format redirects like `> word`\n  - Parse `$\\\"` correctly within double quotes\n  - A few fixes where minification would break programs\n  - Printing of heredocs within `<()` no longer breaks them\n  - Printing of single statements no longer adds empty lines\n  - Error on invalid parameter names like `${1a}`\n- **interp**\n  - `Runner.Dir` is now always an absolute path\n- **shell**\n  - `Expand` now supports expanding a lone `~`\n  - `Expand` and `SourceNode` now have default timeouts\n- **cmd/shfmt**\n  - Add `-sr` to print spaces after redirect operators\n  - Don't skip empty string values in `-tojson`\n  - Include comment positions in `-tojson`\n\n## [2.4.0] - 2018-05-16\n\n- Publish as a JS package, [mvdan-sh](https://www.npmjs.com/package/mvdan-sh)\n- **syntax**\n  - Add `DebugPrint` to pretty-print a syntax tree\n  - Fix comment parsing and printing in some edge cases\n  - Indent `<<-` heredoc bodies if indenting with tabs\n  - Add support for nested backquotes\n  - Relax parser to allow quotes in arithmetic expressions\n  - Don't rewrite `declare foo=` into `declare foo`\n- **interp**\n  - Add support for `shopt -s globstar`\n  - Replace `Runner.Env` with an interface\n- **shell**\n  - Add `Expand` as a fully featured version of `os.Expand`\n- **cmd/shfmt**\n  - Set appropriate exit status when `-d` is used\n\n## [2.3.0] - 2018-03-07\n\n- **syntax**\n  - Case clause patterns are no longer forced on a single line\n  - Add `ExpandBraces`, to perform Bash brace expansion on words\n  - Improve the handling of backslashes within backquotes\n  - Improve the parsing of Bash test regexes\n- **interp**\n  - Support `$DIRSTACK`, `${param[@]#word}`, and `${param,word}`\n- **cmd/shfmt**\n  - Add `-d`, to display diffs when formatting differs\n  - Promote `-exp.tojson` to `-tojson`\n  - Add `Pos` and `End` fields to nodes in `-tojson`\n  - Inline `StmtList` fields to simplify the `-tojson` output\n  - Support `-l` on standard input\n\n## [2.2.1] - 2018-01-25\n\n- **syntax**\n  - Don't error on `${1:-default}`\n  - Allow single quotes in `${x['str key']}` as well as double quotes\n  - Add support for `${!foo[@]}`\n  - Don't simplify `foo[$x]` to `foo[x]`, to not break string indexes\n  - Fix `Stmt.End` when the end token is the background operator `&`\n  - Never apply the negation operator `!` to `&&` and `||` lists\n  - Apply the background operator `&` to entire `&&` and `||` lists\n  - Fix `StopAt` when the stop string is at the beginning of the source\n  - In `N>word`, check that `N` is a valid numeric literal\n  - Fix a couple of crashers found via fuzzing\n- **cmd/shfmt**\n  - Don't error if non-bash files can't be written to\n\n## [2.2.0] - 2018-01-18\n\n- Tests on Mac and Windows are now ran as part of CI\n- **syntax**\n  - Add `StopAt` to stop lexing at a custom arbitrary token\n  - Add `TranslatePattern` and `QuotePattern` for pattern matching\n  - Minification support added to the printer - see `Minify`\n  - Add ParamExp.Names to represent `${!prefix*}`\n  - Add TimeClause.PosixFormat for its `-p` flag\n  - Fix parsing of assignment values containing `=`\n  - Fix parsing of parameter expansions followed by a backslash\n  - Fix quotes in parameter expansion operators like `${v:-'def'}`\n  - Fix parsing of negated declare attributes like `declare +x name`\n  - Fix parsing of `${#@}`\n  - Reject bad parameter expansion operators like `${v@WRONG}`\n  - Reject inline array variables like `a=(b c) prog`\n  - Reject indexing of special vars like `${1[3]}`\n  - Reject `${!name}` when in POSIX mode\n  - Reject multiple parameter expansion actions like `${#v:-def}`\n- **interp**\n  - Add Bash brace expansion support, including `{a,b}` and `{x..y}`\n  - Pattern matching actions are more correct and precise\n  - Exported some Runner internals, including `Vars` and `Funcs`\n  - Use the interpreter's `$PATH` to find binaries\n  - Roll our own globbing to use our own pattern matching code\n  - Support the `getopts` sh builtin\n  - Support the `read` bash builtin\n  - Numerous changes to improve Windows support\n- **shell**\n  - New experimental package with high-level utility functions\n  - Add `SourceFile` to get the variables declared in a script\n  - Add `SourceNode` as a lower-level version of the above\n- **cmd/shfmt**\n  - Add `-mn`, which minifies programs via `syntax.Minify`\n\n## [2.1.0] - 2017-11-25\n\n- **syntax**\n  - Add `Stmts`, to parse one statement at a time\n  - Walk no longer ignores comments\n  - Parameter expansion end fixes, such as `$foo.bar`\n  - Whitespace alignment can now be kept - see `KeepPadding`\n  - Introduce an internal newline token to simplify the parser\n  - Fix `Block.Pos` to actually return the start position\n  - Fix mishandling of inline comments in two edge cases\n- **interp**\n  - Expose `Fields` to expand words into strings\n  - First configurable modules - cmds and files\n  - Add support for the new `TimeClause`\n  - Add support for namerefs and readonly vars\n  - Add support for associative arrays (maps)\n  - More sh builtins: `exec return`\n  - More bash builtins: `command pushd popd dirs`\n  - More `test` operators: `-b -c -t -o`\n  - Configurable kill handling - see `KillTimeout`\n- **cmd/shfmt**\n  - Add `-f` to just list all the shell files found\n  - Add `-kp` to keep the column offsets in place\n- **cmd/gosh**\n  - Now supports a basic interactive mode\n\n## [2.0.0] - 2017-08-30\n\n- The package import paths were moved to `mvdan.cc/sh/...`\n- **syntax**\n  - Parser and Printer structs introduced with functional options\n  - Node positions are now independent - `Position` merged into `Pos`\n  - All comments are now attached to nodes\n  - Support `mksh` - MirBSD's Korn Shell, used in Android\n  - Various changes to the AST:\n    - `EvalClause` removed; `eval` is no longer parsed as a keyword\n    - Add support for Bash's `time` and `select`\n    - Merge `UntilClause` into `WhileClause`\n    - Moved `Stmt.Assigns` to `CallExpr.Assigns`\n    - Remove `Elif` - chain `IfClause` nodes instead\n  - Support for indexed assignments like `a[i]=b`\n  - Allow expansions in arithmetic expressions again\n  - Unclosed heredocs now produce an error\n  - Binary ops are kept in the same line - see `BinaryNextLine`\n  - Switch cases are not indented by default - see `SwitchCaseIndent`\n- **cmd/shfmt**\n  - Add `-s`, which simplifies programs via `syntax.Simplify`\n  - Add `-ln <lang>`, like `-ln mksh`\n  - Add `-bn` to put binary ops in the next line, like in v1\n  - Add `-ci` to indent switch cases, like in v1\n- **interp**\n  - Some progress made, though still experimental\n  - Most of POSIX done - some builtins remain to be done\n\n## [1.3.1] - 2017-05-26\n\n- **syntax**\n  - Fix parsing of `${foo[$bar]}`\n  - Fix printer regression where `> >(foo)` would be turned into `>>(foo)`\n  - Break comment alignment on any line without a comment, fixing formatting issues\n  - Error on keywords like `fi` and `done` used as commands\n\n## [1.3.0] - 2017-04-24\n\n- **syntax**\n  - Fix backslashes in backquote command substitutions\n  - Disallow some test expressions like `[[ a == ! b ]]`\n  - Disallow some parameter expansions like `${$foo}`\n  - Disallow some arithmetic expressions like `((1=3))` and `(($(echo 1 + 2)))`\n  - Binary commands like `&&`, `||` and pipes are now left-associative\n- **fileutil**\n  - `CouldBeScript` may now return true on non-regular files such as symlinks\n- **interp**\n  - New experimental package to interpret a `syntax.File` in pure Go\n\n## [1.2.0] - 2017-02-22\n\n- **syntax**\n  - Add support for escaped characters in bash regular expressions\n- **fileutil**\n  - New package with some code moved from `cmd/shfmt`, now importable\n  - New funcs `HasShebang` and `CouldBeScript`\n  - Require shebangs to end with whitespace to reject `#!/bin/shfoo`\n\n## [1.1.0] - 2017-01-05\n\n- **syntax**\n  - Parse `[[ a = b ]]` like `[[ a == b ]]`, deprecating `TsAssgn` in favour of `TsEqual`\n  - Add support for the `-k`, `-G`, `-O` and `-N` unary operators inside `[[ ]]`\n  - Add proper support for `!` in parameter expansions, like `${!foo}`\n  - Fix a couple of crashes found via fuzzing\n- **cmd/shfmt**\n  - Rewrite `[[ a = b ]]` into the saner `[[ a == b ]]` (see above)\n\n## [1.0.0] - 2016-12-13\n\n- **syntax**\n  - Stable release, API now frozen\n  - `Parse` now reads input in chunks of 1KiB\n- **cmd/shfmt**\n  - Add `-version` flag\n\n## [0.6.0] - 2016-12-05\n\n- **syntax**\n  - `Parse` now takes an `io.Reader` instead of `[]byte`\n  - Invalid UTF-8 is now reported as an error\n  - Remove backtracking for `$((` and `((`\n  - `Walk` now takes a func literal to simplify its use\n\n## [0.5.0] - 2016-11-24\n\n- **cmd/shfmt**\n  - Remove `-cpuprofile`\n  - Don't read entire files into memory to check for a shebang\n- **syntax**\n  - Use `uint32` for tokens and positions in nodes\n  - Use `Word` and `Lit` pointers consistently instead of values\n  - Ensure `Word.Parts` is never empty\n  - Add support for expressions in array indexing and parameter expansion slicing\n\n## [0.4.0] - 2016-11-08\n\n- Merge `parser`, `ast`, `token` and `printer` into a single package `syntax`\n- Use separate operator types in nodes rather than `Token`\n- Use operator value names that express their function\n- Keep `;` if on a separate line when formatting\n- **cmd/shfmt**\n  - Allow whitespace after `#!` in a shebang\n- **syntax**\n  - Implement operator precedence for `[[ ]]`\n  - Parse `$(foo)` and ``foo`` as the same (`shfmt` then converts the latter to the former)\n  - Rename `Quoted` to `DblQuoted` for clarity\n  - Split `((foo))` nodes as their own type, `ArithmCmd`\n  - Add support for bash parameter expansion slicing\n\n## [0.3.0] - 2016-10-26\n\n- Add support for bash's `coproc` and extended globbing like `@(foo)`\n- Improve test coverage, adding tests to `cmd/shfmt` and bringing `parser` and `printer` close to 100%\n- Support empty C-style for loops like `for ((;;)) ...`\n- Support for the `>|` redirect operand\n- **cmd/shfmt**\n  - Fix issue where `.sh` and `.bash` files might not be walked if running on a directory\n  - Fix issue where `-p` was not obeyed when formatting stdin\n- **parser**\n  - `$''` now generates an `ast.SglQuoted`, not an `ast.Quoted`\n  - Support for ambiguous `((` like with `$((`\n  - Improve special parameter expansions like `$@` or `$!`\n  - Improve bash's `export` `typeset`, `nameref` and `readonly`\n  - `<>`, `>&` and `<&` are valid POSIX\n  - Support for bash's `^`, `^^`, `,` and `,,` operands inside `${}`\n\n## [0.2.0] - 2016-10-13\n\n- Optimizations all around, making `shfmt` ~15% faster\n- **cmd/shfmt**\n  - Add `-p` flag to only accept POSIX Shell programs (`parser.PosixConformant`)\n- **parser**\n  - Add support for ambiguous `$((` as in `$((foo) | bar)`\n  - Limit more bash features to `PosixConformant` being false\n  - Don't parse heredoc bodies in nested expansions and contexts\n  - Run tests through `bash` to confirm the presence of a parse error\n- **ast**\n  - Add `Walk(Visitor, Node)` function\n\n## [0.1.0] - 2016-09-20\n\nInitial release.\n\n[3.12.0]: https://github.com/mvdan/sh/releases/tag/v3.12.0\n[3.11.0]: https://github.com/mvdan/sh/releases/tag/v3.11.0\n[3.10.0]: https://github.com/mvdan/sh/releases/tag/v3.10.0\n[3.9.0]: https://github.com/mvdan/sh/releases/tag/v3.9.0\n[3.8.0]: https://github.com/mvdan/sh/releases/tag/v3.8.0\n[3.7.0]: https://github.com/mvdan/sh/releases/tag/v3.7.0\n\n[3.6.0]: https://github.com/mvdan/sh/releases/tag/v3.6.0\n[#779]: https://github.com/mvdan/sh/issues/779\n[#803]: https://github.com/mvdan/sh/issues/803\n[#814]: https://github.com/mvdan/sh/issues/814\n[#835]: https://github.com/mvdan/sh/issues/835\n[#841]: https://github.com/mvdan/sh/issues/841\n[#849]: https://github.com/mvdan/sh/pull/849\n[#863]: https://github.com/mvdan/sh/pull/863\n[#866]: https://github.com/mvdan/sh/pull/866\n[#873]: https://github.com/mvdan/sh/issues/873\n[#877]: https://github.com/mvdan/sh/issues/877\n[#879]: https://github.com/mvdan/sh/pull/879\n[#881]: https://github.com/mvdan/sh/issues/881\n[#884]: https://github.com/mvdan/sh/issues/884\n[#885]: https://github.com/mvdan/sh/issues/885\n[#886]: https://github.com/mvdan/sh/issues/886\n[#892]: https://github.com/mvdan/sh/issues/892\n[#900]: https://github.com/mvdan/sh/pull/900\n[#917]: https://github.com/mvdan/sh/pull/917\n[#928]: https://github.com/mvdan/sh/issues/928\n[#929]: https://github.com/mvdan/sh/pull/929\n[#955]: https://github.com/mvdan/sh/pull/955\n\n[3.5.1]: https://github.com/mvdan/sh/releases/tag/v3.5.1\n[#860]: https://github.com/mvdan/sh/issues/860\n[#861]: https://github.com/mvdan/sh/pull/861\n[#862]: https://github.com/mvdan/sh/pull/862\n\n[3.5.0]: https://github.com/mvdan/sh/releases/tag/v3.5.0\n[3.4.3]: https://github.com/mvdan/sh/releases/tag/v3.4.3\n[3.4.2]: https://github.com/mvdan/sh/releases/tag/v3.4.2\n[3.4.1]: https://github.com/mvdan/sh/releases/tag/v3.4.1\n[3.4.0]: https://github.com/mvdan/sh/releases/tag/v3.4.0\n[3.3.1]: https://github.com/mvdan/sh/releases/tag/v3.3.1\n[3.3.0]: https://github.com/mvdan/sh/releases/tag/v3.3.0\n[3.2.4]: https://github.com/mvdan/sh/releases/tag/v3.2.4\n[3.2.2]: https://github.com/mvdan/sh/releases/tag/v3.2.2\n[3.2.1]: https://github.com/mvdan/sh/releases/tag/v3.2.1\n[3.2.0]: https://github.com/mvdan/sh/releases/tag/v3.2.0\n[3.1.2]: https://github.com/mvdan/sh/releases/tag/v3.1.2\n[3.1.1]: https://github.com/mvdan/sh/releases/tag/v3.1.1\n[3.1.0]: https://github.com/mvdan/sh/releases/tag/v3.1.0\n[3.0.2]: https://github.com/mvdan/sh/releases/tag/v3.0.2\n[3.0.1]: https://github.com/mvdan/sh/releases/tag/v3.0.1\n[3.0.0]: https://github.com/mvdan/sh/releases/tag/v3.0.0\n[2.6.4]: https://github.com/mvdan/sh/releases/tag/v2.6.4\n[2.6.3]: https://github.com/mvdan/sh/releases/tag/v2.6.3\n[2.6.2]: https://github.com/mvdan/sh/releases/tag/v2.6.2\n[2.6.1]: https://github.com/mvdan/sh/releases/tag/v2.6.1\n[2.6.0]: https://github.com/mvdan/sh/releases/tag/v2.6.0\n[2.5.1]: https://github.com/mvdan/sh/releases/tag/v2.5.1\n[2.5.0]: https://github.com/mvdan/sh/releases/tag/v2.5.0\n[2.4.0]: https://github.com/mvdan/sh/releases/tag/v2.4.0\n[2.3.0]: https://github.com/mvdan/sh/releases/tag/v2.3.0\n[2.2.1]: https://github.com/mvdan/sh/releases/tag/v2.2.1\n[2.2.0]: https://github.com/mvdan/sh/releases/tag/v2.2.0\n[2.1.0]: https://github.com/mvdan/sh/releases/tag/v2.1.0\n[2.0.0]: https://github.com/mvdan/sh/releases/tag/v2.0.0\n[1.3.1]: https://github.com/mvdan/sh/releases/tag/v1.3.1\n[1.3.0]: https://github.com/mvdan/sh/releases/tag/v1.3.0\n[1.2.0]: https://github.com/mvdan/sh/releases/tag/v1.2.0\n[1.1.0]: https://github.com/mvdan/sh/releases/tag/v1.1.0\n[1.0.0]: https://github.com/mvdan/sh/releases/tag/v1.0.0\n[0.6.0]: https://github.com/mvdan/sh/releases/tag/v0.6.0\n[0.5.0]: https://github.com/mvdan/sh/releases/tag/v0.5.0\n[0.4.0]: https://github.com/mvdan/sh/releases/tag/v0.4.0\n[0.3.0]: https://github.com/mvdan/sh/releases/tag/v0.3.0\n[0.2.0]: https://github.com/mvdan/sh/releases/tag/v0.2.0\n[0.1.0]: https://github.com/mvdan/sh/releases/tag/v0.1.0\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2016, Daniel Martí. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of the copyright holder nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# sh\n\n[![Go Reference](https://pkg.go.dev/badge/mvdan.cc/sh/v3.svg)](https://pkg.go.dev/mvdan.cc/sh/v3)\n\nA shell parser, formatter, and interpreter.\nSupports [POSIX Shell], [Bash], [Zsh], and [mksh]. Requires Go 1.25 or later.\n\n### Quick start\n\nTo parse shell scripts, inspect them, and print them out,\nsee the [syntax package](https://pkg.go.dev/mvdan.cc/sh/v3/syntax).\n\nFor high-level operations like performing shell expansions on strings,\nsee the [shell package](https://pkg.go.dev/mvdan.cc/sh/v3/shell).\n\nTo interpret or run shell scripts,\nsee the [interp package](https://pkg.go.dev/mvdan.cc/sh/v3/interp).\n\n### shfmt\n\n\tgo install mvdan.cc/sh/v3/cmd/shfmt@latest\n\n`shfmt` formats shell programs. See [canonical.sh](syntax/canonical.sh) for a\nquick look at its default style. For example:\n\n\tshfmt -l -w script.sh\n\nFor more information, see [its manpage](cmd/shfmt/shfmt.1.scd), which can be\nviewed directly as Markdown or rendered with [scdoc].\n\nPackages are available on [Alpine], [Arch], [Debian], [Docker], [Fedora], [FreeBSD],\n[Homebrew], [MacPorts], [NixOS], [OpenSUSE], [Scoop], [Snapcraft], [Void] and [webi].\n\n### gosh\n\n\tgo install mvdan.cc/sh/v3/cmd/gosh@latest\n\nProof of concept shell that uses the `interp` package.\n\n### Fuzzing\n\nWe use Go's native fuzzing support. For instance:\n\n\tcd syntax\n\tgo test -run=- -fuzz=ParsePrint\n\n### Caveats\n\n* When indexing Bash associative arrays, always use quotes. The static parser\n  will otherwise have to assume that the index is an arithmetic expression.\n\n```sh\n$ echo '${array[spaced string]}' | shfmt\n<standard input>:1:16: not a valid arithmetic operator: string\n$ echo '${array[weird!key]}' | shfmt\n<standard input>:1:8: reached ! without matching [ with ]\n$ echo '${array[dash-string]}' | shfmt\n${array[dash - string]}\n```\n\n* `$((` and `((` ambiguity is not supported. Backtracking would complicate the\n  parser and make streaming support via `io.Reader` impossible. The POSIX spec\n  recommends to [space the operands][posix-ambiguity] if `$( (` is meant.\n\n```sh\n$ echo '$((foo); (bar))' | shfmt\n1:1: reached ) without matching $(( with ))\n```\n\n* `export`, `let`, and `declare` are parsed as keywords.\n  This allows statically building their syntax tree,\n  as opposed to keeping the arguments as a slice of words.\n  It is also required to support `declare foo=(bar)`.\n\n* The entire library is written in pure Go, which limits how closely the\n  interpreter can follow POSIX Shell and Bash semantics.\n  For example, Go does not support forking its own process, so subshells\n  use a goroutine instead, meaning that real PIDs and file descriptors\n  cannot be used directly.\n\n### Formatting FAQs\n\n* The formatter cannot be disabled for ranges of lines; most users wanting this\n  are working around a bug or they don't like how a piece of code is formatted.\n  Instead, search the issue tracker and file a new issue if necessary.\n  Formatting of partial files leads to lots of edge cases and complexity\n  which this project has no resources for, nor interest in, getting into.\n\n* We avoid adding more formatting options where possible. Each added flag interacts\n  with all others, multiplying the human cost of development, maintenance, testing,\n  and properly documenting the behavior for end users.\n\n* The true value in a formatter is consistency, especially for teams of developers.\n  We do not aim to satisfy every developer's personal choice of optimal formatting.\n\n### JavaScript\n\nThe parser and formatter are available as a third party npm package called [sh-syntax],\nwhich bundles a version of this library compiled to WASM.\n\nPreviously, we maintained an npm package called [mvdan-sh] which used GopherJS\nto bundle a JS version of this library. That npm package is now archived\ngiven its poor performance and GopherJS not being as actively developed.\nAny existing or new users should look at [sh-syntax] instead.\n\n### Docker\n\nAll release tags are published via [Docker], such as `v3.5.1`.\nThe latest stable release is currently published as `v3`,\nand the latest development version as `latest`.\nThe images only include `shfmt`; `-alpine` variants exist on Alpine Linux.\n\nTo build a Docker image, run:\n\n\tdocker build -t my:tag -f cmd/shfmt/Dockerfile .\n\nTo use a Docker image, run:\n\n\tdocker run --rm -u \"$(id -u):$(id -g)\" -v \"$PWD:/mnt\" -w /mnt my:tag <shfmt arguments>\n\n### Related projects\n\nThe following editor integrations wrap `shfmt`:\n\n- [BashSupport-Pro] - Bash plugin for JetBrains IDEs\n- [dockerfmt] - Dockerfile formatter using shfmt\n- [intellij-shellscript] - Intellij Jetbrains `shell script` plugin\n- [micro] - Editor with a built-in plugin\n- [neoformat] - (Neo)Vim plugin\n- [shell-format] - VS Code plugin\n- [vscode-shfmt] - VS Code plugin\n- [shfmt.el] - Emacs package\n- [Sublime-Pretty-Shell] - Sublime Text 3 plugin\n- [Trunk] - Universal linter, available as a CLI, VS Code plugin, and GitHub action\n- [vim-shfmt] - Vim plugin\n\nOther noteworthy integrations include:\n\n- [modd] - A developer tool that responds to filesystem changes\n- [prettier-plugin-sh] - [Prettier] plugin using [sh-syntax]\n- [sh-checker] - A GitHub Action that performs static analysis for shell scripts\n- [mdformat-shfmt] - [mdformat] plugin to format shell scripts embedded in Markdown with shfmt\n- [pre-commit-shfmt] - [pre-commit] shfmt hook\n- [tesh] - Run scripts with mocks, assertions, and coverage\n\n[alpine]: https://pkgs.alpinelinux.org/packages?name=shfmt\n[arch]: https://archlinux.org/packages/extra/x86_64/shfmt/\n[bash]: https://www.gnu.org/software/bash/\n[BashSupport-Pro]: https://www.bashsupport.com/manual/editor/formatter/\n[debian]: https://tracker.debian.org/pkg/golang-mvdan-sh\n[docker]: https://hub.docker.com/r/mvdan/shfmt/\n[dockerfmt]: https://github.com/reteps/dockerfmt\n[editorconfig]: https://editorconfig.org/\n[examples]: https://pkg.go.dev/mvdan.cc/sh/v3/syntax#pkg-examples\n[fedora]: https://packages.fedoraproject.org/pkgs/golang-mvdan-sh-3/shfmt/\n[freebsd]: https://www.freshports.org/devel/shfmt\n[homebrew]: https://formulae.brew.sh/formula/shfmt\n[intellij-shellscript]: https://www.jetbrains.com/help/idea/shell-scripts.html\n[macports]: https://ports.macports.org/port/shfmt/details/\n[mdformat-shfmt]: https://github.com/hukkin/mdformat-shfmt\n[mdformat]: https://github.com/executablebooks/mdformat\n[micro]: https://micro-editor.github.io/\n[mksh]: http://www.mirbsd.org/mksh.htm\n[modd]: https://github.com/cortesi/modd\n[mvdan-sh]: https://www.npmjs.com/package/mvdan-sh\n[neoformat]: https://github.com/sbdchd/neoformat\n[nixos]: https://github.com/NixOS/nixpkgs/blob/HEAD/pkgs/tools/text/shfmt/default.nix\n[OpenSUSE]: https://build.opensuse.org/package/show/openSUSE:Factory/shfmt\n[posix shell]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html\n[posix-ambiguity]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_03\n[pre-commit]: https://pre-commit.com\n[pre-commit-shfmt]: https://github.com/scop/pre-commit-shfmt\n[prettier-plugin-sh]: https://github.com/un-ts/prettier/tree/master/packages/sh\n[prettier]: https://prettier.io\n[scdoc]: https://sr.ht/~sircmpwn/scdoc/\n[scoop]: https://github.com/ScoopInstaller/Main/blob/HEAD/bucket/shfmt.json\n[sh-checker]: https://github.com/luizm/action-sh-checker\n[sh-syntax]: https://github.com/un-ts/sh-syntax\n[shell-format]: https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format\n[shfmt.el]: https://github.com/purcell/emacs-shfmt/\n[snapcraft]: https://snapcraft.io/shfmt\n[sublime-pretty-shell]: https://github.com/aerobounce/Sublime-Pretty-Shell\n[tesh]: https://github.com/feloy/tesh\n[trunk]: https://trunk.io/check\n[vim-shfmt]: https://github.com/z0mbix/vim-shfmt\n[void]: https://github.com/void-linux/void-packages/blob/HEAD/srcpkgs/shfmt/template\n[vscode-shfmt]: https://marketplace.visualstudio.com/items?itemName=mkhl.shfmt\n[webi]: https://webinstall.dev/shfmt/\n[Zsh]: https://www.zsh.org/\n"
  },
  {
    "path": "cmd/gosh/main.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// gosh is a proof of concept shell built on top of [interp].\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"golang.org/x/term\"\n\n\t\"mvdan.cc/sh/v3/interp\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nvar command = flag.String(\"c\", \"\", \"command to be executed\")\n\nfunc main() {\n\tflag.Parse()\n\terr := runAll()\n\tvar es interp.ExitStatus\n\tif errors.As(err, &es) {\n\t\tos.Exit(int(es))\n\t}\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc runAll() error {\n\tr, err := interp.New(interp.Interactive(true), interp.StdIO(os.Stdin, os.Stdout, os.Stderr))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif *command != \"\" {\n\t\treturn run(r, strings.NewReader(*command), \"\")\n\t}\n\tif flag.NArg() == 0 {\n\t\tif term.IsTerminal(int(os.Stdin.Fd())) {\n\t\t\treturn runInteractive(r, os.Stdin, os.Stdout, os.Stderr)\n\t\t}\n\t\treturn run(r, os.Stdin, \"\")\n\t}\n\tfor _, path := range flag.Args() {\n\t\tif err := runPath(r, path); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc run(r *interp.Runner, reader io.Reader, name string) error {\n\tprog, err := syntax.NewParser().Parse(reader, name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tr.Reset()\n\tctx := context.Background()\n\treturn r.Run(ctx, prog)\n}\n\nfunc runPath(r *interp.Runner, path string) error {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\treturn run(r, f, path)\n}\n\nfunc runInteractive(r *interp.Runner, stdin io.Reader, stdout, stderr io.Writer) error {\n\tparser := syntax.NewParser()\n\tfmt.Fprintf(stdout, \"$ \")\n\tfor stmts, err := range parser.InteractiveSeq(stdin) {\n\t\tif err != nil {\n\t\t\treturn err // stop at the first error\n\t\t}\n\t\tif parser.Incomplete() {\n\t\t\tfmt.Fprintf(stdout, \"> \")\n\t\t\tcontinue\n\t\t}\n\t\tctx := context.Background()\n\t\tfor _, stmt := range stmts {\n\t\t\terr := r.Run(ctx, stmt)\n\t\t\tif r.Exited() {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfmt.Fprintf(stdout, \"$ \")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/gosh/main_test.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-quicktest/qt\"\n\t\"mvdan.cc/sh/v3/interp\"\n)\n\n// Each test has an even number of strings, which form input-output pairs for\n// the interactive shell. The input string is fed to the interactive shell, and\n// bytes are read from its output until the expected output string is matched or\n// an error is encountered.\n//\n// In other words, each first string is what the user types, and each following\n// string is what the shell will print back. Note that the first \"$ \" output is\n// implicit.\n\nvar interactiveTests = []struct {\n\tpairs   []string\n\twantErr string\n}{\n\t{},\n\t{\n\t\tpairs: []string{\n\t\t\t\"\\n\",\n\t\t\t\"$ \",\n\t\t\t\"\\n\",\n\t\t\t\"$ \",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo\\n\",\n\t\t\t\"foo\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo\\n\",\n\t\t\t\"foo\\n$ \",\n\t\t\t\"echo bar\\n\",\n\t\t\t\"bar\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"if true\\n\",\n\t\t\t\"> \",\n\t\t\t\"then echo bar; fi\\n\",\n\t\t\t\"bar\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo 'foo\\n\",\n\t\t\t\"> \",\n\t\t\t\"bar'\\n\",\n\t\t\t\"foo\\nbar\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo; echo bar\\n\",\n\t\t\t\"foo\\nbar\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo; echo 'bar\\n\",\n\t\t\t\"> \",\n\t\t\t\"baz'\\n\",\n\t\t\t\"foo\\nbar\\nbaz\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"(\\n\",\n\t\t\t\"> \",\n\t\t\t\"echo foo)\\n\",\n\t\t\t\"foo\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"[[\\n\",\n\t\t\t\"> \",\n\t\t\t\"true ]]\\n\",\n\t\t\t\"$ \",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo ||\\n\",\n\t\t\t\"> \",\n\t\t\t\"echo bar\\n\",\n\t\t\t\"foo\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo |\\n\",\n\t\t\t\"> \",\n\t\t\t\"read var; echo $var\\n\",\n\t\t\t\"foo\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo\",\n\t\t\t\"\",\n\t\t\t\" bar\\n\",\n\t\t\t\"foo bar\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo\\\\\\n\",\n\t\t\t\"> \",\n\t\t\t\" foo\\n\",\n\t\t\t\"foo\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo\\\\\\n\",\n\t\t\t\"> \",\n\t\t\t\"bar\\n\",\n\t\t\t\"foobar\\n\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo 你好\\n\",\n\t\t\t\"你好\\n$ \",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo *; :\\n\",\n\t\t\t\"main.go main_test.go\\n$ \",\n\t\t\t\"echo *\\n\",\n\t\t\t\"main.go main_test.go\\n$ \",\n\t\t\t\"shopt -s globstar; echo **\\n\",\n\t\t\t\"main.go main_test.go\\n$ \",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo; exit 0; echo bar\\n\",\n\t\t\t\"foo\\n\",\n\t\t\t\"echo baz\\n\",\n\t\t\t\"\",\n\t\t},\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"echo foo; exit 1; echo bar\\n\",\n\t\t\t\"foo\\n\",\n\t\t\t\"echo baz\\n\",\n\t\t\t\"\",\n\t\t},\n\t\twantErr: \"exit status 1\",\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"(x\\n\",\n\t\t\t\"> \",\n\t\t},\n\t\twantErr: \"1:1: reached EOF without matching `(` with `)`\",\n\t},\n\t{\n\t\tpairs: []string{\n\t\t\t\"gosh_alias arg || true\\n\",\n\t\t\t\"\\\"gosh_alias\\\": executable file not found in $PATH\\n$ \",\n\t\t\t\"alias gosh_alias=echo\\n\",\n\t\t\t\"$ \",\n\t\t\t\"gosh_alias arg || true\\n\",\n\t\t\t\"arg\\n$ \",\n\t\t\t\"unalias gosh_alias\\n\",\n\t\t\t\"$ \",\n\t\t\t\"gosh_alias arg || true\\n\",\n\t\t\t\"\\\"gosh_alias\\\": executable file not found in $PATH\\n$ \",\n\t\t},\n\t},\n}\n\nfunc TestInteractive(t *testing.T) {\n\tt.Parallel()\n\tfor _, tc := range interactiveTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tinReader, inWriter, err := os.Pipe()\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\toutReader, outWriter, err := os.Pipe()\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\trunner, _ := interp.New(interp.Interactive(true), interp.StdIO(inReader, outWriter, outWriter))\n\t\t\terrc := make(chan error, 1)\n\t\t\tgo func() {\n\t\t\t\terrc <- runInteractive(runner, inReader, outWriter, outWriter)\n\t\t\t\t// Discard the rest of the input.\n\t\t\t\tio.Copy(io.Discard, inReader)\n\t\t\t\tinReader.Close()\n\t\t\t\toutWriter.Close()\n\t\t\t}()\n\n\t\t\tif err := readString(outReader, \"$ \"); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tline := 1\n\t\t\tfor len(tc.pairs) > 0 {\n\t\t\t\tt.Logf(\"write %q\", tc.pairs[0])\n\t\t\t\tif _, err := io.WriteString(inWriter, tc.pairs[0]); err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tt.Logf(\"read %q\", tc.pairs[1])\n\t\t\t\tif err := readString(outReader, tc.pairs[1]); err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tline++\n\t\t\t\ttc.pairs = tc.pairs[2:]\n\t\t\t}\n\n\t\t\t// Close the input pipe, so that the parser can stop.\n\t\t\tinWriter.Close()\n\n\t\t\t// Once the input pipe is closed, close the output pipe\n\t\t\t// so that any remaining prompt writes get discarded.\n\t\t\toutReader.Close()\n\n\t\t\terr = <-errc\n\t\t\tif err != nil && tc.wantErr == \"\" {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t} else if tc.wantErr != \"\" && fmt.Sprint(err) != tc.wantErr {\n\t\t\t\tt.Fatalf(\"want error %q, got: %v\", tc.wantErr, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInteractiveExit(t *testing.T) {\n\tinReader, inWriter, err := os.Pipe()\n\tqt.Assert(t, qt.IsNil(err))\n\tdefer inReader.Close()\n\tgo func() {\n\t\tio.WriteString(inWriter, \"exit\\n\")\n\t\tinWriter.Close()\n\t}()\n\tw := io.Discard\n\trunner, _ := interp.New(interp.Interactive(true), interp.StdIO(inReader, w, w))\n\tif err := runInteractive(runner, inReader, w, w); err != nil {\n\t\tt.Fatal(\"expected a nil error\")\n\t}\n}\n\n// readString will keep reading from a reader until all bytes from the supplied\n// string are read.\nfunc readString(r io.Reader, want string) error {\n\tp := make([]byte, len(want))\n\t_, err := io.ReadFull(r, p)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgot := string(p)\n\tif got != want {\n\t\treturn fmt.Errorf(\"ReadString: read %q, wanted %q\", got, want)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/shfmt/Dockerfile",
    "content": "FROM golang:1.26.1-alpine AS build\n\nWORKDIR /src\nRUN apk add --no-cache git\nCOPY . .\nRUN CGO_ENABLED=0 go build -ldflags \"-w -s -extldflags '-static'\" ./cmd/shfmt\n\nFROM alpine:3.23.3 AS alpine\nCOPY --from=build /src/shfmt /bin/shfmt\nCOPY \"./cmd/shfmt/docker-entrypoint.sh\" \"/init\"\nENTRYPOINT [\"/init\"]\n\nFROM scratch\nCOPY --from=build /src/shfmt /bin/shfmt\nENTRYPOINT [\"/bin/shfmt\"]\nCMD [\"-h\"]\n"
  },
  {
    "path": "cmd/shfmt/docker-entrypoint.sh",
    "content": "#!/bin/sh\n# SPDX-License-Identifier: BSD-3-Clause\n#\n# Copyright (C) 2019 Olliver Schinagl <oliver@schinagl.nl>\n#\n# A beginning user should be able to docker run image bash (or sh) without\n# needing to learn about --entrypoint\n# https://github.com/docker-library/official-images#consistency\n\nset -eu\n\n# run command if it is not starting with a \"-\" and is an executable in PATH\nif [ \"${#}\" -gt 0 ] &&\n\t[ \"${1#-}\" = \"${1}\" ] &&\n\tcommand -v \"${1}\" >\"/dev/null\" 2>&1; then\n\texec \"${@}\"\nelse\n\t# else default to run the command\n\texec /bin/shfmt \"${@}\"\nfi\n\nexit 0\n"
  },
  {
    "path": "cmd/shfmt/main.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// shfmt formats shell programs.\npackage main\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime/debug\"\n\t\"strings\"\n\n\tmaybeio \"github.com/google/renameio/v2/maybe\"\n\tdiffpkg \"github.com/rogpeppe/go-internal/diff\"\n\t\"golang.org/x/term\"\n\t\"mvdan.cc/editorconfig\"\n\n\t\"mvdan.cc/sh/v3/fileutil\"\n\t\"mvdan.cc/sh/v3/syntax\"\n\t\"mvdan.cc/sh/v3/syntax/typedjson\"\n)\n\ntype boolStringValue string\n\nfunc (b *boolStringValue) Set(val string) error {\n\t*b = boolStringValue(val)\n\treturn nil\n}\nfunc (b *boolStringValue) String() string {\n\treturn string(*b)\n}\nfunc (*boolStringValue) IsBoolFlag() bool { return true }\n\nfunc boolStringVar(p *string, name string, value string, usage string) {\n\t*p = value\n\tflag.Var((*boolStringValue)(p), name, usage)\n}\n\nfunc langVariantVar(p *syntax.LangVariant, name string, value syntax.LangVariant, usage string) {\n\t*p = value\n\tflag.Var(p, name, usage)\n}\n\ntype multiFlag[T any] struct {\n\tshort, long string\n\tval         T\n}\n\nfunc flagVal[T any](short, long string, val T, register func(*T, string, T, string)) *multiFlag[T] {\n\tf := &multiFlag[T]{short, long, val}\n\tif short != \"\" {\n\t\tregister(&f.val, short, val, \"\")\n\t}\n\tif long != \"\" {\n\t\tregister(&f.val, long, val, \"\")\n\t}\n\treturn f\n}\n\nvar (\n\t// Generic flags.\n\tversionFlag = flagVal(\"\", \"version\", false, flag.BoolVar)\n\tlist        = flagVal(\"l\", \"list\", \"false\", boolStringVar)\n\twrite       = flagVal(\"w\", \"write\", false, flag.BoolVar)\n\tdiff        = flagVal(\"d\", \"diff\", false, flag.BoolVar)\n\tapplyIgnore = flagVal(\"\", \"apply-ignore\", false, flag.BoolVar)\n\tfilename    = flagVal(\"\", \"filename\", \"\", flag.StringVar)\n\n\t// Parser flags.\n\tlang     = flagVal(\"ln\", \"language-dialect\", syntax.LangAuto, langVariantVar)\n\tposix    = flagVal(\"p\", \"posix\", false, flag.BoolVar)\n\tsimplify = flagVal(\"s\", \"simplify\", false, flag.BoolVar)\n\t// TODO: when promoting exp.recover to a stable flag, add it as an EditorConfig knob too, and perhaps rename to recover-errors\n\texpRecover = flagVal(\"\", \"exp.recover\", 0, flag.IntVar)\n\n\t// Printer flags.\n\tindent      = flagVal(\"i\", \"indent\", 0, flag.UintVar)\n\tbinNext     = flagVal(\"bn\", \"binary-next-line\", false, flag.BoolVar)\n\tcaseIndent  = flagVal(\"ci\", \"case-indent\", false, flag.BoolVar)\n\tspaceRedirs = flagVal(\"sr\", \"space-redirects\", false, flag.BoolVar)\n\tkeepPadding = flagVal(\"kp\", \"keep-padding\", false, flag.BoolVar)\n\tfuncNext    = flagVal(\"fn\", \"func-next-line\", false, flag.BoolVar)\n\tminify      = flagVal(\"mn\", \"minify\", false, flag.BoolVar)\n\n\t// Utility flags.\n\tfind     = flagVal(\"f\", \"find\", \"false\", boolStringVar)\n\ttoJSON   = flagVal(\"tojson\", \"to-json\", false, flag.BoolVar) // TODO(v4): remove \"tojson\" for consistency\n\tfromJSON = flagVal(\"\", \"from-json\", false, flag.BoolVar)\n\n\t// useEditorConfig will be false if any parser or printer flags were used.\n\tuseEditorConfig = true\n\n\tparser            *syntax.Parser\n\tprinter           *syntax.Printer\n\treadBuf, writeBuf bytes.Buffer\n\tcolor             bool\n\n\tcopyBuf = make([]byte, 32*1024)\n)\n\nfunc main() {\n\tflag.Usage = func() {\n\t\tfmt.Fprint(os.Stderr, `usage: shfmt [flags] [path ...]\n\nshfmt formats shell programs. If the only argument is a dash ('-') or no\narguments are given, standard input will be used. If a given path is a\ndirectory, all shell scripts found under that directory will be used.\n\n  --version  show version and exit\n\n  -l[=0], --list[=0]  error with a list of files whose formatting differs from shfmt;\n                      paths are separated by a newline or a null character if -l=0\n  -w,     --write     write result to file instead of stdout\n  -d,     --diff      error with a diff when the formatting differs\n  --apply-ignore      always apply EditorConfig ignore rules\n  --filename str      provide a name for the standard input file\n\nParser options:\n\n  -ln, --language-dialect str  bash/posix/mksh/bats/zsh, default \"auto\"\n  -p,  --posix                 shorthand for -ln=posix\n  -s,  --simplify              simplify the code\n\nPrinter options:\n\n  -i,  --indent uint       0 for tabs (default), >0 for number of spaces\n  -bn, --binary-next-line  binary ops like && and | may start a line\n  -ci, --case-indent       switch cases will be indented\n  -sr, --space-redirects   redirect operators will be followed by a space\n  -kp, --keep-padding      keep column alignment paddings\n  -fn, --func-next-line    function opening braces are placed on a separate line\n  -mn, --minify             minify the code to reduce its size (implies -s)\n\nUtilities:\n\n  -f[=0], --find[=0]  recursively find all shell files and print the paths;\n                      paths are separated by a newline or a null character if -f=0\n  --to-json           print syntax tree to stdout as a typed JSON\n  --from-json         read syntax tree from stdin as a typed JSON\n\nFormatting options can also be read from EditorConfig files; see 'man shfmt'\nfor a detailed description of the tool's behavior.\nFor more information and to report bugs, see https://github.com/mvdan/sh.\n`)\n\t}\n\tflag.Parse()\n\n\tif versionFlag.val {\n\t\tversion := \"(unknown)\"\n\t\tif info, ok := debug.ReadBuildInfo(); ok {\n\t\t\tmod := &info.Main\n\t\t\tif mod.Replace != nil {\n\t\t\t\tmod = mod.Replace\n\t\t\t}\n\t\t\tversion = mod.Version\n\t\t}\n\t\tfmt.Println(version)\n\t\treturn\n\t}\n\tif posix.val && lang.val != syntax.LangAuto {\n\t\tfmt.Fprintf(os.Stderr, \"-p and -ln=lang cannot coexist\\n\")\n\t\tos.Exit(1)\n\t}\n\tif list.val != \"true\" && list.val != \"false\" && list.val != \"0\" {\n\t\tfmt.Fprintf(os.Stderr, \"only -l and -l=0 allowed\\n\")\n\t\tos.Exit(1)\n\t}\n\tif find.val != \"true\" && find.val != \"false\" && find.val != \"0\" {\n\t\tfmt.Fprintf(os.Stderr, \"only -f and -f=0 allowed\\n\")\n\t\tos.Exit(1)\n\t}\n\tsimplify.val = simplify.val || minify.val\n\tflag.Visit(func(f *flag.Flag) {\n\t\t// This list should be in sync with the grouping of parser and printer options\n\t\t// as shown by ./shfmt.1.scd.\n\t\tswitch f.Name {\n\t\tcase lang.short, lang.long,\n\t\t\tposix.short, posix.long,\n\t\t\tsimplify.short, simplify.long,\n\t\t\tindent.short, indent.long,\n\t\t\tbinNext.short, binNext.long,\n\t\t\tcaseIndent.short, caseIndent.long,\n\t\t\tspaceRedirs.short, spaceRedirs.long,\n\t\t\tkeepPadding.short, keepPadding.long,\n\t\t\tfuncNext.short, funcNext.long,\n\t\t\tminify.short, minify.long:\n\t\t\tuseEditorConfig = false\n\t\t}\n\t})\n\tparser = syntax.NewParser(syntax.KeepComments(true))\n\tprinter = syntax.NewPrinter(syntax.Minify(minify.val))\n\n\tsyntax.RecoverErrors(expRecover.val)(parser)\n\n\tif !useEditorConfig {\n\t\tif posix.val {\n\t\t\t// -p equals -ln=posix\n\t\t\tlang.val = syntax.LangPOSIX\n\t\t}\n\n\t\tsyntax.Indent(indent.val)(printer)\n\t\tsyntax.BinaryNextLine(binNext.val)(printer)\n\t\tsyntax.SwitchCaseIndent(caseIndent.val)(printer)\n\t\tsyntax.SpaceRedirects(spaceRedirs.val)(printer)\n\t\tsyntax.KeepPadding(keepPadding.val)(printer)\n\t\tsyntax.FunctionNextLine(funcNext.val)(printer)\n\t}\n\n\t// Decide whether or not to use color for the diff output,\n\t// as described in shfmt.1.scd.\n\tif os.Getenv(\"FORCE_COLOR\") != \"\" {\n\t\tcolor = true\n\t} else if os.Getenv(\"NO_COLOR\") != \"\" || os.Getenv(\"TERM\") == \"dumb\" {\n\t} else if term.IsTerminal(int(os.Stdout.Fd())) {\n\t\tcolor = true\n\t}\n\t// TODO(v4): show the help text on zero arguments,\n\t// having the user run `shfmt -` if they want to format stdin.\n\t// Using a dash is more explicit, and new users can easily be\n\t// confused by `shfmt` seemingly hanging forever.\n\tif flag.NArg() == 0 || (flag.NArg() == 1 && flag.Arg(0) == \"-\") {\n\t\tname := \"<standard input>\"\n\t\tif toJSON.val {\n\t\t\tname = \"\" // the default is not useful there\n\t\t}\n\t\tif filename.val != \"\" {\n\t\t\tname = filename.val\n\t\t}\n\t\tif err := formatStdin(name); err != nil {\n\t\t\tif err != errFormattingDiffers {\n\t\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\t}\n\t\t\tos.Exit(1)\n\t\t}\n\t\treturn\n\t}\n\tif filename.val != \"\" {\n\t\tfmt.Fprintln(os.Stderr, \"-filename can only be used with stdin\")\n\t\tos.Exit(1)\n\t}\n\tif toJSON.val {\n\t\tfmt.Fprintln(os.Stderr, \"--to-json can only be used with stdin\")\n\t\tos.Exit(1)\n\t}\n\tstatus := 0\n\tfor _, path := range flag.Args() {\n\t\tif info, err := os.Stat(path); err == nil && !info.IsDir() && !applyIgnore.val && find.val == \"false\" {\n\t\t\t// When given paths to files directly, always format them,\n\t\t\t// no matter their extension or shebang.\n\t\t\t//\n\t\t\t// One exception is --apply-ignore, which explicitly changes this behavior.\n\t\t\t// Another is --find, whose logic depends on walkPath being called.\n\t\t\tif err := formatPath(path, false); err != nil {\n\t\t\t\tif err != errFormattingDiffers {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\t\t}\n\t\t\t\tstatus = 1\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif err := filepath.WalkDir(path, func(path string, entry fs.DirEntry, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tswitch err := walkPath(path, entry); err {\n\t\t\tcase nil:\n\t\t\tcase filepath.SkipDir:\n\t\t\t\treturn err\n\t\t\tcase errFormattingDiffers:\n\t\t\t\tstatus = 1\n\t\t\tdefault:\n\t\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\t\tstatus = 1\n\t\t\t}\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\tstatus = 1\n\t\t}\n\t}\n\tos.Exit(status)\n}\n\nvar errFormattingDiffers = fmt.Errorf(\"\")\n\nfunc formatStdin(name string) error {\n\tif write.val {\n\t\treturn fmt.Errorf(\"-w cannot be used on standard input\")\n\t}\n\tif applyIgnore.val {\n\t\t// Mimic the logic from walkPath to apply the ignore rules.\n\t\tprops, err := ecQuery.Find(name, []string{\"shell\"})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif props.Get(\"ignore\") == \"true\" {\n\t\t\treturn nil\n\t\t}\n\t}\n\tsrc, err := io.ReadAll(os.Stdin)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfileLang := lang.val\n\tif fileLang == syntax.LangAuto {\n\t\textensionLang := strings.TrimPrefix(filepath.Ext(name), \".\")\n\t\tif err := fileLang.Set(extensionLang); err != nil || fileLang == syntax.LangPOSIX {\n\t\t\tshebangLang := fileutil.Shebang(src)\n\t\t\tif err := fileLang.Set(shebangLang); err != nil {\n\t\t\t\t// Fall back to bash.\n\t\t\t\tfileLang = syntax.LangBash\n\t\t\t}\n\t\t}\n\t}\n\treturn formatBytes(src, name, fileLang)\n}\n\nvar vcsDir = regexp.MustCompile(`^\\.(git|svn|hg)$`)\n\nfunc walkPath(path string, entry fs.DirEntry) error {\n\tif entry.IsDir() && vcsDir.MatchString(entry.Name()) {\n\t\treturn filepath.SkipDir\n\t}\n\t// We don't know the language variant at this point yet, as we are walking directories\n\t// and we first want to tell if we should skip a path entirely.\n\t//\n\t// TODO: Should the call to Find with the language name check \"ignore\" too, then?\n\t// Otherwise, a [[bash]] section with ignore=true is effectively never used.\n\t//\n\t// TODO: Should there be a way to explicitly turn off ignore rules when walking?\n\t// Perhaps swapping the default to --apply-ignore=auto and allowing --apply-ignore=false?\n\t// I don't imagine it's a particularly useful scenario for now.\n\tprops, err := ecQuery.Find(path, []string{\"shell\"})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif props.Get(\"ignore\") == \"true\" {\n\t\tif entry.IsDir() {\n\t\t\treturn filepath.SkipDir\n\t\t} else {\n\t\t\treturn nil\n\t\t}\n\t}\n\tconf := fileutil.CouldBeScript2(entry)\n\tif conf == fileutil.ConfNotScript {\n\t\treturn nil\n\t}\n\terr = formatPath(path, conf == fileutil.ConfIfShebang)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nvar ecQuery = editorconfig.Query{\n\tFileCache:   make(map[string]*editorconfig.File),\n\tRegexpCache: make(map[string]*regexp.Regexp),\n}\n\nfunc propsOptions(lang syntax.LangVariant, props editorconfig.Section) (_ syntax.LangVariant, validLang bool) {\n\t// if shell_variant is set to a valid string, it will take precedence\n\tlangErr := lang.Set(props.Get(\"shell_variant\"))\n\tsyntax.Variant(lang)(parser)\n\n\tsize := uint(0)\n\tif props.Get(\"indent_style\") == \"space\" {\n\t\tsize = 8\n\t\tif n := props.IndentSize(); n > 0 {\n\t\t\tsize = uint(n)\n\t\t}\n\t}\n\tsyntax.Indent(size)(printer)\n\n\tsyntax.BinaryNextLine(props.Get(\"binary_next_line\") == \"true\")(printer)\n\t// TODO(v4): rename to case_indent for consistency with flags\n\tsyntax.SwitchCaseIndent(props.Get(\"switch_case_indent\") == \"true\")(printer)\n\tsyntax.SpaceRedirects(props.Get(\"space_redirects\") == \"true\")(printer)\n\tsyntax.KeepPadding(props.Get(\"keep_padding\") == \"true\")(printer)\n\t// TODO(v4): rename to func_next_line for consistency with flags\n\tsyntax.FunctionNextLine(props.Get(\"function_next_line\") == \"true\")(printer)\n\n\tminify := props.Get(\"minify\") == \"true\"\n\tsyntax.Minify(minify)(printer)\n\t// Note that --simplify is not actually a parser option, so we use a global var.\n\t// Just like the CLI flags, minify=true implies simplify=true.\n\tsimplify.val = minify || props.Get(\"simplify\") == \"true\"\n\n\treturn lang, langErr == nil\n}\n\nfunc formatPath(path string, checkShebang bool) error {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\tfileLang := lang.val\n\tshebangForAuto := false\n\tif fileLang == syntax.LangAuto {\n\t\textensionLang := strings.TrimPrefix(filepath.Ext(path), \".\")\n\t\tif err := fileLang.Set(extensionLang); err != nil || fileLang == syntax.LangPOSIX {\n\t\t\tshebangForAuto = true\n\t\t}\n\t}\n\treadBuf.Reset()\n\tif checkShebang || shebangForAuto {\n\t\tn, err := io.ReadAtLeast(f, copyBuf[:32], len(\"#!/bin/sh\\n\"))\n\t\tswitch {\n\t\tcase !checkShebang:\n\t\t\t// only wanted the shebang for LangAuto\n\t\tcase err == io.EOF, errors.Is(err, io.ErrUnexpectedEOF):\n\t\t\treturn nil // too short to have a shebang\n\t\tcase err != nil:\n\t\t\treturn err // some other read error\n\t\t}\n\t\tshebangLang := fileutil.Shebang(copyBuf[:n])\n\t\tif checkShebang && shebangLang == \"\" {\n\t\t\treturn nil // not a shell script\n\t\t}\n\t\tif shebangForAuto {\n\t\t\tif err := fileLang.Set(shebangLang); err != nil {\n\t\t\t\t// Fall back to bash.\n\t\t\t\tfileLang = syntax.LangBash\n\t\t\t}\n\t\t}\n\t\treadBuf.Write(copyBuf[:n])\n\t}\n\tswitch find.val {\n\tcase \"true\":\n\t\tfmt.Println(path)\n\t\treturn nil\n\tcase \"0\":\n\t\tfmt.Print(path)\n\t\tfmt.Print(\"\\000\")\n\t\treturn nil\n\t}\n\tif _, err := io.CopyBuffer(&readBuf, f, copyBuf); err != nil {\n\t\treturn err\n\t}\n\tf.Close()\n\treturn formatBytes(readBuf.Bytes(), path, fileLang)\n}\n\nfunc editorConfigLangs(l syntax.LangVariant) []string {\n\t// All known shells match [[shell]].\n\t// As a special case, bash and the bash-like bats also match [[bash]],\n\t// and zsh also matches [[zsh]].\n\t// We can later consider others like [[mksh]] or [[posix-shell]],\n\t// just consider what list of languages the EditorConfig spec might eventually use.\n\tswitch l {\n\tcase syntax.LangBash, syntax.LangBats:\n\t\treturn []string{\"shell\", \"bash\"}\n\tcase syntax.LangZsh:\n\t\treturn []string{\"shell\", \"zsh\"}\n\tcase syntax.LangPOSIX, syntax.LangMirBSDKorn, syntax.LangAuto:\n\t\treturn []string{\"shell\"}\n\t}\n\treturn nil\n}\n\nfunc formatBytes(src []byte, path string, fileLang syntax.LangVariant) error {\n\tfileLangFromEditorConfig := false\n\tif useEditorConfig {\n\t\tprops, err := ecQuery.Find(path, editorConfigLangs(fileLang))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfileLang, fileLangFromEditorConfig = propsOptions(fileLang, props)\n\t} else {\n\t\tsyntax.Variant(fileLang)(parser)\n\t}\n\tvar node syntax.Node\n\tvar err error\n\tif fromJSON.val {\n\t\tnode, err = typedjson.Decode(bytes.NewReader(src))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tnode, err = parser.Parse(bytes.NewReader(src), path)\n\t\tif err != nil {\n\t\t\tif s, ok := err.(syntax.LangError); ok && lang.val == syntax.LangAuto {\n\t\t\t\tif fileLangFromEditorConfig {\n\t\t\t\t\treturn fmt.Errorf(\"%w (parsed as %s via EditorConfig)\", s, fileLang)\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"%w (parsed as %s via -%s=%s)\", s, fileLang, lang.short, lang.val)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\t// Note that --simplify is treated as a parser option as it happens\n\t// immediately after parsing, even if it's not a [syntax.ParserOption] today.\n\tif simplify.val {\n\t\tsyntax.Simplify(node)\n\t}\n\tif toJSON.val {\n\t\t// must be standard input; fine to return\n\t\t// TODO: change the default behavior to be compact,\n\t\t// and allow using --to-json=pretty or --to-json=indent.\n\t\treturn typedjson.EncodeOptions{Indent: \"\\t\"}.Encode(os.Stdout, node)\n\t}\n\twriteBuf.Reset()\n\tprinter.Print(&writeBuf, node)\n\tres := writeBuf.Bytes()\n\tif !bytes.Equal(src, res) {\n\t\tswitch list.val {\n\t\tcase \"true\":\n\t\t\tfmt.Println(path)\n\t\tcase \"0\":\n\t\t\tfmt.Print(path)\n\t\t\tfmt.Print(\"\\000\")\n\t\t}\n\t\tif write.val {\n\t\t\tinfo, err := os.Lstat(path)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !info.Mode().IsRegular() {\n\t\t\t\treturn fmt.Errorf(\"refusing to atomically replace %q with a regular file as it is not one already\", path)\n\t\t\t}\n\t\t\tperm := info.Mode().Perm()\n\t\t\t// TODO: support atomic writes on Windows?\n\t\t\tif err := maybeio.WriteFile(path, res, perm); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif diff.val {\n\t\t\tdiffBytes := diffpkg.Diff(path+\".orig\", src, path, res)\n\t\t\tif !color {\n\t\t\t\tos.Stdout.Write(diffBytes)\n\t\t\t\treturn errFormattingDiffers\n\t\t\t}\n\t\t\t// The first three lines are the header with the filenames, including --- and +++,\n\t\t\t// and are marked in bold.\n\t\t\tcurrent := terminalBold\n\t\t\tos.Stdout.WriteString(current)\n\t\t\tfor i, line := range bytes.SplitAfter(diffBytes, []byte(\"\\n\")) {\n\t\t\t\tlast := current\n\t\t\t\tswitch {\n\t\t\t\tcase i < 3: // the first three lines are bold\n\t\t\t\tcase bytes.HasPrefix(line, []byte(\"@@\")):\n\t\t\t\t\tcurrent = terminalCyan\n\t\t\t\tcase bytes.HasPrefix(line, []byte(\"-\")):\n\t\t\t\t\tcurrent = terminalRed\n\t\t\t\tcase bytes.HasPrefix(line, []byte(\"+\")):\n\t\t\t\t\tcurrent = terminalGreen\n\t\t\t\tdefault:\n\t\t\t\t\tcurrent = terminalReset\n\t\t\t\t}\n\t\t\t\tif current != last {\n\t\t\t\t\tos.Stdout.WriteString(current)\n\t\t\t\t}\n\t\t\t\tos.Stdout.Write(line)\n\t\t\t}\n\t\t\treturn errFormattingDiffers\n\t\t}\n\t\tif list.val != \"false\" && !write.val {\n\t\t\treturn errFormattingDiffers\n\t\t}\n\t}\n\tif list.val == \"false\" && !write.val && !diff.val {\n\t\tos.Stdout.Write(res)\n\t}\n\treturn nil\n}\n\nconst (\n\tterminalGreen = \"\\u001b[32m\"\n\tterminalRed   = \"\\u001b[31m\"\n\tterminalCyan  = \"\\u001b[36m\"\n\tterminalReset = \"\\u001b[0m\"\n\tterminalBold  = \"\\u001b[1m\"\n)\n"
  },
  {
    "path": "cmd/shfmt/main_test.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/rogpeppe/go-internal/testscript\"\n)\n\nfunc TestMain(m *testing.M) {\n\ttestscript.Main(m, map[string]func(){\n\t\t\"shfmt\": main,\n\t})\n}\n\nvar update = flag.Bool(\"u\", false, \"update testscript output files\")\n\nfunc TestScript(t *testing.T) {\n\tt.Parallel()\n\ttestscript.Run(t, testscript.Params{\n\t\tDir:                 filepath.Join(\"testdata\", \"script\"),\n\t\tUpdateScripts:       *update,\n\t\tRequireExplicitExec: true,\n\t})\n}\n"
  },
  {
    "path": "cmd/shfmt/shfmt.1.scd",
    "content": "shfmt(1)\n\n; To render and view: scdoc <shfmt.1.scd | man -l -\n\n# NAME\n\nshfmt - Format shell programs\n\n# SYNOPSIS\n\n*shfmt* [flags] [path...]\n\n# DESCRIPTION\n\nshfmt formats shell programs. If the only argument is a dash (*-*) or no\narguments are given, standard input will be used. If a given path is a\ndirectory, all shell scripts found under that directory will be used.\n\nIf any EditorConfig files are found, they will be used to apply formatting\noptions. If any parser or printer flags are given to the tool, no EditorConfig\nformatting options will be used. A default like *-i=0* can be used for this purpose.\n\nshfmt's default shell formatting was chosen to be consistent, common, and\npredictable. Some aspects of the format can be configured via printer flags.\n\n# OPTIONS\n\n## Generic flags\n\n*--version*\n\tShow version and exit.\n\n*-l[=0]*, *--list[=0]*\n\tError with a list of files whose formatting differs from shfmt;\n\tpaths are separated by a newline or a null character if -l=0\n\n*-w*, *--write*\n\tWrite result to file instead of stdout.\n\n*-d*, *--diff*\n\tError with a diff when the formatting differs.\n\n\tThe diff uses color when the output is a terminal.\n\tTo never use color, set a non-empty *NO_COLOR* or *TERM=dumb*.\n\tTo always use color, set a non-empty *FORCE_COLOR*.\n\n*--apply-ignore*\n\tAlways apply EditorConfig ignore rules.\n\n\tWhen formatting files directly, ignore rules are skipped without this flag.\n\tShould be useful to any tools or editors which format stdin or a single file.\n\tWhen printing results to stdout, an ignored file results in no output at all.\n\n*--filename* str\n\tProvide a name for the standard input file.\n\n\tUse of this flag is necessary for EditorConfig support to work with stdin,\n\tsince EditorConfig files are found relative to the location of a script.\n\n## Parser flags\n\n*-ln*, *--language-dialect* <str>\n\tLanguage dialect (*bash*/*posix*/*mksh*/*bats*/*zsh*, default *auto*).\n\n\tWhen set to *auto*, the language is detected from the input filename,\n\tas long as it has a shell extension like *foo.mksh*. Otherwise, if the input\n\tbegins with a shell shebang like *#!/bin/sh*, that's used instead.\n\tIf neither come up with a result, *bash* is used as a fallback.\n\n\tThe filename extension *.sh* is a special case: it implies *posix*,\n\tbut may be overridden by a valid shell shebang.\n\n*-p*, *--posix*\n\tShorthand for *-ln=posix*.\n\n*-s*, *--simplify*\n\tSimplify the code.\n\n## Printer flags\n\n*-i*, *--indent* <uint>\n\tIndent: *0* for tabs (default), *>0* for number of spaces.\n\n*-bn*, *--binary-next-line*\n\tBinary ops like *&&* and *|* may start a line.\n\n*-ci*, *--case-indent*\n\tSwitch cases will be indented.\n\n*-sr*, *--space-redirects*\n\tRedirect operators will be followed by a space.\n\n*-kp*, *--keep-padding*\n\tKeep column alignment paddings.\n\n\tThis flag is *DEPRECATED* and will be removed in the next major version.\n\tFor more information, see: https://github.com/mvdan/sh/issues/658\n\n*-fn*, *--func-next-line*\n\tFunction opening braces are placed on a separate line.\n\n*-mn*, *--minify*\n\tMinify the code to reduce its size (implies *-s*).\n\n## Utility flags\n\n*-f[=0]*, *--find[=0]*\n\tRecursively find all shell files and print the paths;\n\tpaths are separated by a newline or a null character if -f=0.\n\n*--to-json*\n\tPrint syntax tree to stdout as a typed JSON.\n\n*--from-json*\n\tRead syntax tree from stdin as a typed JSON.\n\n# EXAMPLES\n\nFormat all the scripts under the current directory, printing which are modified:\n\n\tshfmt -l -w .\n\nFor CI, one can use a variant where formatting changes are just shown as diffs:\n\n\tshfmt -d .\n\nThe following formatting flags closely resemble Google's shell style defined in\n<https://google.github.io/styleguide/shellguide.html>:\n\n\tshfmt -i 2 -ci -bn\n\nBelow is a sample EditorConfig file as defined by <https://editorconfig.org/>,\nshowing how to set supported options:\n\n```\n[*.sh]\n# like -i=4\nindent_style = space\nindent_size = 4\n\n# --language-dialect\nshell_variant      = posix\nsimplify           = true\nbinary_next_line   = true\n# --case-indent\nswitch_case_indent = true\nspace_redirects    = true\nkeep_padding       = true\n# --func-next-line\nfunction_next_line = true\nminify             = true\n\n# Ignore the entire \"third_party\" directory when calling shfmt on directories,\n# such as \"shfmt -l -w .\". When formatting files directly,\n# like \"shfmt -w third_party/foo.sh\" or \"shfmt --filename=third_party/foo.sh\",\n# the ignore logic is applied only when the --apply-ignore flag is given.\n[third_party/**]\nignore = true\n```\n\nEditorConfig sections may also use `[[shell]]`, `[[bash]]`, or `[[zsh]]` to match shell scripts,\nwhich is particularly useful when scripts use a shebang but no extension.\nNote that this feature is outside of the EditorConfig spec and may be changed in the future.\n\nshfmt can also replace *bash -n* to check shell scripts for syntax errors. It is\nmore exhaustive, as it parses all syntax statically and requires valid UTF-8:\n\n```\n$ echo '${foo:1 2}' | bash -n\n$ echo '${foo:1 2}' | shfmt >/dev/null\n1:9: not a valid arithmetic operator: 2\n$ echo 'foo=(1 2)' | bash --posix -n\n$ echo 'foo=(1 2)' | shfmt -p >/dev/null\n1:5: arrays are a bash feature\n```\n\n# AUTHORS\n\nMaintained by Daniel Martí <mvdan@mvdan.cc>, who is assisted by other open\nsource contributors. For more information and development, see\n<https://github.com/mvdan/sh>.\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/atomic.txtar",
    "content": "[windows] skip 'atomic writes aren''t supported on Windows'\n[!exec:sh] skip 'sh is required to run this test'\n\n# If we don't do atomic writes, most shells will error when shfmt overwrites the\n# very script it's running from. This is because the shell doesn't read all of\n# the input script upfront.\nexec sh input.sh\ncmp stdout stdout.golden\n! stderr .\n\ncmp input.sh input.sh.golden\n\n-- input.sh --\necho foo\n  shfmt -l -w input.sh\necho bar\n-- input.sh.golden --\necho foo\nshfmt -l -w input.sh\necho bar\n-- stdout.golden --\nfoo\ninput.sh\nbar\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/basic.txtar",
    "content": "cp input.sh input.sh.orig\n\nstdin input.sh\nexec shfmt\ncmp stdout input.sh.golden\n! stderr .\n\nstdin input.sh\nexec shfmt -\ncmp stdout input.sh.golden\n! stderr .\n\nexec shfmt input.sh\ncmp stdout input.sh.golden\n! stderr .\n\n! exec shfmt -l input.sh\nstdout 'input\\.sh'\n! stdout foo\n! stderr .\ncmp input.sh input.sh.orig\n\n! exec shfmt -l input.sh input.sh\nstdout -count=2 'input.sh'\n\nexec shfmt -l -w input.sh\nstdout 'input\\.sh'\n! stdout foo\n! stderr .\ncmp input.sh input.sh.golden\n\ncp input.sh.orig input.sh\nexec shfmt --list --write input.sh\nstdout 'input\\.sh'\n! stdout foo\n! stderr .\ncmp input.sh input.sh.golden\n\n-- input.sh --\n foo\n-- input.sh.golden --\nfoo\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/diff.txtar",
    "content": "stdin input.sh\n! exec shfmt -d\ncmp stdout input.sh.stdindiff\n! stderr .\n\nstdin input.sh\n! exec shfmt --diff\ncmp stdout input.sh.stdindiff\n! stderr .\n\n! exec shfmt -d input.sh\ncmp stdout input.sh.filediff\n! stderr .\n\n! exec shfmt -d input.sh input.sh\nstdout -count=4 'input.sh.orig'\n\nenv FORCE_COLOR=true\nstdin input.sh\n! exec shfmt -d\nstdout '\\x1b\\[31m- foo'\n! stderr .\nenv FORCE_COLOR=\n\n! exec shfmt -d .\ncmp stdout input.sh.filediff\n! stderr .\n\n-- input.sh --\n foo\n\n\nbar\n-- input.sh.golden --\nfoo\n\nbar\n-- input.sh.stdindiff --\ndiff <standard input>.orig <standard input>\n--- <standard input>.orig\n+++ <standard input>\n@@ -1,4 +1,3 @@\n- foo\n-\n+foo\n \n bar\n-- input.sh.filediff --\ndiff input.sh.orig input.sh\n--- input.sh.orig\n+++ input.sh\n@@ -1,4 +1,3 @@\n- foo\n-\n+foo\n \n bar\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/editorconfig.txtar",
    "content": "cp input.sh input.sh.orig\n\n# Using stdin should use EditorConfig.\nstdin input.sh\nexec shfmt\ncmp stdout input.sh.golden\n! stderr .\n\n# Verify that -filename works well with EditorConfig.\nstdin stdin-filename-bash\nexec shfmt\n\nstdin stdin-filename-bash\n! exec shfmt -filename=foo_posix.sh\nstderr '^foo_posix.sh:.* arrays are a bash.*parsed as posix via EditorConfig'\n\nstdin stdin-filename-bash\n! exec shfmt -filename=${WORK}/foo_posix.sh\nstderr ^${WORK@R}/'foo_posix.sh:.* arrays are a bash.*parsed as posix via EditorConfig'\n\n# Using a file path should use EditorConfig, including with the use of flags\n# like -l.\nexec shfmt input.sh\ncmp stdout input.sh.golden\n! stderr .\n\n! exec shfmt -l input.sh\nstdout 'input\\.sh'\n! stderr .\n\n# Using any formatting option should skip all EditorConfig usage.\nexec shfmt -p input.sh\ncmp stdout input.sh.orig\n! stderr .\n\nexec shfmt -l -p input.sh\n! stdout .\n! stderr .\n\nexec shfmt -sr input.sh\ncmp stdout input.sh.orig\n! stderr .\n\n# Check that EditorConfig files merge properly.\nexec shfmt morespaces/input.sh\ncmp stdout morespaces/input.sh.golden\n! stderr .\n\n# Check a folder with all other knobs.\nexec shfmt -l otherknobs\n! stdout .\n! stderr .\n\n# Files found by walking directories are skipped if they match ignore=true properties.\n! exec shfmt -l ignored\nstdout 'regular\\.sh'\n! stdout 'ignored\\.sh'\n! stderr .\n\n# EditorConfig ignore=true properties are obeyed even when any formatting flags\n# are used, which cause formatting options from EditorConfig files to be skipped.\n! exec shfmt -i=0 -l ignored\nstdout 'regular\\.sh'\n! stdout 'ignored\\.sh'\n! stderr .\n\n# Formatting files directly does not obey ignore=true properties by default.\n# Test the various modes in which shfmt can run.\n! exec shfmt -l input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh\nstdout -count=1 'input\\.sh$'\nstdout -count=1 'ignored\\.sh$'\nstderr -count=1 'ignored\\.sh.* must be followed by'\n! exec shfmt -d input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh\nstdout -count=2 'input\\.sh$'\nstdout -count=2 'ignored\\.sh$'\nstderr -count=1 'ignored\\.sh.* must be followed by'\n! exec shfmt input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh\nstdout -count=1 'indented'\nstdout -count=1 'echo foo'\nstderr -count=1 'ignored\\.sh.* must be followed by'\nstdin ignored/1_lone_ignored.sh\nexec shfmt --filename=ignored/1_lone_ignored.sh\nstdout -count=1 'echo foo'\n! stderr .\n\n# Formatting files directly obeys ignore=true when --apply-ignore is given.\n# Test the same modes that the earlier section does.\n! exec shfmt --apply-ignore -l input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh\nstdout -count=1 'input\\.sh$'\n! stdout 'ignored\\.sh'\n! stderr .\n! exec shfmt --apply-ignore -d input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh\nstdout -count=2 'input\\.sh$'\n! stdout 'ignored\\.sh'\n! stderr .\nexec shfmt --apply-ignore input.sh ignored/1_lone_ignored.sh ignored/third_party/bad_syntax_ignored.sh\nstdout -count=1 'indented'\n! stdout 'echo foo'\n! stderr .\nstdin ignored/1_lone_ignored.sh\nexec shfmt --apply-ignore --filename=ignored/1_lone_ignored.sh\n! stdout .\n! stderr .\n\n# Check EditorConfig [[language]] sections, used primarily for extension-less strings with shebangs.\nexec shfmt -d shebang\n! stdout .\n! stderr .\n\n# Verify that sibling EditorConfig files do not get their settings mixed up,\n# which could happen if we incrementally use their flags without care.\nexec shfmt -d multiconfig\n! stdout .\n! stderr .\n\n-- .editorconfig --\nroot = true\n\n[*]\nindent_style = space\nindent_size = 3\n\n[*_posix.sh]\nshell_variant = posix\n-- input.sh --\n{\n\tindented\n}\n-- input.sh.golden --\n{\n   indented\n}\n-- stdin-filename-bash --\narray=(\n\telement\n)\n-- morespaces/.editorconfig --\n[*.sh]\nindent_size = 6\n-- morespaces/input.sh --\n{\n\tindented\n}\n-- morespaces/input.sh.golden --\n{\n      indented\n}\n-- otherknobs/.editorconfig --\nroot = true\n\n[*_bash.sh]\nshell_variant = bash\n\n[*_mksh.sh]\nshell_variant = mksh\n\n[indent.sh]\n# check its default; we tested \"space\" above.\n\n[binary_next_line.sh]\nbinary_next_line = true\n\n[switch_case_indent.sh]\nswitch_case_indent = true\n\n[space_redirects.sh]\nspace_redirects = true\n\n[keep_padding.sh]\nkeep_padding = true\n\n[function_next_line.sh]\nfunction_next_line = true\n\n[simplify.sh]\nsimplify = true\n\n[minify.sh]\nminify = true\n\n-- otherknobs/shell_variant_bash.sh --\narray=(elem)\n-- otherknobs/shell_variant_mksh.sh --\ncoprocess |&\n-- otherknobs/indent.sh --\n{\n\tindented\n}\n-- otherknobs/binary_next_line.sh --\nfoo \\\n\t| bar\n-- otherknobs/switch_case_indent.sh --\ncase \"$1\" in\n\tA) echo foo ;;\nesac\n-- otherknobs/space_redirects.sh --\necho foo > bar\n-- otherknobs/keep_padding.sh --\necho  foo    bar\n-- otherknobs/function_next_line.sh --\nfoo()\n{\n\techo foo\n}\n-- otherknobs/simplify.sh --\nfoo() {\n\t((bar))\n}\n-- otherknobs/minify.sh --\nfoo(){\n((bar))\n}\n-- ignored/.editorconfig --\nroot = true\n\n[third_party/**]\nignore = true\n\n[1_lone_ignored.sh]\nignore = true\n\n[2_dir_ignored]\nignore = true\n\n-- ignored/third_party/bad_syntax_ignored.sh --\nbad (syntax\n-- ignored/1_lone_ignored.sh --\necho   foo\n-- ignored/2_dir_ignored/ignored.sh --\necho   foo\n-- ignored/3_regular/regular.sh --\necho   foo\n-- shebang/.editorconfig --\nroot = true\n\n[*]\nindent_style = space\nindent_size = 1\n\n[[shell]]\nindent_size = 2\n\n[[bash]]\nindent_size = 4\n\n[[zsh]]\nindent_size = 5\n\n-- shebang/binsh --\n#!/bin/sh\n\n{\n  indented\n}\n-- shebang/binbash --\n#!/bin/bash\n\n{\n    indented\n}\narray=(elem)\n-- shebang/binzsh --\n#!/bin/zsh\n\n{\n     indented\n}\narray=(elem)\n-- multiconfig/space_redirects/.editorconfig --\n[*]\nspace_redirects = true\n-- multiconfig/space_redirects/f.sh --\nfoo > bar\nfoo &&\n   bar\n-- multiconfig/binary_next_line/.editorconfig --\n[*]\nbinary_next_line = true\n-- multiconfig/binary_next_line/f.sh --\nfoo >bar\nfoo \\\n   && bar\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/flags.txtar",
    "content": "exec shfmt -h\n! stderr 'flag provided but not defined'\nstderr 'usage: shfmt'\nstderr 'Utilities' # definitely includes our help text\n! stderr 'help requested' # don't duplicate usage output\n! stderr '-test\\.' # don't show the test binary's usage func\n\nexec shfmt --help\nstderr 'usage: shfmt'\n\nexec shfmt -version\nstdout 'devel|v3'\n! stderr .\n\nexec shfmt --version\nstdout 'devel|v3'\n! stderr .\n\n! exec shfmt -ln=bash -p\nstderr 'cannot coexist'\n\n! exec shfmt --language-dialect=bash --posix\nstderr 'cannot coexist'\n\n! exec shfmt -ln=bad\nstderr 'unknown shell language'\n\n! exec shfmt --to-json file\nstderr '--to-json can only be used with stdin'\n\n! exec shfmt -filename=foo file\nstderr '-filename can only be used with stdin'\n\n# Check all the -ln variations.\n\nstdin input-posix\n! exec shfmt\n\nstdin input-posix\nexec shfmt -ln=posix\nstdout 'let'\n\nstdin input-posix\nexec shfmt -p\nstdout 'let'\n\nstdin input-posix\n! exec shfmt -ln=mksh\n\nstdin input-posix\n! exec shfmt -ln=bash\n\nstdin input-mksh\nexec shfmt -ln=mksh\nstdout 'coprocess'\n\nstdin input-mksh\n! exec shfmt\n\n# Ensure that the default \"bash\" language works with and without flags.\nstdin input-bash\nexec shfmt\nstdout loop\n\n# Ensure that -ln=auto works on stdin via filename.\nstdin input-mksh\nexec shfmt -filename=input.mksh\nstdout 'coprocess'\n\n# Ensure that -ln=auto works on stdin via shebang.\nstdin input-mksh-shebang\nexec shfmt\nstdout 'coprocess'\n\n# Ensure that -ln=auto works on stdin using the fallback.\nstdin input-bash\nexec shfmt\nstdout 'loop'\n\n# The default -ln=auto shouldn't require an extension or shebang,\n# as long as we're explicitly formatting a file.\nexec shfmt input-tiny\nstdout foo\n\n# -ln=auto should prefer a shebang if the extension is only \".sh\".\nstdin input-mksh-shebang\nexec shfmt -filename=input.sh\nstdout 'coprocess'\n\n# An explicit -ln=auto should still work.\nstdin input-mksh\nexec shfmt -ln=auto -filename=input.mksh\nstdout 'coprocess'\n\n# Explicitly state language on parse errors\nstdin input-bash-arrays\n! exec shfmt -ln=auto -filename=input.sh\nstderr 'parsed as posix via -ln=auto'\n\nstdin input-bash-extglobs\n! exec shfmt -filename=input.sh\nstderr 'parsed as posix via -ln=auto'\n\nstdin flags-input\nexec shfmt -i 2\ncmp stdout flags-output.indent-golden\n\nstdin flags-input\nexec shfmt --indent 2\ncmp stdout flags-output.indent-golden\n\nstdin flags-input\nexec shfmt -bn\ncmp stdout flags-output.binary-next-line-golden\n\nstdin flags-input\nexec shfmt --binary-next-line\ncmp stdout flags-output.binary-next-line-golden\n\nstdin flags-input\nexec shfmt -ci\ncmp stdout flags-output.case-indent-golden\n\nstdin flags-input\nexec shfmt --case-indent\ncmp stdout flags-output.case-indent-golden\n\nstdin flags-input\nexec shfmt -sr\ncmp stdout flags-output.space-redirects-golden\n\nstdin flags-input\nexec shfmt --space-redirects\ncmp stdout flags-output.space-redirects-golden\n\nstdin flags-input\nexec shfmt -kp\ncmp stdout flags-output.keep-padding-golden\n\nstdin flags-input\nexec shfmt --keep-padding\ncmp stdout flags-output.keep-padding-golden\n\nstdin flags-input\nexec shfmt -fn\ncmp stdout flags-output.func-next-line-golden\n\nstdin flags-input\nexec shfmt --func-next-line\ncmp stdout flags-output.func-next-line-golden\n\n-- input-posix --\nlet a+\n-- input-bash --\nfor ((;;)); do loop; done\n-- input-tiny --\nfoo\n-- input-mksh --\ncoprocess |&\n-- input-mksh-shebang --\n#!/bin/mksh\ncoprocess |&\n-- input-bash-extglobs --\n#!/bin/sh\necho !(a)\n-- input-bash-arrays --\n#!/bin/sh\nfoo=(bar)\n\n-- flags-input --\nfoo() {\n\tbar &&\n\t\tbaz\n\n\tcase $i in\n\tj)\n\t\tz\n\t\t;;\n\tesac\n\n\tspace >redirs\n\n\tkeep    padding\n}\n-- flags-output.indent-golden --\nfoo() {\n  bar &&\n    baz\n\n  case $i in\n  j)\n    z\n    ;;\n  esac\n\n  space >redirs\n\n  keep padding\n}\n-- flags-output.binary-next-line-golden --\nfoo() {\n\tbar \\\n\t\t&& baz\n\n\tcase $i in\n\tj)\n\t\tz\n\t\t;;\n\tesac\n\n\tspace >redirs\n\n\tkeep padding\n}\n-- flags-output.case-indent-golden --\nfoo() {\n\tbar &&\n\t\tbaz\n\n\tcase $i in\n\t\tj)\n\t\t\tz\n\t\t\t;;\n\tesac\n\n\tspace >redirs\n\n\tkeep padding\n}\n-- flags-output.space-redirects-golden --\nfoo() {\n\tbar &&\n\t\tbaz\n\n\tcase $i in\n\tj)\n\t\tz\n\t\t;;\n\tesac\n\n\tspace > redirs\n\n\tkeep padding\n}\n-- flags-output.keep-padding-golden --\nfoo() {\n\tbar &&\n\t\tbaz\n\n\tcase $i in\n\tj)\n\t\tz\n\t\t;;\n\tesac\n\n\tspace >redirs\n\n\tkeep  padding\n}\n-- flags-output.func-next-line-golden --\nfoo()\n{\n\tbar &&\n\t\tbaz\n\n\tcase $i in\n\tj)\n\t\tz\n\t\t;;\n\tesac\n\n\tspace >redirs\n\n\tkeep padding\n}\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/simplify.txtar",
    "content": "exec shfmt -s input.sh\ncmp stdout input.sh.simplify-golden\n\nexec shfmt --simplify input.sh\ncmp stdout input.sh.simplify-golden\n\nexec shfmt -mn input.sh\ncmp stdout input.sh.minify-golden\n\nexec shfmt --minify input.sh\ncmp stdout input.sh.minify-golden\n\n-- input.sh --\nfoo() {\n\t(( $bar ))\n}\n-- input.sh.simplify-golden --\nfoo() {\n\t((bar))\n}\n-- input.sh.minify-golden --\nfoo(){\n((bar))\n}\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/tojson.txtar",
    "content": "stdin empty.sh\nexec shfmt -tojson # old flag name\ncmp stdout empty.sh.json\n! stderr .\n\nstdin simple.sh\nexec shfmt --to-json\ncmp stdout simple.sh.json\n\nstdin arithmetic.sh\nexec shfmt --to-json\ncmp stdout arithmetic.sh.json\n\nstdin comment.sh\nexec shfmt --to-json\ncmp stdout comment.sh.json\n\n-- empty.sh --\n-- empty.sh.json --\n{\n\t\"Type\": \"File\"\n}\n-- simple.sh --\nfoo\n-- simple.sh.json --\n{\n\t\"Type\": \"File\",\n\t\"Pos\": {\n\t\t\"Offset\": 0,\n\t\t\"Line\": 1,\n\t\t\"Col\": 1\n\t},\n\t\"End\": {\n\t\t\"Offset\": 3,\n\t\t\"Line\": 1,\n\t\t\"Col\": 4\n\t},\n\t\"Stmts\": [\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 0,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 3,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 4\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\"Col\": 4\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 0,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t}\n\t]\n}\n-- arithmetic.sh --\n((2))\n-- arithmetic.sh.json --\n{\n\t\"Type\": \"File\",\n\t\"Pos\": {\n\t\t\"Offset\": 0,\n\t\t\"Line\": 1,\n\t\t\"Col\": 1\n\t},\n\t\"End\": {\n\t\t\"Offset\": 5,\n\t\t\"Line\": 1,\n\t\t\"Col\": 6\n\t},\n\t\"Stmts\": [\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 0,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 5,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 6\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"ArithmCmd\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 5,\n\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\"Col\": 6\n\t\t\t\t},\n\t\t\t\t\"Left\": {\n\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Right\": {\n\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\"Col\": 4\n\t\t\t\t},\n\t\t\t\t\"X\": {\n\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 2,\n\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t},\n\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 2,\n\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 2,\n\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Value\": \"2\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 0,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t}\n\t]\n}\n-- comment.sh --\n#\n-- comment.sh.json --\n{\n\t\"Type\": \"File\",\n\t\"Pos\": {\n\t\t\"Offset\": 0,\n\t\t\"Line\": 1,\n\t\t\"Col\": 1\n\t},\n\t\"End\": {\n\t\t\"Offset\": 1,\n\t\t\"Line\": 1,\n\t\t\"Col\": 2\n\t},\n\t\"Last\": [\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 0,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 1,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 2\n\t\t\t},\n\t\t\t\"Hash\": {\n\t\t\t\t\"Offset\": 0,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/walk.txtar",
    "content": "mkdir symlink/subdir\n# Remember that the symlink target is relative to the symlink directory.\n[symlink] symlink symlink/subdir/symlink-file -> ../../modify/ext-shebang.sh\n[symlink] symlink symlink/symlink-shebang.sh -> ../modify/ext-shebang.sh\n[symlink] cp modify/ext.bash symlink/target-ext-bash # a copy that won't be formatted on its own\n[symlink] symlink symlink/symlink-ext.bash -> target-ext-bash\n[symlink] symlink symlink/symlink-dir -> subdir\n[symlink] symlink symlink/symlink-none -> nonexistent\n\n# Other non-regular files like FIFOs could cause issues like blocking reads\n# when we look for shebangs. We use the \"mkfifo\" tool here, available on Linux.\n[exec:mkfifo] exec mkfifo named-pipe\n\nexec shfmt -f .\n! stderr .\ncmpenv stdout find.golden\n\nexec shfmt --find .\n! stderr .\ncmpenv stdout find.golden\n\n# try to format missing paths\n! exec shfmt nonexistent\nstderr -count=1 nonexistent\n\n! exec shfmt nonexistent-1 nonexistent-2 nonexistent-3\nstderr -count=1 nonexistent-1\nstderr -count=1 nonexistent-2\nstderr -count=1 nonexistent-3\n\n# format an entire directory without -l or -w\n! exec shfmt .\nstdout 'foo'\nstdout 'bin/env'\nstderr -count=1 'parse-error\\.sh'\n\n# just -l, as a dry run\n! exec shfmt --list .\nstderr -count=1 'parse-error\\.sh'\n! stderr '^modify'\ncmpenv stdout modify.golden\n\n# format an entire directory with -l and -w\n! exec shfmt -l -w .\nstderr -count=1 'parse-error\\.sh'\n! stderr '^modify'\ncmpenv stdout modify.golden\n\n# parse-error again, but now as a lone file\n! exec shfmt error/parse-error.sh\nstderr -count=1 'parse-error\\.sh'\n\n# format files directly which we would ignore when walking directories\nexec shfmt none/ext-shebang.other\nstdout 'foo'\nexec shfmt none/noext-noshebang\nstdout 'foo'\n[symlink] exec shfmt symlink/symlink-shebang.sh\n[symlink] stdout 'foo'\n[symlink] exec shfmt symlink/symlink-dir\n[symlink] ! stdout . # note that filepath.WalkDir does not follow symlinks\n\n# writing to non-regular files is forbidden as they'd be replaced by a regular file.\n[symlink] ! exec shfmt -l symlink/symlink-ext.bash\n[symlink] stdout 'symlink-ext.bash'\n[symlink] ! exec shfmt -w symlink/symlink-ext.bash\n[symlink] stderr 'refusing to atomically replace'\n[symlink] ! exec shfmt -l symlink/symlink-ext.bash\n[symlink] stdout 'symlink-ext.bash'\n\n# -f on files should still check extension and shebang\nexec shfmt -f modify/ext.sh modify/shebang-sh none/ext-shebang.other none/noext-noshebang\nstdout -count=2 '^modify'\n! stdout '^none'\n\n# -ln shouldn't be overwritten by a filename\nmkdir modify-ln\ncp modify/ext.mksh modify-ln\n! exec shfmt -ln=bash modify-ln\nstderr '|& must be followed by a statement'\nrm modify-ln\n\n# -ln shouldn't be overwritten by a shebang\nmkdir modify-ln\ncp modify/shebang-mksh modify-ln\n! exec shfmt -ln=bash modify-ln\nstderr '|& must be followed by a statement'\nrm modify-ln\n\n-- find.golden --\nerror${/}parse-error.sh\nmodify${/}dir${/}ext.sh\nmodify${/}ext-shebang.sh\nmodify${/}ext.bash\nmodify${/}ext.bats\nmodify${/}ext.mksh\nmodify${/}ext.sh\nmodify${/}ext.zsh\nmodify${/}shebang-args\nmodify${/}shebang-bash\nmodify${/}shebang-bash.sh\nmodify${/}shebang-env-bash\nmodify${/}shebang-env-bats\nmodify${/}shebang-env-sh\nmodify${/}shebang-mksh\nmodify${/}shebang-sh\nmodify${/}shebang-space\nmodify${/}shebang-tabs\nmodify${/}shebang-usr-sh\nmodify${/}shebang-zsh\n-- modify.golden --\nmodify${/}dir${/}ext.sh\nmodify${/}ext-shebang.sh\nmodify${/}ext.bash\nmodify${/}ext.bats\nmodify${/}ext.mksh\nmodify${/}ext.sh\nmodify${/}ext.zsh\nmodify${/}shebang-args\nmodify${/}shebang-bash\nmodify${/}shebang-bash.sh\nmodify${/}shebang-env-bash\nmodify${/}shebang-env-bats\nmodify${/}shebang-env-sh\nmodify${/}shebang-mksh\nmodify${/}shebang-sh\nmodify${/}shebang-space\nmodify${/}shebang-tabs\nmodify${/}shebang-usr-sh\nmodify${/}shebang-zsh\n-- modify/shebang-sh --\n#!/bin/sh\n foo\n-- modify/shebang-bash --\n#!/bin/bash\n foo=(bar)\n-- modify/shebang-bash.sh --\n#!/bin/bash\n foo=(bar)\n-- modify/shebang-usr-sh --\n#!/usr/bin/sh\n foo\n-- modify/shebang-env-bash --\n#!/usr/bin/env bash\n foo=(bar)\n-- modify/shebang-env-sh --\n#!/bin/env sh\n foo\n-- modify/shebang-mksh --\n#!/bin/mksh\n foo |&\n-- modify/shebang-zsh --\n#!/bin/zsh\n ${+foo}\n-- modify/shebang-env-bats --\n#!/usr/bin/env bats\n @test \"foo\" { bar; }\n-- modify/shebang-space --\n#! /bin/sh\n foo\n-- modify/shebang-tabs --\n#!\t/bin/env\tsh\n foo\n-- modify/shebang-args --\n#!/bin/bash -e -x\n foo\n-- modify/ext.sh --\n foo\n-- modify/ext.bash --\n foo=(bar)\n-- modify/ext.mksh --\n foo |&\n-- modify/ext.zsh --\n ${+foo}\n-- modify/ext.bats --\n @test \"foo\" { bar; }\n-- modify/ext-shebang.sh --\n#!/bin/sh\n foo\n-- modify/dir/ext.sh --\nfoo\n\n-- none/.hidden --\nfoo long enough\n-- none/.hidden-shebang --\n#!/bin/sh\n foo\n-- none/..hidden-shebang --\n#!/bin/sh\n foo\n-- none/noext-empty --\nfoo\n-- none/noext-noshebang --\nfoo long enough\n-- none/shebang-nonewline --\n#!/bin/shfoo\n-- none/ext.other --\nfoo\n-- none/empty --\n-- none/ext-shebang.other --\n#!/bin/sh\n foo\n-- none/shebang-nospace --\n#!/bin/envsh\n foo\n\n-- skip/.git/ext.sh --\nfoo\n-- skip/.svn/ext.sh --\nfoo\n-- skip/.hg/ext.sh --\nfoo\n\n-- error/parse-error.sh --\nfoo(\n"
  },
  {
    "path": "expand/arith.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// TODO(v4): the arithmetic APIs should return int64 for portability with 32-bit systems,\n// even if Bash only supports native int sizes.\n\nfunc Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) {\n\tswitch expr := expr.(type) {\n\tcase *syntax.Word:\n\t\tstr, err := Literal(cfg, expr)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\t// recursively fetch vars\n\t\ti := 0\n\t\tfor syntax.ValidName(str) {\n\t\t\tval := cfg.envGet(str)\n\t\t\tif val == \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif i++; i >= maxNameRefDepth {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tstr = val\n\t\t}\n\t\t// default to 0\n\t\treturn int(atoi(str)), nil\n\tcase *syntax.ParenArithm:\n\t\treturn Arithm(cfg, expr.X)\n\tcase *syntax.UnaryArithm:\n\t\tswitch expr.Op {\n\t\tcase syntax.Inc, syntax.Dec:\n\t\t\tname := expr.X.(*syntax.Word).Lit()\n\t\t\told := atoi(cfg.envGet(name))\n\t\t\tval := old\n\t\t\tif expr.Op == syntax.Inc {\n\t\t\t\tval++\n\t\t\t} else {\n\t\t\t\tval--\n\t\t\t}\n\t\t\tif err := cfg.envSet(name, strconv.FormatInt(val, 10)); err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tif expr.Post {\n\t\t\t\treturn int(old), nil\n\t\t\t}\n\t\t\treturn int(val), nil\n\t\t}\n\t\tval, err := Arithm(cfg, expr.X)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tswitch expr.Op {\n\t\tcase syntax.Not:\n\t\t\treturn oneIf(val == 0), nil\n\t\tcase syntax.BitNegation:\n\t\t\treturn ^val, nil\n\t\tcase syntax.Plus:\n\t\t\treturn val, nil\n\t\tcase syntax.Minus:\n\t\t\treturn -val, nil\n\t\tdefault:\n\t\t\treturn 0, fmt.Errorf(\"unsupported unary arithmetic operator: %q\", expr.Op)\n\t\t}\n\tcase *syntax.BinaryArithm:\n\t\tswitch expr.Op {\n\t\tcase syntax.Assgn, syntax.AddAssgn, syntax.SubAssgn,\n\t\t\tsyntax.MulAssgn, syntax.QuoAssgn, syntax.RemAssgn,\n\t\t\tsyntax.AndAssgn, syntax.OrAssgn, syntax.XorAssgn,\n\t\t\tsyntax.ShlAssgn, syntax.ShrAssgn:\n\t\t\treturn cfg.assgnArit(expr)\n\t\tcase syntax.TernQuest: // TernColon can't happen here\n\t\t\tcond, err := Arithm(cfg, expr.X)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tb2 := expr.Y.(*syntax.BinaryArithm) // must have Op==TernColon\n\t\t\tif cond == 1 {\n\t\t\t\treturn Arithm(cfg, b2.X)\n\t\t\t}\n\t\t\treturn Arithm(cfg, b2.Y)\n\t\t}\n\t\tleft, err := Arithm(cfg, expr.X)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tright, err := Arithm(cfg, expr.Y)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn binArit(expr.Op, left, right)\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unexpected arithm expr: %T\", expr))\n\t}\n}\n\nfunc oneIf(b bool) int {\n\tif b {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// atoi is like [strconv.ParseInt](s, 10, 64), but it ignores errors and trims whitespace.\nfunc atoi(s string) int64 {\n\ts = strings.TrimSpace(s)\n\tn, _ := strconv.ParseInt(s, 10, 64)\n\treturn n\n}\n\nfunc (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) {\n\tname := b.X.(*syntax.Word).Lit()\n\tval := atoi(cfg.envGet(name))\n\targ_, err := Arithm(cfg, b.Y)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\targ := int64(arg_)\n\tswitch b.Op {\n\tcase syntax.Assgn:\n\t\tval = arg\n\tcase syntax.AddAssgn:\n\t\tval += arg\n\tcase syntax.SubAssgn:\n\t\tval -= arg\n\tcase syntax.MulAssgn:\n\t\tval *= arg\n\tcase syntax.QuoAssgn:\n\t\tif arg == 0 {\n\t\t\treturn 0, fmt.Errorf(\"division by zero\")\n\t\t}\n\t\tval /= arg\n\tcase syntax.RemAssgn:\n\t\tif arg == 0 {\n\t\t\treturn 0, fmt.Errorf(\"division by zero\")\n\t\t}\n\t\tval %= arg\n\tcase syntax.AndAssgn:\n\t\tval &= arg\n\tcase syntax.OrAssgn:\n\t\tval |= arg\n\tcase syntax.XorAssgn:\n\t\tval ^= arg\n\tcase syntax.ShlAssgn:\n\t\tval <<= uint(arg)\n\tcase syntax.ShrAssgn:\n\t\tval >>= uint(arg)\n\t}\n\tif err := cfg.envSet(name, strconv.FormatInt(val, 10)); err != nil {\n\t\treturn 0, err\n\t}\n\treturn int(val), nil\n}\n\nfunc intPow(a, b int) int {\n\tp := 1\n\tfor b > 0 {\n\t\tif b&1 != 0 {\n\t\t\tp *= a\n\t\t}\n\t\tb >>= 1\n\t\ta *= a\n\t}\n\treturn p\n}\n\nfunc binArit(op syntax.BinAritOperator, x, y int) (int, error) {\n\tswitch op {\n\tcase syntax.Add:\n\t\treturn x + y, nil\n\tcase syntax.Sub:\n\t\treturn x - y, nil\n\tcase syntax.Mul:\n\t\treturn x * y, nil\n\tcase syntax.Quo:\n\t\tif y == 0 {\n\t\t\treturn 0, fmt.Errorf(\"division by zero\")\n\t\t}\n\t\treturn x / y, nil\n\tcase syntax.Rem:\n\t\tif y == 0 {\n\t\t\treturn 0, fmt.Errorf(\"division by zero\")\n\t\t}\n\t\treturn x % y, nil\n\tcase syntax.Pow:\n\t\treturn intPow(x, y), nil\n\tcase syntax.Eql:\n\t\treturn oneIf(x == y), nil\n\tcase syntax.Gtr:\n\t\treturn oneIf(x > y), nil\n\tcase syntax.Lss:\n\t\treturn oneIf(x < y), nil\n\tcase syntax.Neq:\n\t\treturn oneIf(x != y), nil\n\tcase syntax.Leq:\n\t\treturn oneIf(x <= y), nil\n\tcase syntax.Geq:\n\t\treturn oneIf(x >= y), nil\n\tcase syntax.And:\n\t\treturn x & y, nil\n\tcase syntax.Or:\n\t\treturn x | y, nil\n\tcase syntax.Xor:\n\t\treturn x ^ y, nil\n\tcase syntax.Shr:\n\t\treturn x >> uint(y), nil\n\tcase syntax.Shl:\n\t\treturn x << uint(y), nil\n\tcase syntax.AndArit:\n\t\treturn oneIf(x != 0 && y != 0), nil\n\tcase syntax.OrArit:\n\t\treturn oneIf(x != 0 || y != 0), nil\n\tcase syntax.Comma:\n\t\t// x is executed but its result discarded\n\t\treturn y, nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"unsupported binary arithmetic operator: %q\", op)\n\t}\n}\n"
  },
  {
    "path": "expand/braces.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// Braces performs brace expansion on a word, given that it contains any\n// [syntax.BraceExp] parts. For example, the word with a brace expansion\n// \"foo{bar,baz}\" will return two literal words, \"foobar\" and \"foobaz\".\n//\n// Note that the resulting words may share word parts.\nfunc Braces(word *syntax.Word) []*syntax.Word {\n\tvar all []*syntax.Word\n\tvar left []syntax.WordPart\n\tfor i, wp := range word.Parts {\n\t\tbr, ok := wp.(*syntax.BraceExp)\n\t\tif !ok {\n\t\t\tleft = append(left, wp)\n\t\t\tcontinue\n\t\t}\n\t\tif br.Sequence {\n\t\t\tchars := false\n\n\t\t\tfromLit := br.Elems[0].Lit()\n\t\t\ttoLit := br.Elems[1].Lit()\n\t\t\tzeros := max(extraLeadingZeros(fromLit), extraLeadingZeros(toLit))\n\n\t\t\tfrom, err1 := strconv.Atoi(fromLit)\n\t\t\tto, err2 := strconv.Atoi(toLit)\n\t\t\tif err1 != nil || err2 != nil {\n\t\t\t\tchars = true\n\t\t\t\tfrom = int(br.Elems[0].Lit()[0])\n\t\t\t\tto = int(br.Elems[1].Lit()[0])\n\t\t\t}\n\t\t\tupward := from <= to\n\t\t\tincr := 1\n\t\t\tif !upward {\n\t\t\t\tincr = -1\n\t\t\t}\n\t\t\tif len(br.Elems) > 2 {\n\t\t\t\tn, _ := strconv.Atoi(br.Elems[2].Lit())\n\t\t\t\tif n != 0 && n > 0 == upward {\n\t\t\t\t\tincr = n\n\t\t\t\t}\n\t\t\t}\n\t\t\tn := from\n\t\t\tfor {\n\t\t\t\tif upward && n > to {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif !upward && n < to {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tnext := *word\n\t\t\t\tnext.Parts = next.Parts[i+1:]\n\t\t\t\tlit := &syntax.Lit{}\n\t\t\t\tif chars {\n\t\t\t\t\tlit.Value = string(rune(n))\n\t\t\t\t} else {\n\t\t\t\t\tlit.Value = strings.Repeat(\"0\", zeros) + strconv.Itoa(n)\n\t\t\t\t}\n\t\t\t\tnext.Parts = append([]syntax.WordPart{lit}, next.Parts...)\n\t\t\t\texp := Braces(&next)\n\t\t\t\tfor _, w := range exp {\n\t\t\t\t\tw.Parts = append(left, w.Parts...)\n\t\t\t\t}\n\t\t\t\tall = append(all, exp...)\n\t\t\t\tn += incr\n\t\t\t}\n\t\t\treturn all\n\t\t}\n\t\tfor _, elem := range br.Elems {\n\t\t\tnext := *word\n\t\t\tnext.Parts = next.Parts[i+1:]\n\t\t\tnext.Parts = append(elem.Parts, next.Parts...)\n\t\t\texp := Braces(&next)\n\t\t\tfor _, w := range exp {\n\t\t\t\tw.Parts = append(left, w.Parts...)\n\t\t\t}\n\t\t\tall = append(all, exp...)\n\t\t}\n\t\treturn all\n\t}\n\treturn []*syntax.Word{{Parts: left}}\n}\n\nfunc extraLeadingZeros(s string) int {\n\tfor i, r := range s {\n\t\tif r != '0' {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn 0 // \"0\" has no extra leading zeros\n}\n"
  },
  {
    "path": "expand/braces_test.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc lit(s string) *syntax.Lit                { return &syntax.Lit{Value: s} }\nfunc word(ps ...syntax.WordPart) *syntax.Word { return &syntax.Word{Parts: ps} }\nfunc litWord(s string) *syntax.Word           { return word(lit(s)) }\nfunc litWords(strs ...string) []*syntax.Word {\n\tl := make([]*syntax.Word, 0, len(strs))\n\tfor _, s := range strs {\n\t\tl = append(l, litWord(s))\n\t}\n\treturn l\n}\n\nvar braceTests = []struct {\n\tin   *syntax.Word\n\twant []*syntax.Word\n}{\n\t{\n\t\tlitWord(\"a{b\"),\n\t\tlitWords(\"a{b\"),\n\t},\n\t{\n\t\tlitWord(\"a}b\"),\n\t\tlitWords(\"a}b\"),\n\t},\n\t{\n\t\tlitWord(\"{a,b{c,d}\"),\n\t\tlitWords(\"{a,bc\", \"{a,bd\"),\n\t},\n\t{\n\t\tlitWord(\"{a{b\"),\n\t\tlitWords(\"{a{b\"),\n\t},\n\t{\n\t\tlitWord(\"a{}\"),\n\t\tlitWords(\"a{}\"),\n\t},\n\t{\n\t\tlitWord(\"a{b}\"),\n\t\tlitWords(\"a{b}\"),\n\t},\n\t{\n\t\tlitWord(\"a{b,c}\"),\n\t\tlitWords(\"ab\", \"ac\"),\n\t},\n\t{\n\t\tlitWord(\"a{à,世界}\"),\n\t\tlitWords(\"aà\", \"a世界\"),\n\t},\n\t{\n\t\tlitWord(\"a{b,c}d{e,f}g\"),\n\t\tlitWords(\"abdeg\", \"abdfg\", \"acdeg\", \"acdfg\"),\n\t},\n\t{\n\t\tlitWord(\"a{b{x,y},c}d\"),\n\t\tlitWords(\"abxd\", \"abyd\", \"acd\"),\n\t},\n\t{\n\t\tlitWord(\"a{1,2,3,4,5}\"),\n\t\tlitWords(\"a1\", \"a2\", \"a3\", \"a4\", \"a5\"),\n\t},\n\t{\n\t\tlitWord(\"a{1..\"),\n\t\tlitWords(\"a{1..\"),\n\t},\n\t{\n\t\tlitWord(\"a{1..4\"),\n\t\tlitWords(\"a{1..4\"),\n\t},\n\t{\n\t\tlitWord(\"a{1.4}\"),\n\t\tlitWords(\"a{1.4}\"),\n\t},\n\t{\n\t\tlitWord(\"{a,b}{1..4\"),\n\t\tlitWords(\"a{1..4\", \"b{1..4\"),\n\t},\n\t{\n\t\tlitWord(\"a{1..4}\"),\n\t\tlitWords(\"a1\", \"a2\", \"a3\", \"a4\"),\n\t},\n\t{\n\t\tlitWord(\"a{1..2}b{4..5}c\"),\n\t\tlitWords(\"a1b4c\", \"a1b5c\", \"a2b4c\", \"a2b5c\"),\n\t},\n\t{\n\t\tlitWord(\"a{1..f}\"),\n\t\tlitWords(\"a{1..f}\"),\n\t},\n\t{\n\t\tlitWord(\"a{c..f}\"),\n\t\tlitWords(\"ac\", \"ad\", \"ae\", \"af\"),\n\t},\n\t{\n\t\tlitWord(\"a{H..K}\"),\n\t\tlitWords(\"aH\", \"aI\", \"aJ\", \"aK\"),\n\t},\n\t{\n\t\tlitWord(\"a{-..f}\"),\n\t\tlitWords(\"a{-..f}\"),\n\t},\n\t{\n\t\tlitWord(\"a{3..-}\"),\n\t\tlitWords(\"a{3..-}\"),\n\t},\n\t{\n\t\tlitWord(\"a{1..10..3}\"),\n\t\tlitWords(\"a1\", \"a4\", \"a7\", \"a10\"),\n\t},\n\t{\n\t\tlitWord(\"a{1..4..0}\"),\n\t\tlitWords(\"a1\", \"a2\", \"a3\", \"a4\"),\n\t},\n\t{\n\t\tlitWord(\"a{4..1}\"),\n\t\tlitWords(\"a4\", \"a3\", \"a2\", \"a1\"),\n\t},\n\t{\n\t\tlitWord(\"a{4..1..-2}\"),\n\t\tlitWords(\"a4\", \"a2\"),\n\t},\n\t{\n\t\tlitWord(\"a{4..1..1}\"),\n\t\tlitWords(\"a4\", \"a3\", \"a2\", \"a1\"),\n\t},\n\t{\n\t\tlitWord(\"{1..005}\"),\n\t\tlitWords(\"001\", \"002\", \"003\", \"004\", \"005\"),\n\t},\n\t{\n\t\tlitWord(\"{0001..05..2}\"),\n\t\tlitWords(\"0001\", \"0003\", \"0005\"),\n\t},\n\t{\n\t\tlitWord(\"{0..1}\"),\n\t\tlitWords(\"0\", \"1\"),\n\t},\n\t{\n\t\tlitWord(\"a{d..k..3}\"),\n\t\tlitWords(\"ad\", \"ag\", \"aj\"),\n\t},\n\t{\n\t\tlitWord(\"a{d..k..n}\"),\n\t\tlitWords(\"a{d..k..n}\"),\n\t},\n\t{\n\t\tlitWord(\"a{k..d..-2}\"),\n\t\tlitWords(\"ak\", \"ai\", \"ag\", \"ae\"),\n\t},\n\t{\n\t\tlitWord(\"{1..1}\"),\n\t\tlitWords(\"1\"),\n\t},\n}\n\nfunc TestBraces(t *testing.T) {\n\tt.Parallel()\n\tfor _, tc := range braceTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tinStr := printWords(tc.in)\n\t\t\twantStr := printWords(tc.want...)\n\t\t\twantBraceExpParts(t, tc.in, false)\n\n\t\t\tinBraces := *tc.in\n\t\t\tsyntax.SplitBraces(&inBraces)\n\t\t\twantBraceExpParts(t, &inBraces, inStr != wantStr)\n\n\t\t\tgot := Braces(&inBraces)\n\t\t\tgotStr := printWords(got...)\n\t\t\tif gotStr != wantStr {\n\t\t\t\tt.Fatalf(\"mismatch in %q\\nwant:\\n%s\\ngot: %s\",\n\t\t\t\t\tinStr, wantStr, gotStr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc wantBraceExpParts(t *testing.T, word *syntax.Word, want bool) {\n\tt.Helper()\n\tanyBrace := false\n\tfor _, part := range word.Parts {\n\t\tif _, anyBrace = part.(*syntax.BraceExp); anyBrace {\n\t\t\tbreak\n\t\t}\n\t}\n\tif anyBrace && !want {\n\t\tt.Fatalf(\"didn't want any BraceExp node, but found one\")\n\t} else if !anyBrace && want {\n\t\tt.Fatalf(\"wanted a BraceExp node, but found none\")\n\t}\n}\n\nfunc printWords(words ...*syntax.Word) string {\n\tp := syntax.NewPrinter()\n\tvar buf bytes.Buffer\n\tcall := &syntax.CallExpr{Args: words}\n\tp.Print(&buf, call)\n\treturn buf.String()\n}\n"
  },
  {
    "path": "expand/doc.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package expand contains code to perform various shell expansions.\npackage expand\n"
  },
  {
    "path": "expand/environ.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"cmp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n)\n\n// Environ is the base interface for a shell's environment, allowing it to fetch\n// variables by name and to iterate over all the currently set variables.\ntype Environ interface {\n\t// Get retrieves a variable by its name. To check if the variable is\n\t// set, use Variable.IsSet.\n\tGet(name string) Variable\n\n\t// TODO(v4): make Each below a func that returns an iterator.\n\n\t// Each iterates over all the currently set variables, calling the\n\t// supplied function on each variable. Iteration is stopped if the\n\t// function returns false.\n\t//\n\t// The names used in the calls aren't required to be unique or sorted.\n\t// If a variable name appears twice, the latest occurrence takes\n\t// priority.\n\t//\n\t// Each is required to forward exported variables when executing\n\t// programs.\n\tEach(func(name string, vr Variable) bool)\n}\n\n// TODO(v4): [WriteEnviron.Set] below is overloaded to the point that correctly\n// implementing both sides of the interface is tricky. In particular, some operations\n// such as `export foo` or `readonly foo` alter the attributes but not the value,\n// and `foo=bar` or `foo=[3]=baz` alter the value but not the attributes.\n\n// WriteEnviron is an extension on Environ that supports modifying and deleting\n// variables.\ntype WriteEnviron interface {\n\tEnviron\n\t// Set sets a variable by name. If !vr.IsSet(), the variable is being\n\t// unset; otherwise, the variable is being replaced.\n\t//\n\t// The given variable can have the kind [KeepValue] to replace an existing\n\t// variable's attributes without changing its value at all.\n\t// This is helpful to implement `readonly foo=bar; export foo`,\n\t// as the second declaration needs to clearly signal that the value is not modified.\n\t//\n\t// An error may be returned if the operation is invalid, such as if the\n\t// name is empty or if we're trying to overwrite a read-only variable.\n\tSet(name string, vr Variable) error\n}\n\n//go:generate go tool stringer -type=ValueKind\n\n// ValueKind describes which kind of value the variable holds.\n// While most unset variables will have an [Unknown] kind, an unset variable may\n// have a kind associated too, such as via `declare -a foo` resulting in [Indexed].\ntype ValueKind uint8\n\nconst (\n\t// Unknown is used for unset variables which do not have a kind yet.\n\tUnknown ValueKind = iota\n\t// String describes plain string variables, such as `foo=bar`.\n\tString\n\t// NameRef describes variables which reference another by name, such as `declare -n foo=foo2`.\n\tNameRef\n\t// Indexed describes indexed array variables, such as `foo=(bar baz)`.\n\tIndexed\n\t// Associative describes associative array variables, such as `foo=([bar]=x [baz]=y)`.\n\tAssociative\n\n\t// KeepValue is used by [WriteEnviron.Set] to signal that we are changing attributes\n\t// about a variable, such as exporting it, without changing its value at all.\n\tKeepValue\n\n\t// Deprecated: use [Unknown], as tracking whether or not a variable is set\n\t// is now done via [Variable.Set].\n\t// Otherwise it was impossible to describe an unset variable with a known kind\n\t// such as `declare -A foo`.\n\tUnset = Unknown\n)\n\n// Variable describes a shell variable, which can have a number of attributes\n// and a value.\ntype Variable struct {\n\t// Set is true when the variable has been set to a value,\n\t// which may be empty.\n\tSet bool\n\n\tLocal    bool\n\tExported bool\n\tReadOnly bool\n\n\t// Kind defines which of the value fields below should be used.\n\tKind ValueKind\n\n\tStr  string            // Used when Kind is String or NameRef.\n\tList []string          // Used when Kind is Indexed.\n\tMap  map[string]string // Used when Kind is Associative.\n}\n\n// IsSet reports whether the variable has been set to a value.\n// The zero value of a Variable is unset.\nfunc (v Variable) IsSet() bool {\n\treturn v.Set\n}\n\n// Declared reports whether the variable has been declared.\n// Declared variables may not be set; `export foo` is exported but not set to a value,\n// and `declare -a foo` is an indexed array but not set to a value.\nfunc (v Variable) Declared() bool {\n\treturn v.Set || v.Local || v.Exported || v.ReadOnly || v.Kind != Unknown\n}\n\n// Flags returns the variable's attribute flags in the order used by bash's\n// declare builtin and ${var@a}: type (a/A/n), readonly (r), exported (x).\nfunc (v Variable) Flags() string {\n\tvar flags []byte\n\tswitch v.Kind {\n\tcase Indexed:\n\t\tflags = append(flags, 'a')\n\tcase Associative:\n\t\tflags = append(flags, 'A')\n\tcase NameRef:\n\t\tflags = append(flags, 'n')\n\t}\n\tif v.ReadOnly {\n\t\tflags = append(flags, 'r')\n\t}\n\tif v.Exported {\n\t\tflags = append(flags, 'x')\n\t}\n\treturn string(flags)\n}\n\n// String returns the variable's value as a string. In general, this only makes\n// sense if the variable has a string value or no value at all.\nfunc (v Variable) String() string {\n\tswitch v.Kind {\n\tcase String:\n\t\treturn v.Str\n\tcase Indexed:\n\t\tif len(v.List) > 0 {\n\t\t\treturn v.List[0]\n\t\t}\n\tcase Associative:\n\t\t// nothing to do\n\t}\n\treturn \"\"\n}\n\n// maxNameRefDepth defines the maximum number of times to follow references when\n// resolving a variable. Otherwise, simple name reference loops could crash a\n// program quite easily.\nconst maxNameRefDepth = 100\n\n// Resolve follows a number of nameref variables, returning the last reference\n// name that was followed and the variable that it points to.\nfunc (v Variable) Resolve(env Environ) (string, Variable) {\n\tname := \"\"\n\tfor range maxNameRefDepth {\n\t\tif v.Kind != NameRef {\n\t\t\treturn name, v\n\t\t}\n\t\tname = v.Str // keep name for the next iteration\n\t\tv = env.Get(name)\n\t}\n\treturn name, Variable{}\n}\n\n// FuncEnviron wraps a function mapping variable names to their string values,\n// and implements [Environ]. Empty strings returned by the function will be\n// treated as unset variables. All variables will be exported.\n//\n// Note that the returned Environ's Each method will be a no-op.\nfunc FuncEnviron(fn func(string) string) Environ {\n\treturn funcEnviron(fn)\n}\n\ntype funcEnviron func(string) string\n\nfunc (f funcEnviron) Get(name string) Variable {\n\tvalue := f(name)\n\tif value == \"\" {\n\t\treturn Variable{}\n\t}\n\treturn Variable{Set: true, Exported: true, Kind: String, Str: value}\n}\n\nfunc (f funcEnviron) Each(func(name string, vr Variable) bool) {}\n\n// ListEnviron returns an [Environ] with the supplied variables, in the form\n// \"key=value\". All variables will be exported. The last value in pairs is used\n// if multiple values are present.\n//\n// On Windows, where environment variable names are case-insensitive, the\n// resulting variable names will all be uppercase.\nfunc ListEnviron(pairs ...string) Environ {\n\treturn listEnviron_(runtime.GOOS == \"windows\", pairs...)\n}\n\n// listEnviron_ implements [ListEnviron], but letting the tests specify\n// whether to uppercase all names or not.\nfunc listEnviron_(caseInsensitive bool, pairs ...string) Environ {\n\tlist := slices.Clone(pairs)\n\tenv := listEnviron{caseInsensitive: caseInsensitive}\n\tslices.SortStableFunc(list, func(a, b string) int {\n\t\tisep := strings.IndexByte(a, '=')\n\t\tjsep := strings.IndexByte(b, '=')\n\t\tif isep < 0 {\n\t\t\tisep = 0\n\t\t} else {\n\t\t\tisep += 1\n\t\t}\n\t\tif jsep < 0 {\n\t\t\tjsep = 0\n\t\t} else {\n\t\t\tjsep += 1\n\t\t}\n\t\treturn env.compare(a[:isep], b[:jsep])\n\t})\n\n\tlast := \"\"\n\tfor i := 0; i < len(list); {\n\t\tname, _, ok := strings.Cut(list[i], \"=\")\n\t\tif name == \"\" || !ok {\n\t\t\t// invalid element; remove it\n\t\t\tlist = slices.Delete(list, i, i+1)\n\t\t\tcontinue\n\t\t}\n\t\tif env.compare(last, name) == 0 {\n\t\t\t// duplicate; the last one wins\n\t\t\tlist = slices.Delete(list, i-1, i)\n\t\t\tcontinue\n\t\t}\n\t\tlast = name\n\t\ti++\n\t}\n\tenv.pairs = list\n\treturn env\n}\n\n// listEnviron is a sorted list of \"name=value\" strings.\ntype listEnviron struct {\n\tcaseInsensitive bool\n\tpairs           []string\n}\n\nfunc (l listEnviron) compare(a, b string) int {\n\tif l.caseInsensitive {\n\t\t// This is not particularly efficient, but it does the job.\n\t\t// If we had a cmp-compatible version of [strings.EqualFold], we'd use it.\n\t\ta = strings.ToUpper(a)\n\t\tb = strings.ToUpper(b)\n\t}\n\treturn strings.Compare(a, b)\n}\n\nfunc (l listEnviron) Get(name string) Variable {\n\teqpos := len(name)\n\tendpos := len(name) + 1\n\ti, ok := slices.BinarySearchFunc(l.pairs, name, func(pair, name string) int {\n\t\tif len(pair) < endpos {\n\t\t\t// Too short; see if we are before or after the name.\n\t\t\treturn l.compare(pair, name)\n\t\t}\n\t\t// Compare the name prefix, then the equal character.\n\t\tc := l.compare(pair[:eqpos], name)\n\t\teq := pair[eqpos]\n\t\tif c == 0 {\n\t\t\treturn cmp.Compare(eq, '=')\n\t\t}\n\t\treturn c\n\t})\n\tif ok {\n\t\treturn Variable{Set: true, Exported: true, Kind: String, Str: l.pairs[i][endpos:]}\n\t}\n\treturn Variable{}\n}\n\nfunc (l listEnviron) Each(fn func(name string, vr Variable) bool) {\n\tfor _, pair := range l.pairs {\n\t\tname, value, ok := strings.Cut(pair, \"=\")\n\t\tif !ok {\n\t\t\t// should never happen; see listEnvironWithUpper\n\t\t\tpanic(\"expand.listEnviron: did not expect malformed name-value pair: \" + pair)\n\t\t}\n\t\tif !fn(name, Variable{Set: true, Exported: true, Kind: String, Str: value}) {\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "expand/environ_test.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestListEnviron(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinsensitive bool\n\t\tpairs       []string\n\t\twant        []string\n\t}{\n\t\t{\n\t\t\tname:  \"Empty\",\n\t\t\tpairs: nil,\n\t\t\twant:  nil,\n\t\t},\n\t\t{\n\t\t\tname:  \"Simple\",\n\t\t\tpairs: []string{\"A=b\", \"c=\"},\n\t\t\twant:  []string{\"A=b\", \"c=\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"MissingEqual\",\n\t\t\tpairs: []string{\"A=b\", \"invalid\", \"c=\"},\n\t\t\twant:  []string{\"A=b\", \"c=\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"DuplicateNames\",\n\t\t\tpairs: []string{\"A=x\", \"A=b\", \"c=\", \"c=y\"},\n\t\t\twant:  []string{\"A=b\", \"c=y\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"NoName\",\n\t\t\tpairs: []string{\"=b\", \"=c\"},\n\t\t\twant:  []string{},\n\t\t},\n\t\t{\n\t\t\tname:  \"EmptyElements\",\n\t\t\tpairs: []string{\"A=b\", \"\", \"\", \"c=\"},\n\t\t\twant:  []string{\"A=b\", \"c=\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"MixedCaseNoInsensitive\",\n\t\t\tpairs: []string{\"A=b1\", \"Path=foo\", \"a=b2\"},\n\t\t\twant:  []string{\"A=b1\", \"Path=foo\", \"a=b2\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"MixedCaseInsensitive\",\n\t\t\tinsensitive: true,\n\t\t\tpairs:       []string{\"A=b1\", \"Path=foo\", \"a=b2\"},\n\t\t\twant:        []string{\"a=b2\", \"Path=foo\"},\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgotEnv := listEnviron_(tc.insensitive, tc.pairs...)\n\t\t\tgot := gotEnv.(listEnviron).pairs\n\t\t\tif !reflect.DeepEqual(got, tc.want) {\n\t\t\t\tt.Fatalf(\"ListEnviron(%t, %q) wanted %#v, got %#v\",\n\t\t\t\t\ttc.insensitive, tc.pairs, tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetWithSameSubPrefix(t *testing.T) {\n\tgotEnv := ListEnviron(\"GREETING=text1\", \"GREETING2=text2\")\n\tgot := gotEnv.Get(\"GREETING2\").String()\n\tif got != \"text2\" {\n\t\tt.Fatalf(\"ListEnviron.Get(GREETING2) wanted text2, got %q\", got)\n\t}\n\tgot = gotEnv.Get(\"GREETING\").String()\n\tif got != \"text1\" {\n\t\tt.Fatalf(\"ListEnviron.Get(GREETING) wanted text1, got %q\", got)\n\t}\n}\n"
  },
  {
    "path": "expand/expand.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"cmp\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"iter\"\n\t\"maps\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/internal\"\n\t\"mvdan.cc/sh/v3/pattern\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// A Config specifies details about how shell expansion should be performed. The\n// zero value is a valid configuration.\ntype Config struct {\n\t// Env is used to get and set environment variables when performing\n\t// shell expansions. Some special parameters are also expanded via this\n\t// interface, such as:\n\t//\n\t//   * \"#\", \"@\", \"*\", \"0\"-\"9\" for the shell's parameters\n\t//   * \"?\", \"$\", \"PPID\" for the shell's status and process\n\t//   * \"HOME foo\" to retrieve user foo's home directory (if unset,\n\t//     os/user.Lookup will be used)\n\t//\n\t// If nil, there are no environment variables set. Use\n\t// ListEnviron(os.Environ()...) to use the system's environment\n\t// variables.\n\tEnv Environ\n\n\t// CmdSubst expands a command substitution node, writing its standard\n\t// output to the provided [io.Writer].\n\t//\n\t// If nil, encountering a command substitution will result in an\n\t// UnexpectedCommandError.\n\tCmdSubst func(io.Writer, *syntax.CmdSubst) error\n\n\t// ProcSubst expands a process substitution node.\n\tProcSubst func(*syntax.ProcSubst) (string, error)\n\n\t// TODO(v4): replace ReadDir with ReadDir2.\n\n\t// ReadDir is the older form of [ReadDir2], before io/fs.\n\t//\n\t// Deprecated: use ReadDir2 instead.\n\tReadDir func(string) ([]fs.FileInfo, error)\n\n\t// ReadDir2 is used for file path globbing.\n\t// If nil, and [ReadDir] is nil as well, globbing is disabled.\n\t// Use [os.ReadDir] to use the filesystem directly.\n\tReadDir2 func(string) ([]fs.DirEntry, error)\n\n\t// GlobStar corresponds to the shell option which allows globbing with \"**\".\n\tGlobStar bool\n\n\t// DotGlob corresponds to the shell option which allows filenames beginning\n\t// with a dot to be matched by a pattern which does not begin with a dot.\n\tDotGlob bool\n\n\t// NoCaseGlob corresponds to the shell option which causes case-insensitive\n\t// pattern matching in pathname expansion.\n\tNoCaseGlob bool\n\n\t// NullGlob corresponds to the shell option which allows globbing\n\t// patterns which match nothing to result in zero fields.\n\tNullGlob bool\n\n\t// NoUnset corresponds to the shell option which treats unset variables\n\t// as errors.\n\tNoUnset bool\n\n\t// ExtGlob corresponds to the shell option which allows using extended\n\t// pattern matching features when performing pathname expansion (globbing).\n\tExtGlob bool\n\n\tbufferAlloc strings.Builder\n\tfieldAlloc  [4]fieldPart\n\tfieldsAlloc [4][]fieldPart\n\n\tifs string\n\t// A pointer to a parameter expansion node, if we're inside one.\n\t// Necessary for ${LINENO}.\n\tcurParam *syntax.ParamExp\n}\n\n// UnexpectedCommandError is returned if a command substitution is encountered\n// when [Config.CmdSubst] is nil.\ntype UnexpectedCommandError struct {\n\tNode *syntax.CmdSubst\n}\n\nfunc (u UnexpectedCommandError) Error() string {\n\treturn fmt.Sprintf(\"unexpected command substitution at %s\", u.Node.Pos())\n}\n\nvar zeroConfig = &Config{}\n\n// TODO: note that prepareConfig is modifying the user's config in place,\n// which doesn't feel right - we should make a copy.\n\nfunc prepareConfig(cfg *Config) *Config {\n\tcfg = cmp.Or(cfg, zeroConfig)\n\tcfg.Env = cmp.Or(cfg.Env, FuncEnviron(func(string) string { return \"\" }))\n\n\tcfg.ifs = \" \\t\\n\"\n\tif vr := cfg.Env.Get(\"IFS\"); vr.IsSet() {\n\t\tcfg.ifs = vr.String()\n\t}\n\n\tif cfg.ReadDir != nil && cfg.ReadDir2 == nil {\n\t\tcfg.ReadDir2 = func(path string) ([]fs.DirEntry, error) {\n\t\t\tinfos, err := cfg.ReadDir(path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tentries := make([]fs.DirEntry, len(infos))\n\t\t\tfor i, info := range infos {\n\t\t\t\tentries[i] = fs.FileInfoToDirEntry(info)\n\t\t\t}\n\t\t\treturn entries, nil\n\t\t}\n\t}\n\treturn cfg\n}\n\nfunc (cfg *Config) ifsRune(r rune) bool {\n\tfor _, r2 := range cfg.ifs {\n\t\tif r == r2 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (cfg *Config) ifsJoin(strs []string) string {\n\tsep := \"\"\n\tif cfg.ifs != \"\" {\n\t\tsep = cfg.ifs[:1]\n\t}\n\treturn strings.Join(strs, sep)\n}\n\nfunc (cfg *Config) strBuilder() *strings.Builder {\n\tb := &cfg.bufferAlloc\n\tb.Reset()\n\treturn b\n}\n\nfunc (cfg *Config) envGet(name string) string {\n\treturn cfg.Env.Get(name).String()\n}\n\nfunc (cfg *Config) envSet(name, value string) error {\n\twenv, ok := cfg.Env.(WriteEnviron)\n\tif !ok {\n\t\treturn fmt.Errorf(\"environment is read-only\")\n\t}\n\treturn wenv.Set(name, Variable{Set: true, Kind: String, Str: value})\n}\n\n// Literal expands a single shell word. It is similar to [Fields], but the result\n// is a single string. This is the behavior when a word is used as the value in\n// a shell variable assignment, for example.\n//\n// The config specifies shell expansion options; nil behaves the same as an\n// empty config.\nfunc Literal(cfg *Config, word *syntax.Word) (string, error) {\n\tif word == nil {\n\t\treturn \"\", nil\n\t}\n\tcfg = prepareConfig(cfg)\n\tfield, err := cfg.wordField(word.Parts, quoteNone)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cfg.fieldJoin(field), nil\n}\n\n// Document expands a single shell word as if it were a here-document body.\n// It is similar to [Literal], but without brace expansion, tilde expansion, and\n// globbing.\n//\n// The config specifies shell expansion options; nil behaves the same as an\n// empty config.\nfunc Document(cfg *Config, word *syntax.Word) (string, error) {\n\tif word == nil {\n\t\treturn \"\", nil\n\t}\n\tcfg = prepareConfig(cfg)\n\tfield, err := cfg.wordField(word.Parts, quoteHeredoc)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cfg.fieldJoin(field), nil\n}\n\n// Pattern expands a single shell word as a pattern, using [pattern.QuoteMeta]\n// on any non-quoted parts of the input word. The result can be used on\n// [pattern.Regexp] directly.\n//\n// The config specifies shell expansion options; nil behaves the same as an\n// empty config.\nfunc Pattern(cfg *Config, word *syntax.Word) (string, error) {\n\tif word == nil {\n\t\treturn \"\", nil\n\t}\n\tcfg = prepareConfig(cfg)\n\tfield, err := cfg.wordField(word.Parts, quoteNone)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tsb := cfg.strBuilder()\n\tfor _, part := range field {\n\t\tif part.quote > quoteNone {\n\t\t\tsb.WriteString(pattern.QuoteMeta(part.val, 0))\n\t\t} else {\n\t\t\tsb.WriteString(part.val)\n\t\t}\n\t}\n\treturn sb.String(), nil\n}\n\n// Format expands a format string with a number of arguments, following the\n// shell's format specifications. These include printf(1), among others.\n//\n// The resulting string is returned, along with the number of arguments used.\n// Note that the resulting string may contain null bytes, for example\n// if the format string used `\\x00`. The caller should terminate the string\n// at the first null byte if needed, such as when expanding for `$'foo\\x00bar'`.\n//\n// The config specifies shell expansion options; nil behaves the same as an\n// empty config.\nfunc Format(cfg *Config, format string, args []string) (string, int, error) {\n\tcfg = prepareConfig(cfg)\n\tsb := cfg.strBuilder()\n\n\tconsumed, err := formatInto(sb, format, args)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\n\treturn sb.String(), consumed, err\n}\n\nfunc formatInto(sb *strings.Builder, format string, args []string) (int, error) {\n\tvar fmts []byte\n\tinitialArgs := len(args)\n\n\tfor i := 0; i < len(format); i++ {\n\t\t// readDigits reads from 0 to max digits, either octal or\n\t\t// hexadecimal.\n\t\treadDigits := func(max int, hex bool) string {\n\t\t\tj := 0\n\t\t\tfor ; j < max && i+j < len(format); j++ {\n\t\t\t\tc := format[i+j]\n\t\t\t\tif (c >= '0' && c <= '9') ||\n\t\t\t\t\t(hex && c >= 'a' && c <= 'f') ||\n\t\t\t\t\t(hex && c >= 'A' && c <= 'F') {\n\t\t\t\t\t// valid octal or hex char\n\t\t\t\t} else {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tdigits := format[i : i+j]\n\t\t\ti += j - 1 // -1 since the outer loop does i++\n\t\t\treturn digits\n\t\t}\n\t\tc := format[i]\n\t\tswitch {\n\t\tcase c == '\\\\': // escaped\n\t\t\ti++\n\t\t\tif i >= len(format) {\n\t\t\t\tsb.WriteByte('\\\\')\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tswitch c = format[i]; c {\n\t\t\tcase 'a': // bell\n\t\t\t\tsb.WriteByte('\\a')\n\t\t\tcase 'b': // backspace\n\t\t\t\tsb.WriteByte('\\b')\n\t\t\tcase 'e', 'E': // escape\n\t\t\t\tsb.WriteByte('\\x1b')\n\t\t\tcase 'f': // form feed\n\t\t\t\tsb.WriteByte('\\f')\n\t\t\tcase 'n': // new line\n\t\t\t\tsb.WriteByte('\\n')\n\t\t\tcase 'r': // carriage return\n\t\t\t\tsb.WriteByte('\\r')\n\t\t\tcase 't': // horizontal tab\n\t\t\t\tsb.WriteByte('\\t')\n\t\t\tcase 'v': // vertical tab\n\t\t\t\tsb.WriteByte('\\v')\n\t\t\tcase '\\\\', '\\'', '\"', '?': // just the character\n\t\t\t\tsb.WriteByte(c)\n\t\t\tcase '0', '1', '2', '3', '4', '5', '6', '7':\n\t\t\t\tdigits := readDigits(3, false)\n\t\t\t\t// if digits don't fit in 8 bits, 0xff via strconv\n\t\t\t\tn, _ := strconv.ParseUint(digits, 8, 8)\n\t\t\t\tsb.WriteByte(byte(n))\n\t\t\tcase 'x', 'u', 'U':\n\t\t\t\ti++\n\t\t\t\tmax := 2\n\t\t\t\tswitch c {\n\t\t\t\tcase 'u':\n\t\t\t\t\tmax = 4\n\t\t\t\tcase 'U':\n\t\t\t\t\tmax = 8\n\t\t\t\t}\n\t\t\t\tdigits := readDigits(max, true)\n\t\t\t\tif len(digits) > 0 {\n\t\t\t\t\t// can't error\n\t\t\t\t\tn, _ := strconv.ParseUint(digits, 16, 32)\n\t\t\t\t\tif c == 'x' {\n\t\t\t\t\t\t// always as a single byte\n\t\t\t\t\t\tsb.WriteByte(byte(n))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsb.WriteRune(rune(n))\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tfallthrough\n\t\t\tdefault: // no escape sequence\n\t\t\t\tsb.WriteByte('\\\\')\n\t\t\t\tsb.WriteByte(c)\n\t\t\t}\n\t\tcase len(fmts) > 0:\n\t\t\tswitch c {\n\t\t\tcase '%':\n\t\t\t\tsb.WriteByte('%')\n\t\t\t\tfmts = nil\n\t\t\tcase 'c':\n\t\t\t\tvar b byte\n\t\t\t\tif len(args) > 0 {\n\t\t\t\t\targ := \"\"\n\t\t\t\t\targ, args = args[0], args[1:]\n\t\t\t\t\tif len(arg) > 0 {\n\t\t\t\t\t\tb = arg[0]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsb.WriteByte(b)\n\t\t\t\tfmts = nil\n\t\t\tcase '+', '-', ' ':\n\t\t\t\tif len(fmts) > 1 {\n\t\t\t\t\treturn 0, fmt.Errorf(\"invalid format char: %c\", c)\n\t\t\t\t}\n\t\t\t\tfmts = append(fmts, c)\n\t\t\tcase '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':\n\t\t\t\tfmts = append(fmts, c)\n\t\t\tcase 's', 'b', 'd', 'i', 'u', 'o', 'x':\n\t\t\t\targ := \"\"\n\t\t\t\tif len(args) > 0 {\n\t\t\t\t\targ, args = args[0], args[1:]\n\t\t\t\t}\n\t\t\t\tvar farg any\n\t\t\t\tif c == 'b' {\n\t\t\t\t\t// Passing in nil for args ensures that % format\n\t\t\t\t\t// strings aren't processed; only escape sequences\n\t\t\t\t\t// will be handled.\n\t\t\t\t\t_, err := formatInto(sb, arg, nil)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn 0, err\n\t\t\t\t\t}\n\t\t\t\t} else if c != 's' {\n\t\t\t\t\tn, _ := strconv.ParseInt(arg, 0, 0)\n\t\t\t\t\tif c == 'i' || c == 'd' {\n\t\t\t\t\t\tfarg = int(n)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfarg = uint(n)\n\t\t\t\t\t}\n\t\t\t\t\tif c == 'i' || c == 'u' {\n\t\t\t\t\t\tc = 'd'\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tfarg = arg\n\t\t\t\t}\n\t\t\t\tif farg != nil {\n\t\t\t\t\tfmts = append(fmts, c)\n\t\t\t\t\tfmt.Fprintf(sb, string(fmts), farg)\n\t\t\t\t}\n\t\t\t\tfmts = nil\n\t\t\tdefault:\n\t\t\t\treturn 0, fmt.Errorf(\"invalid format char: %c\", c)\n\t\t\t}\n\t\tcase args != nil && c == '%':\n\t\t\t// if args == nil, we are not doing format\n\t\t\t// arguments\n\t\t\tfmts = []byte{c}\n\t\tdefault:\n\t\t\tsb.WriteByte(c)\n\t\t}\n\t}\n\tif len(fmts) > 0 {\n\t\treturn 0, fmt.Errorf(\"missing format char\")\n\t}\n\treturn initialArgs - len(args), nil\n}\n\nfunc (cfg *Config) fieldJoin(parts []fieldPart) string {\n\tswitch len(parts) {\n\tcase 0:\n\t\treturn \"\"\n\tcase 1: // short-cut without a string copy\n\t\treturn parts[0].val\n\t}\n\tsb := cfg.strBuilder()\n\tfor _, part := range parts {\n\t\tsb.WriteString(part.val)\n\t}\n\treturn sb.String()\n}\n\nfunc (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob bool) {\n\tsb := cfg.strBuilder()\n\tfor _, part := range parts {\n\t\tif part.quote > quoteNone {\n\t\t\tsb.WriteString(pattern.QuoteMeta(part.val, 0))\n\t\t\tcontinue\n\t\t}\n\t\tsb.WriteString(part.val)\n\t\tif pattern.HasMeta(part.val, 0) {\n\t\t\tglob = true\n\t\t}\n\t}\n\tif glob { // only copy the string if it will be used\n\t\tescaped = sb.String()\n\t}\n\treturn escaped, glob\n}\n\n// Fields is a pre-iterators API which now wraps [FieldsSeq].\nfunc Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {\n\tvar fields []string\n\tfor s, err := range FieldsSeq(cfg, words...) {\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfields = append(fields, s)\n\t}\n\treturn fields, nil\n}\n\n// FieldsSeq expands a number of words as if they were arguments in a shell\n// command. This includes brace expansion, tilde expansion, parameter expansion,\n// command substitution, arithmetic expansion, quote removal, and globbing.\nfunc FieldsSeq(cfg *Config, words ...*syntax.Word) iter.Seq2[string, error] {\n\tcfg = prepareConfig(cfg)\n\tdir := cfg.envGet(\"PWD\")\n\treturn func(yield func(string, error) bool) {\n\t\tfor _, word := range words {\n\t\t\tword := *word // make a copy, since SplitBraces replaces the Parts slice\n\t\t\tafterBraces := []*syntax.Word{&word}\n\t\t\tif syntax.SplitBraces(&word) {\n\t\t\t\tafterBraces = Braces(&word)\n\t\t\t}\n\t\t\tfor _, word2 := range afterBraces {\n\t\t\t\twfields, err := cfg.wordFields(word2.Parts)\n\t\t\t\tif err != nil {\n\t\t\t\t\tyield(\"\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor _, field := range wfields {\n\t\t\t\t\tpath, doGlob := cfg.escapedGlobField(field)\n\t\t\t\t\tif doGlob && cfg.ReadDir2 != nil {\n\t\t\t\t\t\t// Note that globbing requires keeping a slice state, so it doesn't\n\t\t\t\t\t\t// really benefit from using an iterator.\n\t\t\t\t\t\tmatches, err := cfg.glob(dir, path)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t// We avoid [errors.As] as it allocates,\n\t\t\t\t\t\t\t// and we know that [Config.glob] returns [pattern.Regexp] errors without wrapping.\n\t\t\t\t\t\t\tif _, ok := err.(*pattern.SyntaxError); !ok {\n\t\t\t\t\t\t\t\tyield(\"\", err)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if len(matches) > 0 || cfg.NullGlob {\n\t\t\t\t\t\t\tfor _, m := range matches {\n\t\t\t\t\t\t\t\tif !yield(m, nil) {\n\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !yield(cfg.fieldJoin(field), nil) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype fieldPart struct {\n\tval   string\n\tquote quoteLevel\n}\n\ntype quoteLevel uint\n\nconst (\n\tquoteNone quoteLevel = iota\n\tquoteDouble\n\tquoteHeredoc\n\tquoteSingle\n)\n\nfunc (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) {\n\tvar field []fieldPart\n\tfor i, wp := range wps {\n\t\tswitch wp := wp.(type) {\n\t\tcase *syntax.Lit:\n\t\t\ts := wp.Value\n\t\t\tif i == 0 && ql == quoteNone {\n\t\t\t\tif prefix, rest := cfg.expandUser(s, len(wps) > 1); prefix != \"\" {\n\t\t\t\t\t// TODO: return two separate fieldParts,\n\t\t\t\t\t// like in wordFields?\n\t\t\t\t\ts = prefix + rest\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (ql == quoteDouble || ql == quoteHeredoc) && strings.Contains(s, \"\\\\\") {\n\t\t\t\tsb := cfg.strBuilder()\n\t\t\t\tfor i := 0; i < len(s); i++ {\n\t\t\t\t\tb := s[i]\n\t\t\t\t\tif b == '\\\\' && i+1 < len(s) {\n\t\t\t\t\t\tswitch s[i+1] {\n\t\t\t\t\t\tcase '\"':\n\t\t\t\t\t\t\tif ql != quoteDouble {\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfallthrough\n\t\t\t\t\t\tcase '\\\\', '$', '`': // special chars\n\t\t\t\t\t\t\ti++\n\t\t\t\t\t\t\tb = s[i] // write the special char, skipping the backslash\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tsb.WriteByte(b)\n\t\t\t\t}\n\t\t\t\ts = sb.String()\n\t\t\t}\n\t\t\ts, _, _ = strings.Cut(s, \"\\x00\") // TODO: why is this needed?\n\t\t\tfield = append(field, fieldPart{val: s})\n\t\tcase *syntax.SglQuoted:\n\t\t\tfp := fieldPart{quote: quoteSingle, val: wp.Value}\n\t\t\tif wp.Dollar {\n\t\t\t\tfp.val, _, _ = Format(cfg, fp.val, nil)\n\t\t\t\tfp.val, _, _ = strings.Cut(fp.val, \"\\x00\") // cut the string if format included \\x00\n\t\t\t}\n\t\t\tfield = append(field, fp)\n\t\tcase *syntax.DblQuoted:\n\t\t\twfield, err := cfg.wordField(wp.Parts, quoteDouble)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfor _, part := range wfield {\n\t\t\t\tpart.quote = quoteDouble\n\t\t\t\tfield = append(field, part)\n\t\t\t}\n\t\tcase *syntax.ParamExp:\n\t\t\tval, err := cfg.paramExp(wp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfield = append(field, fieldPart{val: val})\n\t\tcase *syntax.CmdSubst:\n\t\t\tval, err := cfg.cmdSubst(wp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfield = append(field, fieldPart{val: val})\n\t\tcase *syntax.ArithmExp:\n\t\t\tn, err := Arithm(cfg, wp.X)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfield = append(field, fieldPart{val: strconv.Itoa(n)})\n\t\tcase *syntax.ProcSubst:\n\t\t\tpath, err := cfg.ProcSubst(wp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfield = append(field, fieldPart{val: path})\n\t\tcase *syntax.ExtGlob:\n\t\t\t// Like how [Config.wordFields] deals with [syntax.ExtGlob],\n\t\t\t// except that we allow these through even when [Config.ExtGlob]\n\t\t\t// is false, as it only applies to pathname expansion.\n\t\t\tfield = append(field, fieldPart{val: wp.Op.String() + wp.Pattern.Value + \")\"})\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unhandled word part: %T\", wp))\n\t\t}\n\t}\n\treturn field, nil\n}\n\nfunc (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) {\n\tif cfg.CmdSubst == nil {\n\t\treturn \"\", UnexpectedCommandError{Node: cs}\n\t}\n\tsb := cfg.strBuilder()\n\tif err := cfg.CmdSubst(sb, cs); err != nil {\n\t\treturn \"\", err\n\t}\n\tout := sb.String()\n\tout = strings.ReplaceAll(out, \"\\x00\", \"\")\n\treturn strings.TrimRight(out, \"\\n\"), nil\n}\n\nfunc (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {\n\tfields := cfg.fieldsAlloc[:0]\n\tcurField := cfg.fieldAlloc[:0]\n\tallowEmpty := false\n\tflush := func() {\n\t\tif len(curField) == 0 {\n\t\t\treturn\n\t\t}\n\t\tfields = append(fields, curField)\n\t\tcurField = nil\n\t}\n\tsplitAdd := func(val string) {\n\t\tfieldStart := -1\n\t\tfor i, r := range val {\n\t\t\tif cfg.ifsRune(r) {\n\t\t\t\tif fieldStart >= 0 { // ending a field\n\t\t\t\t\tcurField = append(curField, fieldPart{val: val[fieldStart:i]})\n\t\t\t\t\tfieldStart = -1\n\t\t\t\t}\n\t\t\t\tflush()\n\t\t\t} else {\n\t\t\t\tif fieldStart < 0 { // starting a new field\n\t\t\t\t\tfieldStart = i\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif fieldStart >= 0 { // ending a field without IFS\n\t\t\tcurField = append(curField, fieldPart{val: val[fieldStart:]})\n\t\t}\n\t}\n\tfor i, wp := range wps {\n\t\tswitch wp := wp.(type) {\n\t\tcase *syntax.Lit:\n\t\t\ts := wp.Value\n\t\t\tif i == 0 {\n\t\t\t\tprefix, rest := cfg.expandUser(s, len(wps) > 1)\n\t\t\t\tcurField = append(curField, fieldPart{\n\t\t\t\t\tquote: quoteSingle,\n\t\t\t\t\tval:   prefix,\n\t\t\t\t})\n\t\t\t\ts = rest\n\t\t\t}\n\t\t\tif strings.Contains(s, \"\\\\\") {\n\t\t\t\tsb := cfg.strBuilder()\n\t\t\t\tfor i := 0; i < len(s); i++ {\n\t\t\t\t\tb := s[i]\n\t\t\t\t\tif b == '\\\\' {\n\t\t\t\t\t\tif i++; i >= len(s) {\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\tb = s[i]\n\t\t\t\t\t}\n\t\t\t\t\tsb.WriteByte(b)\n\t\t\t\t}\n\t\t\t\ts = sb.String()\n\t\t\t}\n\t\t\tcurField = append(curField, fieldPart{val: s})\n\t\tcase *syntax.SglQuoted:\n\t\t\tallowEmpty = true\n\t\t\tfp := fieldPart{quote: quoteSingle, val: wp.Value}\n\t\t\tif wp.Dollar {\n\t\t\t\tfp.val, _, _ = Format(cfg, fp.val, nil)\n\t\t\t\tfp.val, _, _ = strings.Cut(fp.val, \"\\x00\") // cut the string if format included \\x00\n\t\t\t}\n\t\t\tcurField = append(curField, fp)\n\t\tcase *syntax.DblQuoted:\n\t\t\tif len(wp.Parts) == 1 {\n\t\t\t\tpe, _ := wp.Parts[0].(*syntax.ParamExp)\n\t\t\t\tif elems := cfg.quotedElemFields(pe); elems != nil {\n\t\t\t\t\tfor i, elem := range elems {\n\t\t\t\t\t\tif i > 0 {\n\t\t\t\t\t\t\tflush()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurField = append(curField, fieldPart{\n\t\t\t\t\t\t\tquote: quoteDouble,\n\t\t\t\t\t\t\tval:   elem,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tallowEmpty = true\n\t\t\twfield, err := cfg.wordField(wp.Parts, quoteDouble)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfor _, part := range wfield {\n\t\t\t\tpart.quote = quoteDouble\n\t\t\t\tcurField = append(curField, part)\n\t\t\t}\n\t\tcase *syntax.ParamExp:\n\t\t\tval, err := cfg.paramExp(wp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tsplitAdd(val)\n\t\tcase *syntax.CmdSubst:\n\t\t\tval, err := cfg.cmdSubst(wp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tsplitAdd(val)\n\t\tcase *syntax.ArithmExp:\n\t\t\tn, err := Arithm(cfg, wp.X)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcurField = append(curField, fieldPart{val: strconv.Itoa(n)})\n\t\tcase *syntax.ProcSubst:\n\t\t\tpath, err := cfg.ProcSubst(wp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tsplitAdd(path)\n\t\tcase *syntax.ExtGlob:\n\t\t\tif !cfg.ExtGlob {\n\t\t\t\treturn nil, fmt.Errorf(\"extended globbing operator used without the \\\"extglob\\\" option set\")\n\t\t\t}\n\t\t\t// We don't translate or interpret the pattern here in any way;\n\t\t\t// that's done later when globbing takes place via [pattern.Regexp].\n\t\t\t// Here, all we do is keep the extended globbing expression in string form.\n\t\t\t//\n\t\t\t// TODO(v4): perhaps the syntax parser should keep extended globbing expressions\n\t\t\t// as plain literal strings, because a custom node is not particularly helpful.\n\t\t\t// It's not like other globbing operators like `*` or `**` get their own nodes.\n\t\t\tcurField = append(curField, fieldPart{val: wp.Op.String() + wp.Pattern.Value + \")\"})\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unhandled word part: %T\", wp))\n\t\t}\n\t}\n\tflush()\n\tif allowEmpty && len(fields) == 0 {\n\t\tfields = append(fields, curField)\n\t}\n\treturn fields, nil\n}\n\n// quotedElemFields returns the list of elements resulting from a quoted\n// parameter expansion that should be treated especially, like \"${foo[@]}\".\nfunc (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string {\n\tif pe == nil || pe.Length || pe.Width || pe.IsSet {\n\t\treturn nil\n\t}\n\tname := pe.Param.Value\n\tif pe.Excl {\n\t\tswitch pe.Names {\n\t\tcase syntax.NamesPrefixWords: // \"${!prefix@}\"\n\t\t\treturn cfg.namesByPrefix(pe.Param.Value)\n\t\tcase syntax.NamesPrefix: // \"${!prefix*}\"\n\t\t\treturn nil\n\t\t}\n\t\tswitch nodeLit(pe.Index) {\n\t\tcase \"@\": // \"${!name[@]}\"\n\t\t\tswitch vr := cfg.Env.Get(name); vr.Kind {\n\t\t\tcase Indexed:\n\t\t\t\t// TODO: if an indexed array only has elements 0 and 10,\n\t\t\t\t// we should not return all indices in between those.\n\t\t\t\tkeys := make([]string, 0, len(vr.List))\n\t\t\t\tfor key := range vr.List {\n\t\t\t\t\tkeys = append(keys, strconv.Itoa(key))\n\t\t\t\t}\n\t\t\t\treturn keys\n\t\t\tcase Associative:\n\t\t\t\treturn slices.Collect(maps.Keys(vr.Map))\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\tswitch name {\n\tcase \"*\": // \"${*}\" or \"${*:offset:length}\"\n\t\treturn []string{cfg.ifsJoin(cfg.sliceElems(pe, cfg.Env.Get(name).List, true))}\n\tcase \"@\": // \"${@}\" or \"${@:offset:length}\"\n\t\treturn cfg.sliceElems(pe, cfg.Env.Get(name).List, true)\n\t}\n\tswitch nodeLit(pe.Index) {\n\tcase \"@\": // \"${name[@]}\"\n\t\tvr := cfg.Env.Get(name)\n\t\tswitch vr.Kind {\n\t\tcase Indexed:\n\t\t\treturn cfg.sliceElems(pe, vr.List, false)\n\t\tcase Associative:\n\t\t\treturn slices.Collect(maps.Values(vr.Map))\n\t\tcase Unknown:\n\t\t\tif !vr.IsSet() {\n\t\t\t\t// An unset variable expanded as \"${name[@]}\" produces\n\t\t\t\t// zero fields, just like an empty array.\n\t\t\t\treturn []string{}\n\t\t\t}\n\t\t}\n\tcase \"*\": // \"${name[*]}\"\n\t\tif vr := cfg.Env.Get(name); vr.Kind == Indexed {\n\t\t\treturn []string{cfg.ifsJoin(cfg.sliceElems(pe, vr.List, false))}\n\t\t}\n\t}\n\treturn nil\n}\n\n// sliceElems applies ${var:offset:length} slicing to a list of elements.\n// When positional is true, $0 is prepended to the list before slicing.\n// In bash, positional parameter offsets ($@ and $*) are 1-based and\n// offset 0 includes $0 (the shell or script name). Negative offsets\n// count from $# + 1, so $0 is reachable via large enough negative values.\nfunc (cfg *Config) sliceElems(pe *syntax.ParamExp, elems []string, positional bool) []string {\n\tif pe.Slice == nil {\n\t\treturn elems\n\t}\n\tif positional {\n\t\telems = append([]string{cfg.Env.Get(\"0\").Str}, elems...)\n\t}\n\tslicePos := func(n int) int {\n\t\tif n < 0 {\n\t\t\tn = len(elems) + n\n\t\t\tif n < 0 {\n\t\t\t\tn = len(elems)\n\t\t\t}\n\t\t} else if n > len(elems) {\n\t\t\tn = len(elems)\n\t\t}\n\t\treturn n\n\t}\n\tif pe.Slice.Offset != nil {\n\t\toffset, err := Arithm(cfg, pe.Slice.Offset)\n\t\tif err != nil {\n\t\t\treturn elems\n\t\t}\n\t\telems = elems[slicePos(offset):]\n\t}\n\tif pe.Slice.Length != nil {\n\t\tlength, err := Arithm(cfg, pe.Slice.Length)\n\t\tif err != nil {\n\t\t\treturn elems\n\t\t}\n\t\telems = elems[:slicePos(length)]\n\t}\n\treturn elems\n}\n\nfunc (cfg *Config) expandUser(field string, moreFields bool) (prefix, rest string) {\n\tname, ok := strings.CutPrefix(field, \"~\")\n\tif !ok {\n\t\t// No tilde prefix to expand, e.g. \"foo\".\n\t\treturn \"\", field\n\t}\n\ti := strings.IndexByte(name, '/')\n\tif i < 0 && moreFields {\n\t\t// There is a tilde prefix, but followed by more fields, e.g. \"~'foo'\".\n\t\t// We only proceed if an unquoted slash was found in this field, e.g. \"~/'foo'\".\n\t\treturn \"\", field\n\t}\n\tif i >= 0 {\n\t\trest = name[i:]\n\t\tname = name[:i]\n\t}\n\tif name == \"\" {\n\t\t// Current user; try via \"HOME\", otherwise fall back to the\n\t\t// system's appropriate home dir env var. Don't use os/user, as\n\t\t// that's overkill. We can't use [os.UserHomeDir], because we want\n\t\t// to use cfg.Env, and we always want to check \"HOME\" first.\n\n\t\tif vr := cfg.Env.Get(\"HOME\"); vr.IsSet() {\n\t\t\treturn vr.String(), rest\n\t\t}\n\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tif vr := cfg.Env.Get(\"USERPROFILE\"); vr.IsSet() {\n\t\t\t\treturn vr.String(), rest\n\t\t\t}\n\t\t}\n\t\treturn \"\", field\n\t}\n\n\t// Not the current user; try via \"HOME <name>\", otherwise fall back to\n\t// os/user. There isn't a way to lookup user home dirs without cgo.\n\n\tif vr := cfg.Env.Get(\"HOME \" + name); vr.IsSet() {\n\t\treturn vr.String(), rest\n\t}\n\n\tu, err := user.Lookup(name)\n\tif err != nil {\n\t\treturn \"\", field\n\t}\n\treturn u.HomeDir, rest\n}\n\nfunc findAllIndex(pat, name string, n int) [][]int {\n\texpr, err := pattern.Regexp(pat, 0)\n\tif err != nil {\n\t\treturn nil\n\t}\n\trx := regexp.MustCompile(expr)\n\treturn rx.FindAllStringIndex(name, n)\n}\n\nvar (\n\trxGlobStar        = regexp.MustCompile(`^[^/.][^/]*$`)\n\trxGlobStarDotGlob = regexp.MustCompile(`^[^/]*$`)\n)\n\n// pathJoin2 is a simpler version of [filepath.Join] without cleaning the result,\n// since that's needed for globbing.\nfunc pathJoin2(elem1, elem2 string) string {\n\tif elem1 == \"\" {\n\t\treturn elem2\n\t}\n\tif strings.HasSuffix(elem1, string(filepath.Separator)) {\n\t\treturn elem1 + elem2\n\t}\n\treturn elem1 + string(filepath.Separator) + elem2\n}\n\n// pathSplit splits a file path into its elements, retaining empty ones. Before\n// splitting, slashes are replaced with [filepath.Separator], so that splitting\n// Unix paths on Windows works as well.\nfunc pathSplit(path string) []string {\n\tpath = filepath.FromSlash(path)\n\treturn strings.Split(path, string(filepath.Separator))\n}\n\nfunc (cfg *Config) glob(base, pat string) ([]string, error) {\n\tparts := pathSplit(pat)\n\tmatches := []string{\"\"}\n\tif filepath.IsAbs(pat) {\n\t\tif parts[0] == \"\" {\n\t\t\t// unix-like\n\t\t\tmatches[0] = string(filepath.Separator)\n\t\t} else {\n\t\t\t// windows (for some reason it won't work without the\n\t\t\t// trailing separator)\n\t\t\tmatches[0] = parts[0] + string(filepath.Separator)\n\t\t}\n\t\tparts = parts[1:]\n\t}\n\t// TODO: as an optimization, we could do chunks of the path all at once,\n\t// like doing a single stat for \"/foo/bar\" in \"/foo/bar/*\".\n\n\t// TODO: Another optimization would be to reduce the number of ReadDir2 calls.\n\t// For example, /foo/* can end up doing one duplicate call:\n\t//\n\t//    ReadDir2(\"/foo\") to ensure that \"/foo/\" exists and only matches a directory\n\t//    ReadDir2(\"/foo\") glob \"*\"\n\n\tfor i, part := range parts {\n\t\t// Keep around for debugging.\n\t\t// log.Printf(\"matches %q part %d %q\", matches, i, part)\n\n\t\twantDir := i < len(parts)-1\n\t\tswitch {\n\t\tcase part == \"\", part == \".\", part == \"..\":\n\t\t\tfor i, dir := range matches {\n\t\t\t\tmatches[i] = pathJoin2(dir, part)\n\t\t\t}\n\t\t\tcontinue\n\t\tcase !pattern.HasMeta(part, 0):\n\t\t\tvar newMatches []string\n\t\t\tfor _, dir := range matches {\n\t\t\t\tmatch := dir\n\t\t\t\tif !filepath.IsAbs(match) {\n\t\t\t\t\tmatch = filepath.Join(base, match)\n\t\t\t\t}\n\t\t\t\tmatch = pathJoin2(match, part)\n\t\t\t\t// We can't use [Config.ReadDir2] on the parent and match the directory\n\t\t\t\t// entry by name, because short paths on Windows break that.\n\t\t\t\t// Our only option is to [Config.ReadDir2] on the directory entry itself,\n\t\t\t\t// which can be wasteful if we only want to see if it exists,\n\t\t\t\t// but at least it's correct in all scenarios.\n\t\t\t\tif _, err := cfg.ReadDir2(match); err != nil {\n\t\t\t\t\tif isWindowsErrPathNotFound(err) {\n\t\t\t\t\t\t// Unfortunately, [os.File.Readdir] on a regular file on\n\t\t\t\t\t\t// Windows returns an error that satisfies [fs.ErrNotExist].\n\t\t\t\t\t\t// Luckily, it returns a special \"path not found\" rather\n\t\t\t\t\t\t// than the normal \"file not found\" for missing files,\n\t\t\t\t\t\t// so we can use that knowledge to work around the bug.\n\t\t\t\t\t\t// See https://github.com/golang/go/issues/46734.\n\t\t\t\t\t\t// TODO: remove when the Go issue above is resolved.\n\t\t\t\t\t} else if errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\t\tcontinue // simply doesn't exist\n\t\t\t\t\t}\n\t\t\t\t\tif wantDir {\n\t\t\t\t\t\tcontinue // exists but not a directory\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnewMatches = append(newMatches, pathJoin2(dir, part))\n\t\t\t}\n\t\t\tmatches = newMatches\n\t\t\tcontinue\n\t\tcase part == \"**\" && cfg.GlobStar:\n\t\t\t// Find all recursive matches for \"**\".\n\t\t\t// Note that we need the results to be in depth-first order,\n\t\t\t// and to avoid recursion, we use a slice as a stack.\n\t\t\t// Since we pop from the back, we populate the stack backwards.\n\t\t\tstack := make([]string, 0, len(matches))\n\t\t\tfor _, match := range slices.Backward(matches) {\n\t\t\t\t// \"a/**\" should match \"a/ a/b a/b/cfg ...\";\n\t\t\t\t// note how the zero-match case there has a trailing separator.\n\t\t\t\tstack = append(stack, pathJoin2(match, \"\"))\n\t\t\t}\n\t\t\tmatches = matches[:0]\n\t\t\tvar newMatches []string // to reuse its capacity\n\t\t\tfor len(stack) > 0 {\n\t\t\t\tdir := stack[len(stack)-1]\n\t\t\t\tstack = stack[:len(stack)-1]\n\t\t\t\tmatches = append(matches, dir)\n\n\t\t\t\t// If dir is not a directory, we keep the stack as-is and continue.\n\t\t\t\tnewMatches = newMatches[:0]\n\t\t\t\trx := rxGlobStar.MatchString\n\t\t\t\tif cfg.DotGlob {\n\t\t\t\t\trx = rxGlobStarDotGlob.MatchString\n\t\t\t\t}\n\t\t\t\tnewMatches, _ = cfg.globDir(base, dir, rx, wantDir, newMatches)\n\t\t\t\tfor _, match := range slices.Backward(newMatches) {\n\t\t\t\t\tstack = append(stack, match)\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tmode := pattern.Filenames | pattern.EntireString | pattern.NoGlobStar\n\t\tif cfg.NoCaseGlob {\n\t\t\tmode |= pattern.NoGlobCase\n\t\t}\n\t\tif cfg.DotGlob {\n\t\t\tmode |= pattern.GlobLeadingDot\n\t\t}\n\t\tif cfg.ExtGlob {\n\t\t\tmode |= pattern.ExtendedOperators\n\t\t}\n\t\tmatcher, err := internal.ExtendedPatternMatcher(part, mode)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar newMatches []string\n\t\tfor _, dir := range matches {\n\t\t\tnewMatches, err = cfg.globDir(base, dir, matcher, wantDir, newMatches)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tmatches = newMatches\n\t}\n\t// Note that the results need to be sorted.\n\t// TODO: above we do a BFS; if we did a DFS, the matches would already be sorted.\n\tslices.Sort(matches)\n\t// Remove any empty matches left behind from \"**\".\n\tif len(matches) > 0 && matches[0] == \"\" {\n\t\tmatches = matches[1:]\n\t}\n\treturn matches, nil\n}\n\nfunc (cfg *Config) globDir(base, dir string, matcher func(string) bool, wantDir bool, matches []string) ([]string, error) {\n\tfullDir := dir\n\tif !filepath.IsAbs(dir) {\n\t\tfullDir = filepath.Join(base, dir)\n\t}\n\tinfos, err := cfg.ReadDir2(fullDir)\n\tif err != nil {\n\t\t// We still want to return matches, for the sake of reusing slices.\n\t\treturn matches, err\n\t}\n\tfor _, info := range infos {\n\t\tname := info.Name()\n\t\tif !wantDir {\n\t\t\t// No filtering.\n\t\t} else if mode := info.Type(); mode&os.ModeSymlink != 0 {\n\t\t\t// We need to know if the symlink points to a directory.\n\t\t\t// This requires an extra syscall, as [Config.ReadDir] on the parent directory\n\t\t\t// does not follow symlinks for each of the directory entries.\n\t\t\t// ReadDir is somewhat wasteful here, as we only want its error result,\n\t\t\t// but we could try to reuse its result as per the TODO in [Config.glob].\n\t\t\tif _, err := cfg.ReadDir2(filepath.Join(fullDir, info.Name())); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else if !mode.IsDir() {\n\t\t\t// Not a symlink nor a directory.\n\t\t\tcontinue\n\t\t}\n\t\tif matcher(name) {\n\t\t\tmatches = append(matches, pathJoin2(dir, name))\n\t\t}\n\t}\n\treturn matches, nil\n}\n\n// ReadFields splits and returns n fields from s, like the \"read\" shell builtin.\n// If raw is set, backslash escape sequences are not interpreted.\n//\n// The config specifies shell expansion options; nil behaves the same as an\n// empty config.\nfunc ReadFields(cfg *Config, s string, n int, raw bool) []string {\n\tcfg = prepareConfig(cfg)\n\ttype pos struct {\n\t\tstart, end int\n\t}\n\tvar fpos []pos\n\n\trunes := make([]rune, 0, len(s))\n\tinfield := false\n\tesc := false\n\tfor _, r := range s {\n\t\tif infield {\n\t\t\tif cfg.ifsRune(r) && (raw || !esc) {\n\t\t\t\tfpos[len(fpos)-1].end = len(runes)\n\t\t\t\tinfield = false\n\t\t\t}\n\t\t} else {\n\t\t\tif !cfg.ifsRune(r) && (raw || !esc) {\n\t\t\t\tfpos = append(fpos, pos{start: len(runes), end: -1})\n\t\t\t\tinfield = true\n\t\t\t}\n\t\t}\n\t\tif r == '\\\\' {\n\t\t\tif raw || esc {\n\t\t\t\trunes = append(runes, r)\n\t\t\t}\n\t\t\tesc = !esc\n\t\t\tcontinue\n\t\t}\n\t\trunes = append(runes, r)\n\t\tesc = false\n\t}\n\tif len(fpos) == 0 {\n\t\treturn nil\n\t}\n\tif infield {\n\t\tfpos[len(fpos)-1].end = len(runes)\n\t}\n\n\tswitch {\n\tcase n == 1:\n\t\t// include heading/trailing IFSs\n\t\tfpos[0].start, fpos[0].end = 0, len(runes)\n\t\tfpos = fpos[:1]\n\tcase n != -1 && n < len(fpos):\n\t\t// combine to max n fields\n\t\tfpos[n-1].end = fpos[len(fpos)-1].end\n\t\tfpos = fpos[:n]\n\t}\n\n\tfields := make([]string, len(fpos))\n\tfor i, p := range fpos {\n\t\tfields[i] = string(runes[p.start:p.end])\n\t}\n\treturn fields\n}\n"
  },
  {
    "path": "expand/expand_nonwindows.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n//go:build !windows\n\npackage expand\n\nfunc isWindowsErrPathNotFound(error) bool { return false }\n"
  },
  {
    "path": "expand/expand_test.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc parseWord(t *testing.T, src string) *syntax.Word {\n\tt.Helper()\n\tp := syntax.NewParser()\n\tword, err := p.Document(strings.NewReader(src))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn word\n}\n\nfunc TestConfigNils(t *testing.T) {\n\tos.Setenv(\"EXPAND_GLOBAL\", \"value\")\n\ttests := []struct {\n\t\tname string\n\t\tcfg  *Config\n\t\tsrc  string\n\t\twant string\n\t}{\n\t\t{\n\t\t\t\"NilConfig\",\n\t\t\tnil,\n\t\t\t\"$EXPAND_GLOBAL\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"ZeroConfig\",\n\t\t\t&Config{},\n\t\t\t\"$EXPAND_GLOBAL\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"EnvConfig\",\n\t\t\t&Config{Env: ListEnviron(os.Environ()...)},\n\t\t\t\"$EXPAND_GLOBAL\",\n\t\t\t\"value\",\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tword := parseWord(t, tc.src)\n\t\t\tgot, err := Literal(tc.cfg, word)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"did not want error, got %v\", err)\n\t\t\t}\n\t\t\tif got != tc.want {\n\t\t\t\tt.Fatalf(\"wanted %q, got %q\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFieldsIdempotency(t *testing.T) {\n\ttests := []struct {\n\t\tsrc  string\n\t\twant []string\n\t}{\n\t\t{\n\t\t\t\"{1..4}\",\n\t\t\t[]string{\"1\", \"2\", \"3\", \"4\"},\n\t\t},\n\t\t{\n\t\t\t\"a{1..4}\",\n\t\t\t[]string{\"a1\", \"a2\", \"a3\", \"a4\"},\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tword := parseWord(t, tc.src)\n\t\tfor range 2 {\n\t\t\tgot, err := Fields(nil, word)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"did not want error, got %v\", err)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tc.want) {\n\t\t\t\tt.Fatalf(\"wanted %q, got %q\", tc.want, got)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc Test_glob(t *testing.T) {\n\tcfg := &Config{\n\t\tReadDir2: func(string) ([]fs.DirEntry, error) {\n\t\t\treturn []fs.DirEntry{\n\t\t\t\t// The filenames here are sorted, just like [io/fs.ReadDirFS].\n\t\t\t\t&mockFileInfo{name: \"A\"},\n\t\t\t\t&mockFileInfo{name: \"AB\"},\n\t\t\t\t&mockFileInfo{name: \"a\"},\n\t\t\t\t&mockFileInfo{name: \"ab\"},\n\t\t\t}, nil\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tnoCaseGlob bool\n\t\tpat        string\n\t\twant       []string\n\t}{\n\t\t{false, \"a*\", []string{\"a\", \"ab\"}},\n\t\t{false, \"A*\", []string{\"A\", \"AB\"}},\n\t\t{false, \"*b\", []string{\"ab\"}},\n\t\t{false, \"b*\", nil},\n\t\t{true, \"a*\", []string{\"A\", \"AB\", \"a\", \"ab\"}},\n\t\t{true, \"A*\", []string{\"A\", \"AB\", \"a\", \"ab\"}},\n\t\t{true, \"*b\", []string{\"AB\", \"ab\"}},\n\t\t{true, \"b*\", nil},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.pat, func(t *testing.T) {\n\t\t\tcfg.NoCaseGlob = tc.noCaseGlob\n\t\t\tgot, err := cfg.glob(\"/\", tc.pat)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"did not want error, got %v\", err)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tc.want) {\n\t\t\t\tt.Fatalf(\"wanted %q, got %q\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype mockFileInfo struct {\n\tname        string\n\ttyp         fs.FileMode\n\tfs.DirEntry // Stub out everything but Name() & Type()\n}\n\nvar _ fs.DirEntry = (*mockFileInfo)(nil)\n\nfunc (fi *mockFileInfo) Name() string      { return fi.name }\nfunc (fi *mockFileInfo) Type() fs.FileMode { return fi.typ }\n"
  },
  {
    "path": "expand/expand_windows.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"syscall\"\n)\n\nfunc isWindowsErrPathNotFound(err error) bool {\n\tvar pathErr *os.PathError\n\treturn errors.As(err, &pathErr) && pathErr.Err == syscall.ERROR_PATH_NOT_FOUND\n}\n"
  },
  {
    "path": "expand/param.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\t\"mvdan.cc/sh/v3/pattern\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc nodeLit(node syntax.Node) string {\n\tif word, ok := node.(*syntax.Word); ok {\n\t\treturn word.Lit()\n\t}\n\treturn \"\"\n}\n\n// UnsetParameterError is returned when a parameter expansion encounters an\n// unset variable and [Config.NoUnset] has been set.\ntype UnsetParameterError struct {\n\tNode    *syntax.ParamExp\n\tMessage string\n}\n\nfunc (u UnsetParameterError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", u.Node.Param.Value, u.Message)\n}\n\nfunc overridingUnset(pe *syntax.ParamExp) bool {\n\tif pe.Exp == nil {\n\t\treturn false\n\t}\n\tswitch pe.Exp.Op {\n\tcase syntax.AlternateUnset, syntax.AlternateUnsetOrNull,\n\t\tsyntax.DefaultUnset, syntax.DefaultUnsetOrNull,\n\t\tsyntax.ErrorUnset, syntax.ErrorUnsetOrNull,\n\t\tsyntax.AssignUnset, syntax.AssignUnsetOrNull:\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) {\n\toldParam := cfg.curParam\n\tcfg.curParam = pe\n\tdefer func() { cfg.curParam = oldParam }()\n\n\tname := pe.Param.Value\n\tindex := pe.Index\n\tswitch name {\n\tcase \"@\", \"*\":\n\t\tindex = &syntax.Word{Parts: []syntax.WordPart{\n\t\t\t&syntax.Lit{Value: name},\n\t\t}}\n\t}\n\tvar vr Variable\n\tswitch name {\n\tcase \"LINENO\":\n\t\t// This is the only parameter expansion that the environment\n\t\t// interface cannot satisfy.\n\t\tline := uint64(cfg.curParam.Pos().Line())\n\t\tvr = Variable{Set: true, Kind: String, Str: strconv.FormatUint(line, 10)}\n\tdefault:\n\t\tvr = cfg.Env.Get(name)\n\t}\n\torig := vr\n\t_, vr = vr.Resolve(cfg.Env)\n\tif cfg.NoUnset && !vr.IsSet() && !overridingUnset(pe) {\n\t\treturn \"\", UnsetParameterError{\n\t\t\tNode:    pe,\n\t\t\tMessage: \"unbound variable\",\n\t\t}\n\t}\n\n\tvar sliceOffset, sliceLen int\n\tif pe.Slice != nil {\n\t\tvar err error\n\t\tif pe.Slice.Offset != nil {\n\t\t\tsliceOffset, err = Arithm(cfg, pe.Slice.Offset)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\t\tif pe.Slice.Length != nil {\n\t\t\tsliceLen, err = Arithm(cfg, pe.Slice.Length)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\t}\n\n\tvar (\n\t\tstr   string\n\t\telems []string\n\n\t\tindexAllElements bool // true if var has been accessed with * or @ index\n\t\tcallVarInd       = true\n\t)\n\n\tswitch nodeLit(index) {\n\tcase \"@\", \"*\":\n\t\tswitch vr.Kind {\n\t\tcase Unknown:\n\t\t\telems = nil\n\t\t\tindexAllElements = true\n\t\tcase Indexed:\n\t\t\tindexAllElements = true\n\t\t\tcallVarInd = false\n\t\t\telems = cfg.sliceElems(pe, vr.List, name == \"@\" || name == \"*\")\n\t\t\tstr = strings.Join(elems, \" \")\n\t\t}\n\t}\n\tif callVarInd {\n\t\tvar err error\n\t\tstr, err = cfg.varInd(vr, index)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif !indexAllElements {\n\t\telems = []string{str}\n\t}\n\n\tswitch {\n\tcase pe.Length:\n\t\tn := len(elems)\n\t\tswitch nodeLit(index) {\n\t\tcase \"@\", \"*\":\n\t\tdefault:\n\t\t\tn = utf8.RuneCountInString(str)\n\t\t}\n\t\tstr = strconv.Itoa(n)\n\tcase pe.Excl:\n\t\tvar strs []string\n\t\tswitch {\n\t\tcase pe.Names != 0:\n\t\t\tstrs = cfg.namesByPrefix(pe.Param.Value)\n\t\tcase orig.Kind == NameRef:\n\t\t\tstrs = append(strs, orig.Str)\n\t\tcase pe.Index != nil && vr.Kind == Indexed:\n\t\t\tfor i, e := range vr.List {\n\t\t\t\tif e != \"\" {\n\t\t\t\t\tstrs = append(strs, strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t}\n\t\tcase pe.Index != nil && vr.Kind == Associative:\n\t\t\tstrs = slices.AppendSeq(strs, maps.Keys(vr.Map))\n\t\tcase !vr.IsSet():\n\t\t\treturn \"\", fmt.Errorf(\"invalid indirect expansion\")\n\t\tcase str == \"\":\n\t\t\treturn \"\", nil\n\t\tdefault:\n\t\t\tvr = cfg.Env.Get(str)\n\t\t\tstrs = append(strs, vr.String())\n\t\t}\n\t\tslices.Sort(strs)\n\t\tstr = strings.Join(strs, \" \")\n\tcase pe.Width:\n\t\treturn \"\", fmt.Errorf(\"unsupported\")\n\tcase pe.IsSet:\n\t\treturn \"\", fmt.Errorf(\"unsupported\")\n\tcase pe.Slice != nil:\n\t\tif callVarInd {\n\t\t\tslicePos := func(n int) int {\n\t\t\t\tif n < 0 {\n\t\t\t\t\tn = len(str) + n\n\t\t\t\t\tif n < 0 {\n\t\t\t\t\t\tn = len(str)\n\t\t\t\t\t}\n\t\t\t\t} else if n > len(str) {\n\t\t\t\t\tn = len(str)\n\t\t\t\t}\n\t\t\t\treturn n\n\t\t\t}\n\t\t\tif pe.Slice.Offset != nil {\n\t\t\t\tstr = str[slicePos(sliceOffset):]\n\t\t\t}\n\t\t\tif pe.Slice.Length != nil {\n\t\t\t\tstr = str[:slicePos(sliceLen)]\n\t\t\t}\n\t\t} // else, elems are already sliced\n\tcase pe.Repl != nil:\n\t\torig, err := Pattern(cfg, pe.Repl.Orig)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif orig == \"\" {\n\t\t\tbreak // nothing to replace\n\t\t}\n\t\twith, err := Literal(cfg, pe.Repl.With)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tn := 1\n\t\tif pe.Repl.All {\n\t\t\tn = -1\n\t\t}\n\t\tlocs := findAllIndex(orig, str, n)\n\t\tsb := cfg.strBuilder()\n\t\tlast := 0\n\t\tfor _, loc := range locs {\n\t\t\tsb.WriteString(str[last:loc[0]])\n\t\t\tsb.WriteString(with)\n\t\t\tlast = loc[1]\n\t\t}\n\t\tsb.WriteString(str[last:])\n\t\tstr = sb.String()\n\tcase pe.Exp != nil:\n\t\targ, err := Literal(cfg, pe.Exp.Word)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tswitch op := pe.Exp.Op; op {\n\t\tcase syntax.AlternateUnsetOrNull:\n\t\t\tif str == \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase syntax.AlternateUnset:\n\t\t\tif vr.IsSet() {\n\t\t\t\tstr = arg\n\t\t\t}\n\t\tcase syntax.DefaultUnset:\n\t\t\tif vr.IsSet() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase syntax.DefaultUnsetOrNull:\n\t\t\tif str == \"\" {\n\t\t\t\tstr = arg\n\t\t\t}\n\t\tcase syntax.ErrorUnset:\n\t\t\tif vr.IsSet() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase syntax.ErrorUnsetOrNull:\n\t\t\tif str == \"\" {\n\t\t\t\treturn \"\", UnsetParameterError{\n\t\t\t\t\tNode:    pe,\n\t\t\t\t\tMessage: arg,\n\t\t\t\t}\n\t\t\t}\n\t\tcase syntax.AssignUnset:\n\t\t\tif vr.IsSet() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase syntax.AssignUnsetOrNull:\n\t\t\tif str == \"\" {\n\t\t\t\tif err := cfg.envSet(name, arg); err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\tstr = arg\n\t\t\t}\n\t\tcase syntax.RemSmallPrefix, syntax.RemLargePrefix,\n\t\t\tsyntax.RemSmallSuffix, syntax.RemLargeSuffix:\n\t\t\tsuffix := op == syntax.RemSmallSuffix || op == syntax.RemLargeSuffix\n\t\t\tsmall := op == syntax.RemSmallPrefix || op == syntax.RemSmallSuffix\n\t\t\tfor i, elem := range elems {\n\t\t\t\telems[i] = removePattern(elem, arg, suffix, small)\n\t\t\t}\n\t\t\tstr = strings.Join(elems, \" \")\n\t\tcase syntax.UpperFirst, syntax.UpperAll,\n\t\t\tsyntax.LowerFirst, syntax.LowerAll:\n\n\t\t\tcaseFunc := unicode.ToLower\n\t\t\tif op == syntax.UpperFirst || op == syntax.UpperAll {\n\t\t\t\tcaseFunc = unicode.ToUpper\n\t\t\t}\n\t\t\tall := op == syntax.UpperAll || op == syntax.LowerAll\n\n\t\t\t// empty string means '?'; nothing to do there\n\t\t\texpr, err := pattern.Regexp(arg, 0)\n\t\t\tif err != nil {\n\t\t\t\treturn str, nil\n\t\t\t}\n\t\t\trx := regexp.MustCompile(expr)\n\n\t\t\tfor i, elem := range elems {\n\t\t\t\trs := []rune(elem)\n\t\t\t\tfor ri, r := range rs {\n\t\t\t\t\tif rx.MatchString(string(r)) {\n\t\t\t\t\t\trs[ri] = caseFunc(r)\n\t\t\t\t\t\tif !all {\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telems[i] = string(rs)\n\t\t\t}\n\t\t\tstr = strings.Join(elems, \" \")\n\t\tcase syntax.OtherParamOps:\n\t\t\tswitch arg {\n\t\t\tcase \"Q\":\n\t\t\t\tstr, err = syntax.Quote(str, syntax.LangBash)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// Is this even possible? If a user runs into this panic,\n\t\t\t\t\t// it's most likely a bug we need to fix.\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\tcase \"E\":\n\t\t\t\ttail := str\n\t\t\t\tvar rns []rune\n\t\t\t\tfor tail != \"\" {\n\t\t\t\t\tvar rn rune\n\t\t\t\t\trn, _, tail, _ = strconv.UnquoteChar(tail, 0)\n\t\t\t\t\trns = append(rns, rn)\n\t\t\t\t}\n\t\t\t\tstr = string(rns)\n\t\t\tcase \"a\":\n\t\t\t\t// ${var@a} returns variable attribute flags.\n\t\t\t\t// We use orig (before nameref resolve) for the attributes.\n\t\t\t\tstr = orig.Flags()\n\t\t\tcase \"A\":\n\t\t\t\t// ${var@A} returns a declare statement that recreates the variable.\n\t\t\t\tflags := orig.Flags()\n\t\t\t\tquoted, err := syntax.Quote(str, syntax.LangBash)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\tif flags == \"\" {\n\t\t\t\t\tstr = fmt.Sprintf(\"%s=%s\", name, quoted)\n\t\t\t\t} else {\n\t\t\t\t\tstr = fmt.Sprintf(\"declare -%s %s=%s\", flags, name, quoted)\n\t\t\t\t}\n\t\t\tcase \"P\":\n\t\t\t\t// TODO: implement prompt expansion (\\u, \\h, \\w, etc.).\n\t\t\tdefault:\n\t\t\t\tpanic(fmt.Sprintf(\"unexpected @%s param expansion\", arg))\n\t\t\t}\n\t\t}\n\t}\n\treturn str, nil\n}\n\nfunc removePattern(str, pat string, fromEnd, shortest bool) string {\n\tvar mode pattern.Mode\n\tif shortest {\n\t\tmode |= pattern.Shortest\n\t}\n\texpr, err := pattern.Regexp(pat, mode)\n\tif err != nil {\n\t\treturn str\n\t}\n\tswitch {\n\tcase fromEnd && shortest:\n\t\t// use .* to get the right-most shortest match\n\t\texpr = \".*(\" + expr + \")$\"\n\tcase fromEnd:\n\t\t// simple suffix\n\t\texpr = \"(\" + expr + \")$\"\n\tdefault:\n\t\t// simple prefix\n\t\texpr = \"^(\" + expr + \")\"\n\t}\n\t// no need to check error as Translate returns one\n\trx := regexp.MustCompile(expr)\n\tif loc := rx.FindStringSubmatchIndex(str); loc != nil {\n\t\t// remove the original pattern (the submatch)\n\t\tstr = str[:loc[2]] + str[loc[3]:]\n\t}\n\treturn str\n}\n\nfunc (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, error) {\n\tif idx == nil {\n\t\treturn vr.String(), nil\n\t}\n\tswitch vr.Kind {\n\tcase String:\n\t\tn, err := Arithm(cfg, idx)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif n == 0 {\n\t\t\treturn vr.Str, nil\n\t\t}\n\tcase Indexed:\n\t\tswitch nodeLit(idx) {\n\t\tcase \"*\", \"@\":\n\t\t\treturn strings.Join(vr.List, \" \"), nil\n\t\t}\n\t\ti, err := Arithm(cfg, idx)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif i < 0 {\n\t\t\treturn \"\", fmt.Errorf(\"negative array index\")\n\t\t}\n\t\tif i < len(vr.List) {\n\t\t\treturn vr.List[i], nil\n\t\t}\n\tcase Associative:\n\t\tswitch lit := nodeLit(idx); lit {\n\t\tcase \"@\", \"*\":\n\t\t\tstrs := slices.Sorted(maps.Values(vr.Map))\n\t\t\tif lit == \"*\" {\n\t\t\t\treturn cfg.ifsJoin(strs), nil\n\t\t\t}\n\t\t\treturn strings.Join(strs, \" \"), nil\n\t\t}\n\t\tval, err := Literal(cfg, idx.(*syntax.Word))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn vr.Map[val], nil\n\t}\n\treturn \"\", nil\n}\n\nfunc (cfg *Config) namesByPrefix(prefix string) []string {\n\tvar names []string\n\tfor name := range cfg.Env.Each {\n\t\tif strings.HasPrefix(name, prefix) {\n\t\t\tnames = append(names, name)\n\t\t}\n\t}\n\treturn names\n}\n"
  },
  {
    "path": "expand/valuekind_string.go",
    "content": "// Code generated by \"stringer -type=ValueKind\"; DO NOT EDIT.\n\npackage expand\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[Unknown-0]\n\t_ = x[String-1]\n\t_ = x[NameRef-2]\n\t_ = x[Indexed-3]\n\t_ = x[Associative-4]\n\t_ = x[KeepValue-5]\n}\n\nconst _ValueKind_name = \"UnknownStringNameRefIndexedAssociativeKeepValue\"\n\nvar _ValueKind_index = [...]uint8{0, 7, 13, 20, 27, 38, 47}\n\nfunc (i ValueKind) String() string {\n\tidx := int(i) - 0\n\tif i < 0 || idx >= len(_ValueKind_index)-1 {\n\t\treturn \"ValueKind(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _ValueKind_name[_ValueKind_index[idx]:_ValueKind_index[idx+1]]\n}\n"
  },
  {
    "path": "fileutil/file.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package fileutil allows inspecting shell files, such as detecting whether a\n// file may be shell or extracting its shebang.\npackage fileutil\n\nimport (\n\t\"io/fs\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar (\n\tshebangRe = regexp.MustCompile(`^#![ \\t]*/(usr/)?bin/(env[ \\t]+)?(sh|bash|mksh|bats|zsh)(\\s|$)`)\n\textRe     = regexp.MustCompile(`\\.(sh|bash|mksh|bats|zsh)$`)\n)\n\n// TODO: consider removing HasShebang in favor of Shebang in v4\n\n// HasShebang reports whether bs begins with a valid shell shebang.\n// It supports variations with /usr and env.\nfunc HasShebang(bs []byte) bool {\n\treturn Shebang(bs) != \"\"\n}\n\n// Shebang parses a \"#!\" sequence from the beginning of the input bytes,\n// and returns the shell that it points to.\n//\n// For instance, it returns \"sh\" for \"#!/bin/sh\",\n// and \"bash\" for \"#!/usr/bin/env bash\".\nfunc Shebang(bs []byte) string {\n\tm := shebangRe.FindSubmatch(bs)\n\tif m == nil {\n\t\treturn \"\"\n\t}\n\treturn string(m[3])\n}\n\n// ScriptConfidence defines how likely a file is to be a shell script,\n// from complete certainty that it is not one to complete certainty that\n// it is one.\ntype ScriptConfidence int\n\nconst (\n\t// ConfNotScript describes files which are definitely not shell scripts,\n\t// such as non-regular files or files with a non-shell extension.\n\tConfNotScript ScriptConfidence = iota\n\n\t// ConfIfShebang describes files which might be shell scripts, depending\n\t// on the shebang line in the file's contents. Since [CouldBeScript] only\n\t// works on [fs.FileInfo], the answer in this case can't be final.\n\tConfIfShebang\n\n\t// ConfIsScript describes files which are definitely shell scripts,\n\t// which are regular files with a valid shell extension.\n\tConfIsScript\n)\n\n// CouldBeScript is a shortcut for CouldBeScript2(fs.FileInfoToDirEntry(info)).\n//\n// Deprecated: prefer [CouldBeScript2], which usually requires fewer syscalls.\nfunc CouldBeScript(info fs.FileInfo) ScriptConfidence {\n\treturn CouldBeScript2(fs.FileInfoToDirEntry(info))\n}\n\n// CouldBeScript2 reports how likely a directory entry is to be a shell script.\n// It discards directories and other non-regular files like symbolic links,\n// filenames beginning with '.', and files with non-shell extensions.\nfunc CouldBeScript2(entry fs.DirEntry) ScriptConfidence {\n\tname := entry.Name()\n\tswitch {\n\tcase name[0] == '.':\n\t\treturn ConfNotScript // '.' prefix (hidden file)\n\tcase !entry.Type().IsRegular():\n\t\treturn ConfNotScript // dir, symlink, named pipes, etc\n\tcase extRe.MatchString(name):\n\t\treturn ConfIsScript // shell extension\n\tcase strings.IndexByte(name, '.') > 0:\n\t\treturn ConfNotScript // non-shell extension\n\tdefault:\n\t\treturn ConfIfShebang // no extension; read and look for a shebang\n\t}\n}\n"
  },
  {
    "path": "fileutil/file_test.go",
    "content": "// Copyright (c) 2025, Ville Skyttä <ville.skytta@iki.fi>\n// See LICENSE for licensing information\n\npackage fileutil\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestShebang(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tin   []byte\n\t\twant string\n\t}{\n\t\t{\n\t\t\tin:   []byte(\"#!/usr/bin/env bash\"),\n\t\t\twant: \"bash\",\n\t\t},\n\t\t{\n\t\t\tin:   []byte(\"#!/bin/bash\"),\n\t\t\twant: \"bash\",\n\t\t},\n\t\t{\n\t\t\tin:   []byte(\"#!foo bar\"),\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tin:   []byte(\"#!/bin/zsh\"),\n\t\t\twant: \"zsh\",\n\t\t},\n\t\t{\n\t\t\tin:   []byte(\"#! /bin/zsh true\"),\n\t\t\twant: \"zsh\",\n\t\t},\n\t\t{\n\t\t\tin:   []byte(\"#!  /bin/zsh\"),\n\t\t\twant: \"zsh\",\n\t\t},\n\t\t{\n\t\t\tin:   []byte(\"#!\\t/bin/zsh\"),\n\t\t\twant: \"zsh\",\n\t\t},\n\t\t{\n\t\t\tin:   []byte(\"#!\\f/bin/zsh\"),\n\t\t\twant: \"\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tname := strings.ReplaceAll(strings.ReplaceAll(string(test.in), \"\\f\", \"\\\\f\"), \"\\t\", \"\\\\t\")\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif got := Shebang(test.in); got != test.want {\n\t\t\t\tt.Fatalf(\"want %q, got %q\", test.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module mvdan.cc/sh/v3\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/creack/pty v1.1.24\n\tgithub.com/go-quicktest/qt v1.101.0\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/renameio/v2 v2.0.2\n\tgithub.com/rogpeppe/go-internal v1.14.1\n\tgolang.org/x/sys v0.42.0\n\tgolang.org/x/term v0.40.0\n\tmvdan.cc/editorconfig v0.3.0\n)\n\nrequire (\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgolang.org/x/mod v0.29.0 // indirect\n\tgolang.org/x/sync v0.17.0 // indirect\n\tgolang.org/x/tools v0.38.0 // indirect\n)\n\ntool golang.org/x/tools/cmd/stringer\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=\ngithub.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw=\ngithub.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngolang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=\ngolang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=\ngolang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=\ngolang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=\ngolang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=\nmvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=\nmvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=\n"
  },
  {
    "path": "internal/pattern.go",
    "content": "// Copyright (c) 2026, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/pattern\"\n)\n\n// ExtendedPatternMatcher returns a [regexp.Regexp.MatchString]-like function\n// to support !(pattern-list) extended patterns where possible.\n// It can be used instead of [pattern.Regexp] for narrow use cases.\nfunc ExtendedPatternMatcher(pat string, mode pattern.Mode) (func(string) bool, error) {\n\tif mode&pattern.ExtendedOperators != 0 && mode&pattern.EntireString == 0 {\n\t\t// In the future we could try to support !(pattern) without matching\n\t\t// the entire input, ensuring we add enough test cases.\n\t\tpanic(\"ExtendedOperators is only supported with EntireString\")\n\t}\n\n\t// Extended pattern matching operators are always on outside of pathname expansion.\n\texpr, err := pattern.Regexp(pat, mode)\n\tif err != nil {\n\t\t// Handle !(pattern-list) negation: when Regexp returns NegExtglobError,\n\t\t// match the inner pattern and negate the result.\n\t\tvar negErr *pattern.NegExtGlobError\n\t\tif !errors.As(err, &negErr) {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn extNegatedMatcher(pat, negErr.Groups)\n\t}\n\trx := regexp.MustCompile(expr)\n\treturn rx.MatchString, nil\n}\n\n// extNegatedMatcher handles !(pattern-list) extglob negation.\n// Only a single !(...) group with fixed-string prefix and suffix is supported.\nfunc extNegatedMatcher(pat string, groups []pattern.NegExtGlobGroup) (func(string) bool, error) {\n\tif len(groups) != 1 {\n\t\treturn nil, fmt.Errorf(\"multiple extglob !(...) groups are not supported yet\")\n\t}\n\tg := groups[0]\n\tprefix := pat[:g.Start]\n\tsuffix := pat[g.End:]\n\n\tif pattern.HasMeta(prefix, 0) || pattern.HasMeta(suffix, 0) {\n\t\treturn nil, fmt.Errorf(\"extglob !(...) is only supported with a fixed prefix and suffix\")\n\t}\n\n\t// Use @(inner) to compile the pattern list, then negate the match.\n\tinner := pat[g.Start+len(\"!(\") : g.End-len(\")\")]\n\texpr, err := pattern.Regexp(\"@(\"+inner+\")\", pattern.EntireString|pattern.ExtendedOperators)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trx := regexp.MustCompile(expr)\n\n\treturn func(name string) bool {\n\t\tif !strings.HasPrefix(name, prefix) {\n\t\t\treturn false\n\t\t}\n\t\tif !strings.HasSuffix(name, suffix) {\n\t\t\treturn false\n\t\t}\n\t\tend := len(name) - len(suffix)\n\t\tif end < len(prefix) {\n\t\t\treturn false // prefix and suffix overlap in name\n\t\t}\n\t\tmiddle := name[len(prefix):end]\n\n\t\treturn !rx.MatchString(middle)\n\t}, nil\n}\n"
  },
  {
    "path": "internal/testing.go",
    "content": "// Copyright (c) 2026, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage internal\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// TestMainSetup is used by the integration tests running shell scripts\n// either via our interpreter or via real shells,\n// to ensure a reasonably clean and consistent environment.\nfunc TestMainSetup() {\n\t// Set the locale to computer-friendly English and UTF-8.\n\t// Some systems like macOS miss C.UTF8, so fall back to the US English locale.\n\tif out, _ := exec.Command(\"locale\", \"-a\").Output(); strings.Contains(\n\t\tstrings.ToLower(string(out)), \"c.utf\",\n\t) {\n\t\tos.Setenv(\"LANGUAGE\", \"C.UTF-8\")\n\t\tos.Setenv(\"LC_ALL\", \"C.UTF-8\")\n\t} else {\n\t\tos.Setenv(\"LANGUAGE\", \"en_US.UTF-8\")\n\t\tos.Setenv(\"LC_ALL\", \"en_US.UTF-8\")\n\t}\n\n\t// Bash prints the pwd after changing directories when CDPATH is set.\n\tos.Unsetenv(\"CDPATH\")\n\n\tpathDir, err := os.MkdirTemp(\"\", \"interp-bin-\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// These short names are commonly used as variables.\n\t// Ensure they are unset as env vars.\n\t// We can't easily remove names from $PATH,\n\t// so do the next best thing: override each name with a failing script.\n\tfor _, s := range []string{\n\t\t\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"foo\", \"bar\",\n\t} {\n\t\tos.Unsetenv(s)\n\t\tpathFile := filepath.Join(pathDir, s)\n\t\tif err := os.WriteFile(pathFile, []byte(\"#!/bin/sh\\necho NO_SUCH_COMMAND; exit 1\"), 0o777); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tos.Setenv(\"PATH\", pathDir+string(os.PathListSeparator)+os.Getenv(\"PATH\"))\n}\n"
  },
  {
    "path": "interp/api.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package interp implements an interpreter to execute shell programs\n// parsed by the [syntax] package as either [syntax.LangBash]\n// or [syntax.LangPOSIX], behaving like Bash as a result.\n//\n// The interpreter currently aims to behave like a non-interactive shell,\n// which is how most shells run scripts, and is more useful to machines.\n// In the future, it may gain an option to behave like an interactive shell.\npackage interp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// A Runner interprets shell programs. It can be reused, but it is not safe for\n// concurrent use. Use [New] to build a new Runner.\n//\n// Note that writes to Stdout and Stderr may be concurrent if background\n// commands are used. If you plan on using an [io.Writer] implementation that\n// isn't safe for concurrent use, consider a workaround like hiding writes\n// behind a mutex.\n//\n// Runner's exported fields are meant to be configured via [RunnerOption];\n// once a Runner has been created, the fields should be treated as read-only.\ntype Runner struct {\n\t// Env specifies the initial environment for the interpreter, which must\n\t// not be nil. It can only be set via [Env].\n\t//\n\t// If it includes a TMPDIR variable describing an absolute directory,\n\t// it is used as the directory in which to create temporary files needed\n\t// for the interpreter's use, such as named pipes for process substitutions.\n\t// Otherwise, [os.TempDir] is used.\n\tEnv expand.Environ\n\n\t// writeEnv overlays [Runner.Env] so that we can write environment variables\n\t// as an overlay.\n\twriteEnv expand.WriteEnviron\n\n\t// Dir specifies the working directory of the command, which must be an\n\t// absolute path. It can only be set via [Dir].\n\tDir string\n\n\t// tempDir is either $TMPDIR from [Runner.Env], or [os.TempDir].\n\ttempDir string\n\n\t// Params are the current shell parameters, e.g. from running a shell\n\t// file or calling a function. Accessible via the $@/$* family of vars.\n\t// It can only be set via [Params].\n\tParams []string\n\n\t// Separate maps - note that bash allows a name to be both a var and a\n\t// func simultaneously.\n\t// Vars is mostly superseded by Env at this point.\n\t// TODO(v4): remove these\n\n\tVars  map[string]expand.Variable\n\tFuncs map[string]*syntax.Stmt\n\n\talias map[string]alias\n\n\t// callHandler is a function allowing to replace a simple command's\n\t// arguments. It may be nil.\n\tcallHandler CallHandlerFunc\n\n\t// execHandler is responsible for executing programs. It must not be nil.\n\texecHandler ExecHandlerFunc\n\n\t// execMiddlewares grows with calls to [ExecHandlers],\n\t// and is used to construct execHandler when Reset is first called.\n\t// The slice is needed to preserve the relative order of middlewares.\n\texecMiddlewares []func(ExecHandlerFunc) ExecHandlerFunc\n\n\t// openHandler is a function responsible for opening files. It must not be nil.\n\topenHandler OpenHandlerFunc\n\n\t// readDirHandler is a function responsible for reading directories during\n\t// glob expansion. It must be non-nil.\n\treadDirHandler ReadDirHandlerFunc2\n\n\t// statHandler is a function responsible for getting file stat. It must be non-nil.\n\tstatHandler StatHandlerFunc\n\n\tstdin  *os.File // e.g. the read end of a pipe\n\tstdout io.Writer\n\tstderr io.Writer\n\n\tecfg *expand.Config\n\tectx context.Context // just so that Runner.Subshell can use it again\n\n\t// didReset remembers whether the runner has ever been reset. This is\n\t// used so that Reset is automatically called when running any program\n\t// or node for the first time on a Runner.\n\tdidReset bool\n\n\tusedNew bool\n\n\tfilename string // only if Node was a File\n\n\t// >0 to break or continue out of N enclosing loops\n\tbreakEnclosing, contnEnclosing int\n\n\tinLoop       bool\n\tinFunc       bool\n\tinSource     bool\n\thandlingTrap bool // whether we're currently in a trap callback\n\n\t// track if a sourced script set positional parameters\n\tsourceSetParams bool\n\n\t// noErrExit prevents failing commands from triggering [optErrExit],\n\t// such as the condition in a [syntax.IfClause].\n\tnoErrExit bool\n\n\t// The current and last exit statuses. They can only be different if\n\t// the interpreter is in the middle of running a statement. In that\n\t// scenario, 'exit' is the status for the current statement being run,\n\t// and 'lastExit' corresponds to the previous statement that was run.\n\texit     exitStatus\n\tlastExit exitStatus\n\n\tlastExpandExit exitStatus // used to surface exit statuses while expanding fields\n\n\t// bgProcs holds all background shells spawned by this runner.\n\t// Their PIDs are 1-indexed, from 1 to len(bgProcs), with a \"g\" prefix\n\t// to distinguish them from real PIDs on the host operating system.\n\t//\n\t// Note that each shell only tracks its direct children;\n\t// subshells do not share nor inherit the background PIDs they can wait for.\n\tbgProcs []bgProc\n\n\topts runnerOpts\n\n\torigDir    string\n\torigParams []string\n\torigOpts   runnerOpts\n\torigStdin  *os.File\n\torigStdout io.Writer\n\torigStderr io.Writer\n\n\t// Most scripts don't use pushd/popd, so make space for the initial PWD\n\t// without requiring an extra allocation.\n\tdirStack     []string\n\tdirBootstrap [1]string\n\n\toptState getopts\n\n\t// keepRedirs is used so that \"exec\" can make any redirections\n\t// apply to the current shell, and not just the command.\n\tkeepRedirs bool\n\n\t// Fake signal callbacks\n\tcallbackErr  string\n\tcallbackExit string\n}\n\n// exitStatus holds the state of the shell after running one command.\n// Beyond the exit status code, it also holds whether the shell should return or exit,\n// as well as any Go error values that should be given back to the user.\n//\n// TODO(v4): consider replacing ExitStatus with a struct like this,\n// so that an [ExecHandlerFunc] can e.g. mimic `exit 0` or fatal errors\n// with specific exit codes.\ntype exitStatus struct {\n\t// code is the exit status code.\n\t// When code is zero, err must be nil.\n\tcode uint8\n\n\t// TODO: consider an enum, as only one of these should be set at a time\n\treturning bool // whether the current function `return`ed\n\texiting   bool // whether the current shell is exiting\n\tfatalExit bool // whether the current shell is exiting due to a fatal error; err below must not be nil\n\n\t// err holds the error information for a non-zero exit status code or fatal error.\n\t// Used so that running a single statement with a custom handler\n\t// which returns a non-fatal Go error, such as a Go error wrapping [NewExitStatus],\n\t// can be returned by [Runner.Run] without being lost entirely.\n\terr error\n}\n\n// clear sets the exit status code and error to zero, as long as the exit status\n// was not set by `return`, `exit`, or a fatal error.\nfunc (e *exitStatus) clear() {\n\tif e.returning || e.exiting || e.fatalExit {\n\t\treturn\n\t}\n\te.code = 0\n\te.err = nil\n}\n\nfunc (e *exitStatus) ok() bool { return e.code == 0 }\n\n// oneIf sets the exit status code to 1 if b is true.\n// Note that it assumes the exit status hasn't been set yet,\n// meaning that [exitStatus.code] and [exitStatus.err] are zero values.\nfunc (e *exitStatus) oneIf(b bool) {\n\tif b {\n\t\te.code = 1\n\t}\n}\n\nfunc (e *exitStatus) fatal(err error) {\n\tif e.fatalExit || err == nil {\n\t\treturn\n\t}\n\te.exiting = true\n\te.fatalExit = true\n\te.err = err\n\tif e.code == 0 {\n\t\te.code = 1\n\t}\n}\n\nfunc (e *exitStatus) fromHandlerError(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\tvar exit errBuiltinExitStatus\n\tvar es ExitStatus\n\tif errors.As(err, &exit) {\n\t\t*e = exitStatus(exit)\n\t} else if errors.As(err, &es) {\n\t\te.err = err\n\t\te.code = uint8(es)\n\t} else {\n\t\te.fatal(err) // handler's custom fatal error\n\t}\n}\n\ntype bgProc struct {\n\t// closed when the background process finishes,\n\t// after which point the result fields below are set.\n\tdone chan struct{}\n\n\texit *exitStatus\n}\n\ntype alias struct {\n\targs  []*syntax.Word\n\tblank bool\n}\n\n// New creates a new Runner, applying a number of options. If applying any of\n// the options results in an error, it is returned.\n//\n// Any unset options fall back to their defaults. For example, not supplying the\n// environment falls back to the process's environment, and not supplying the\n// standard output writer means that the output will be discarded.\nfunc New(opts ...RunnerOption) (*Runner, error) {\n\tr := &Runner{\n\t\tusedNew:        true,\n\t\topenHandler:    DefaultOpenHandler(),\n\t\treadDirHandler: DefaultReadDirHandler2(),\n\t\tstatHandler:    DefaultStatHandler(),\n\t}\n\tr.dirStack = r.dirBootstrap[:0]\n\t// turn \"on\" the default Bash options\n\tfor i, opt := range bashOptsTable {\n\t\tr.opts[len(posixOptsTable)+i] = opt.defaultState\n\t}\n\n\tfor _, opt := range opts {\n\t\tif err := opt(r); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Set the default fallbacks, if necessary.\n\tif r.Env == nil {\n\t\tEnv(nil)(r)\n\t}\n\tif r.Dir == \"\" {\n\t\tif err := Dir(\"\")(r); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif r.stdout == nil || r.stderr == nil {\n\t\tStdIO(r.stdin, r.stdout, r.stderr)(r)\n\t}\n\treturn r, nil\n}\n\n// RunnerOption can be passed to [New] to alter a [Runner]'s behaviour.\n// It can also be applied directly on an existing Runner,\n// such as interp.Params(\"-e\")(runner).\n// Note that options cannot be applied once Run or Reset have been called.\ntype RunnerOption func(*Runner) error\n\n// TODO: enforce the rule above via didReset.\n\n// Env sets the interpreter's environment. If nil, a copy of the current\n// process's environment is used.\nfunc Env(env expand.Environ) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tif env == nil {\n\t\t\tenv = expand.ListEnviron(os.Environ()...)\n\t\t}\n\t\tr.Env = env\n\t\treturn nil\n\t}\n}\n\n// Dir sets the interpreter's working directory. If empty, the process's current\n// directory is used.\nfunc Dir(path string) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tif path == \"\" {\n\t\t\tpath, err := os.Getwd()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"could not get current dir: %w\", err)\n\t\t\t}\n\t\t\tr.Dir = path\n\t\t\treturn nil\n\t\t}\n\t\tpath, err := filepath.Abs(path)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not get absolute dir: %w\", err)\n\t\t}\n\t\tinfo, err := os.Stat(path)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not stat: %w\", err)\n\t\t}\n\t\tif !info.IsDir() {\n\t\t\treturn fmt.Errorf(\"%s is not a directory\", path)\n\t\t}\n\t\tr.Dir = path\n\t\treturn nil\n\t}\n}\n\n// Interactive configures the interpreter to behave like an interactive shell,\n// akin to Bash. Currently, this only enables the expansion of aliases,\n// but later on it should also change other behavior.\nfunc Interactive(enabled bool) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tr.opts[optExpandAliases] = enabled\n\t\treturn nil\n\t}\n}\n\n// Params populates the shell options and parameters. For example, Params(\"-e\",\n// \"--\", \"foo\") will set the \"-e\" option and the parameters [\"foo\"], and\n// Params(\"+e\") will unset the \"-e\" option and leave the parameters untouched.\n//\n// This is similar to what the interpreter's \"set\" builtin does.\nfunc Params(args ...string) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tfp := flagParser{remaining: args}\n\t\tfor fp.more() {\n\t\t\tflag := fp.flag()\n\t\t\tif flag == \"-\" {\n\t\t\t\t// TODO: implement \"The -x and -v options are turned off.\"\n\t\t\t\tif args := fp.args(); len(args) > 0 {\n\t\t\t\t\tr.Params = args\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tenable := flag[0] == '-'\n\t\t\tif flag[1] != 'o' {\n\t\t\t\topt := r.posixOptByFlag(flag[1])\n\t\t\t\tif opt == nil {\n\t\t\t\t\treturn fmt.Errorf(\"invalid option: %q\", flag)\n\t\t\t\t}\n\t\t\t\t*opt = enable\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvalue := fp.value()\n\t\t\tif value == \"\" && enable {\n\t\t\t\tfor i, opt := range &posixOptsTable {\n\t\t\t\t\tr.printOptLine(opt.name, r.opts[i], true)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif value == \"\" && !enable {\n\t\t\t\tfor i, opt := range &posixOptsTable {\n\t\t\t\t\tsetFlag := \"+o\"\n\t\t\t\t\tif r.opts[i] {\n\t\t\t\t\t\tsetFlag = \"-o\"\n\t\t\t\t\t}\n\t\t\t\t\tr.outf(\"set %s %s\\n\", setFlag, opt.name)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\topt := r.posixOptByName(value)\n\t\t\tif opt == nil {\n\t\t\t\treturn fmt.Errorf(\"invalid option: %q\", value)\n\t\t\t}\n\t\t\t*opt = enable\n\t\t}\n\t\tif args := fp.args(); args != nil {\n\t\t\t// If \"--\" wasn't given and there were zero arguments,\n\t\t\t// we don't want to override the current parameters.\n\t\t\tr.Params = args\n\n\t\t\t// Record whether a sourced script sets the parameters.\n\t\t\tif r.inSource {\n\t\t\t\tr.sourceSetParams = true\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// CallHandler sets the call handler. See [CallHandlerFunc] for more info.\nfunc CallHandler(f CallHandlerFunc) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tr.callHandler = f\n\t\treturn nil\n\t}\n}\n\n// ExecHandler sets one command execution handler,\n// which replaces [DefaultExecHandler](2 * time.Second).\n//\n// Deprecated: use [ExecHandlers] instead, which allows chaining handlers more easily\n// like middleware functions.\nfunc ExecHandler(f ExecHandlerFunc) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tr.execHandler = f\n\t\treturn nil\n\t}\n}\n\n// ExecHandlers appends middlewares to handle command execution.\n// The middlewares are chained from first to last, and the first is called by the runner.\n// Each middleware is expected to call the \"next\" middleware at most once.\n//\n// For example, a middleware may implement only some commands.\n// For those commands, it can run its logic and avoid calling \"next\".\n// For any other commands, it can call \"next\" with the original parameters.\n//\n// Another common example is a middleware which always calls \"next\",\n// but runs custom logic either before or after that call.\n// For instance, a middleware could change the arguments to the \"next\" call,\n// or it could print log lines before or after the call to \"next\".\n//\n// The last exec handler is always [DefaultExecHandler](2 * time.Second).\nfunc ExecHandlers(middlewares ...func(next ExecHandlerFunc) ExecHandlerFunc) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tr.execMiddlewares = append(r.execMiddlewares, middlewares...)\n\t\treturn nil\n\t}\n}\n\n// TODO: consider porting the middleware API in [ExecHandlers] to [OpenHandler],\n// [ReadDirHandler2], and [StatHandler].\n\n// TODO(v4): now that [ExecHandlers] allows calling a next handler with changed\n// arguments, one of the two advantages of [CallHandler] is gone. The other is the\n// ability to work with builtins; if we make [ExecHandlers] work with builtins, we\n// could join both APIs.\n\n// OpenHandler sets file open handler. See [OpenHandlerFunc] for more info.\nfunc OpenHandler(f OpenHandlerFunc) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tr.openHandler = f\n\t\treturn nil\n\t}\n}\n\n// ReadDirHandler sets the read directory handler. See [ReadDirHandlerFunc] for more info.\n//\n// Deprecated: use [ReadDirHandler2].\nfunc ReadDirHandler(f ReadDirHandlerFunc) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tr.readDirHandler = func(ctx context.Context, path string) ([]fs.DirEntry, error) {\n\t\t\tinfos, err := f(ctx, path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tentries := make([]fs.DirEntry, len(infos))\n\t\t\tfor i, info := range infos {\n\t\t\t\tentries[i] = fs.FileInfoToDirEntry(info)\n\t\t\t}\n\t\t\treturn entries, nil\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// ReadDirHandler2 sets the read directory handler. See [ReadDirHandlerFunc2] for more info.\nfunc ReadDirHandler2(f ReadDirHandlerFunc2) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tr.readDirHandler = f\n\t\treturn nil\n\t}\n}\n\n// StatHandler sets the stat handler. See [StatHandlerFunc] for more info.\nfunc StatHandler(f StatHandlerFunc) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tr.statHandler = f\n\t\treturn nil\n\t}\n}\n\nfunc stdinFile(r io.Reader) (*os.File, error) {\n\tswitch r := r.(type) {\n\tcase *os.File:\n\t\treturn r, nil\n\tcase nil:\n\t\treturn nil, nil\n\tdefault:\n\t\tpr, pw, err := os.Pipe()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tgo func() {\n\t\t\tio.Copy(pw, r)\n\t\t\tpw.Close()\n\t\t}()\n\t\treturn pr, nil\n\t}\n}\n\n// StdIO configures an interpreter's standard input, standard output, and\n// standard error. If out or err are nil, they default to a writer that discards\n// the output.\n//\n// Note that providing a non-nil standard input other than [*os.File] will require\n// an [os.Pipe] and spawning a goroutine to copy into it,\n// as an [os.File] is the only way to share a reader with subprocesses.\n// This may cause the interpreter to consume the entire reader.\n// See [os/exec.Cmd.Stdin].\n//\n// When providing an [*os.File] as standard input, consider using an [os.Pipe]\n// as it has the best chance to support cancellable reads via [os.File.SetReadDeadline],\n// so that cancelling the runner's context can stop a blocked standard input read.\nfunc StdIO(in io.Reader, out, err io.Writer) RunnerOption {\n\treturn func(r *Runner) error {\n\t\tstdin, _err := stdinFile(in)\n\t\tif _err != nil {\n\t\t\treturn _err\n\t\t}\n\t\tr.stdin = stdin\n\t\tif out == nil {\n\t\t\tout = io.Discard\n\t\t}\n\t\tr.stdout = out\n\t\tif err == nil {\n\t\t\terr = io.Discard\n\t\t}\n\t\tr.stderr = err\n\t\treturn nil\n\t}\n}\n\nfunc (r *Runner) posixOptByName(name string) *bool {\n\tfor i, opt := range &posixOptsTable {\n\t\tif opt.name == name {\n\t\t\treturn &r.opts[i]\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *Runner) posixOptByFlag(flag byte) *bool {\n\tfor i, opt := range &posixOptsTable {\n\t\tif opt.flag == flag {\n\t\t\treturn &r.opts[i]\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *Runner) bashOptByName(name string) (status *bool, supported bool) {\n\tfor i, opt := range bashOptsTable {\n\t\tif opt.name == name {\n\t\t\tindex := len(posixOptsTable) + i\n\t\t\treturn &r.opts[index], opt.supported\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// runnerOpts contains all POSIX Shell and Bash options as one contiguous table.\ntype runnerOpts [len(posixOptsTable) + len(bashOptsTable)]bool\n\ntype posixOpt struct {\n\tflag byte   // one-character flag form for this option; a space if none exists\n\tname string // full name of the option\n}\n\ntype bashOpt struct {\n\tname         string\n\tdefaultState bool // Bash's default value for this option\n\tsupported    bool // whether we support the option's non-default state\n}\n\nvar posixOptsTable = [...]posixOpt{\n\t// sorted alphabetically by name\n\t{'a', \"allexport\"},\n\t{'e', \"errexit\"},\n\t{'n', \"noexec\"},\n\t{'f', \"noglob\"},\n\t{'u', \"nounset\"},\n\t{'x', \"xtrace\"},\n\t{' ', \"pipefail\"},\n}\n\nvar bashOptsTable = [...]bashOpt{\n\t// supported options, sorted alphabetically by name\n\t{\n\t\tname:         \"dotglob\",\n\t\tdefaultState: false,\n\t\tsupported:    true,\n\t},\n\t{\n\t\tname:         \"expand_aliases\",\n\t\tdefaultState: false,\n\t\tsupported:    true,\n\t},\n\t{\n\t\tname:         \"extglob\",\n\t\tdefaultState: false,\n\t\tsupported:    true,\n\t},\n\t{\n\t\tname:         \"globstar\",\n\t\tdefaultState: false,\n\t\tsupported:    true,\n\t},\n\t{\n\t\tname:         \"nocaseglob\",\n\t\tdefaultState: false,\n\t\tsupported:    true,\n\t},\n\t{\n\t\tname:         \"nullglob\",\n\t\tdefaultState: false,\n\t\tsupported:    true,\n\t},\n\t// unsupported options, sorted alphabetically by name\n\t{name: \"assoc_expand_once\"},\n\t{name: \"autocd\"},\n\t{name: \"cdable_vars\"},\n\t{name: \"cdspell\"},\n\t{name: \"checkhash\"},\n\t{name: \"checkjobs\"},\n\t{\n\t\tname:         \"checkwinsize\",\n\t\tdefaultState: true,\n\t},\n\t{\n\t\tname:         \"cmdhist\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"compat31\"},\n\t{name: \"compat32\"},\n\t{name: \"compat40\"},\n\t{name: \"compat41\"},\n\t{name: \"compat42\"},\n\t{name: \"compat44\"},\n\t{name: \"compat43\"},\n\t{name: \"compat44\"},\n\t{\n\t\tname:         \"complete_fullquote\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"direxpand\"},\n\t{name: \"dirspell\"},\n\t{name: \"execfail\"},\n\t{name: \"extdebug\"},\n\t{\n\t\tname:         \"extquote\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"failglob\"},\n\t{\n\t\tname:         \"force_fignore\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"globasciiranges\"},\n\t{name: \"gnu_errfmt\"},\n\t{name: \"histappend\"},\n\t{name: \"histreedit\"},\n\t{name: \"histverify\"},\n\t{\n\t\tname:         \"hostcomplete\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"huponexit\"},\n\t{\n\t\tname:         \"inherit_errexit\",\n\t\tdefaultState: true,\n\t},\n\t{\n\t\tname:         \"interactive_comments\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"lastpipe\"},\n\t{name: \"lithist\"},\n\t{name: \"localvar_inherit\"},\n\t{name: \"localvar_unset\"},\n\t{name: \"login_shell\"},\n\t{name: \"mailwarn\"},\n\t{name: \"no_empty_cmd_completion\"},\n\t{name: \"nocasematch\"},\n\t{\n\t\tname:         \"progcomp\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"progcomp_alias\"},\n\t{\n\t\tname:         \"promptvars\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"restricted_shell\"},\n\t{name: \"shift_verbose\"},\n\t{\n\t\tname:         \"sourcepath\",\n\t\tdefaultState: true,\n\t},\n\t{name: \"xpg_echo\"},\n}\n\n// To access the shell options arrays without a linear search when we\n// know which option we're after at compile time. First come the shell options,\n// then the bash options.\nconst (\n\t// These correspond to indexes in [shellOptsTable]\n\toptAllExport = iota\n\toptErrExit\n\toptNoExec\n\toptNoGlob\n\toptNoUnset\n\toptXTrace\n\toptPipeFail\n\n\t// These correspond to indexes (offset by the above seven items) of\n\t// supported options in [bashOptsTable]\n\toptDotGlob\n\toptExpandAliases\n\toptExtGlob\n\toptGlobStar\n\toptNoCaseGlob\n\toptNullGlob\n)\n\n// Reset returns a runner to its initial state, right before the first call to\n// Run or Reset.\n//\n// Typically, this function only needs to be called if a runner is reused to run\n// multiple programs non-incrementally. Not calling Reset between each run will\n// mean that the shell state will be kept, including variables, options, and the\n// current directory.\nfunc (r *Runner) Reset() {\n\tif !r.usedNew {\n\t\tpanic(\"use interp.New to construct a Runner\")\n\t}\n\tif !r.didReset {\n\t\tr.origDir = r.Dir\n\t\tr.origParams = r.Params\n\t\tr.origOpts = r.opts\n\t\tr.origStdin = r.stdin\n\t\tr.origStdout = r.stdout\n\t\tr.origStderr = r.stderr\n\n\t\tif r.execHandler != nil && len(r.execMiddlewares) > 0 {\n\t\t\tpanic(\"interp.ExecHandler should be replaced with interp.ExecHandlers, not mixed\")\n\t\t}\n\t\tif r.execHandler == nil {\n\t\t\tr.execHandler = DefaultExecHandler(2 * time.Second)\n\t\t}\n\t\t// Middlewares are chained from first to last, and each can call the\n\t\t// next in the chain, so we need to construct the chain backwards.\n\t\tfor _, mw := range slices.Backward(r.execMiddlewares) {\n\t\t\tr.execHandler = mw(r.execHandler)\n\t\t}\n\t\t// Fill tempDir; only need to do this once given that Env will not change.\n\t\tif dir := r.Env.Get(\"TMPDIR\").String(); filepath.IsAbs(dir) {\n\t\t\tr.tempDir = dir\n\t\t} else {\n\t\t\tr.tempDir = os.TempDir()\n\t\t}\n\t\t// Clean it as we will later do a string prefix match.\n\t\tr.tempDir = filepath.Clean(r.tempDir)\n\t}\n\t// reset the internal state\n\t*r = Runner{\n\t\tEnv:            r.Env,\n\t\ttempDir:        r.tempDir,\n\t\tcallHandler:    r.callHandler,\n\t\texecHandler:    r.execHandler,\n\t\topenHandler:    r.openHandler,\n\t\treadDirHandler: r.readDirHandler,\n\t\tstatHandler:    r.statHandler,\n\n\t\t// These can be set by functions like [Dir] or [Params], but\n\t\t// builtins can overwrite them; reset the fields to whatever the\n\t\t// constructor set up.\n\t\tDir:    r.origDir,\n\t\tParams: r.origParams,\n\t\topts:   r.origOpts,\n\t\tstdin:  r.origStdin,\n\t\tstdout: r.origStdout,\n\t\tstderr: r.origStderr,\n\n\t\torigDir:    r.origDir,\n\t\torigParams: r.origParams,\n\t\torigOpts:   r.origOpts,\n\t\torigStdin:  r.origStdin,\n\t\torigStdout: r.origStdout,\n\t\torigStderr: r.origStderr,\n\n\t\t// emptied below, to reuse the space\n\t\tVars: r.Vars,\n\n\t\tdirStack: r.dirStack[:0],\n\t\tusedNew:  r.usedNew,\n\t}\n\t// Ensure we stop referencing any pointers before we reuse bgProcs.\n\tclear(r.bgProcs)\n\tr.bgProcs = r.bgProcs[:0]\n\n\tif r.Vars == nil {\n\t\tr.Vars = make(map[string]expand.Variable)\n\t} else {\n\t\tclear(r.Vars)\n\t}\n\t// TODO(v4): Use the supplied Env directly if it implements enough methods.\n\tr.writeEnv = &overlayEnviron{parent: r.Env}\n\tif !r.writeEnv.Get(\"HOME\").IsSet() {\n\t\thome, _ := os.UserHomeDir()\n\t\tr.setVarString(\"HOME\", home)\n\t}\n\tif !r.writeEnv.Get(\"UID\").IsSet() {\n\t\tr.setVar(\"UID\", expand.Variable{\n\t\t\tSet:      true,\n\t\t\tKind:     expand.String,\n\t\t\tReadOnly: true,\n\t\t\tStr:      strconv.Itoa(os.Getuid()),\n\t\t})\n\t}\n\tif !r.writeEnv.Get(\"EUID\").IsSet() {\n\t\tr.setVar(\"EUID\", expand.Variable{\n\t\t\tSet:      true,\n\t\t\tKind:     expand.String,\n\t\t\tReadOnly: true,\n\t\t\tStr:      strconv.Itoa(os.Geteuid()),\n\t\t})\n\t}\n\tif !r.writeEnv.Get(\"GID\").IsSet() {\n\t\tr.setVar(\"GID\", expand.Variable{\n\t\t\tSet:      true,\n\t\t\tKind:     expand.String,\n\t\t\tReadOnly: true,\n\t\t\tStr:      strconv.Itoa(os.Getgid()),\n\t\t})\n\t}\n\tr.setVarString(\"PWD\", r.Dir)\n\tr.setVarString(\"IFS\", \" \\t\\n\")\n\tr.setVarString(\"OPTIND\", \"1\")\n\n\tr.dirStack = append(r.dirStack, r.Dir)\n\n\tr.didReset = true\n}\n\n// ExitStatus is a non-zero status code resulting from running a shell node.\ntype ExitStatus uint8\n\nfunc (s ExitStatus) Error() string { return fmt.Sprintf(\"exit status %d\", s) }\n\n// NewExitStatus creates an error which contains the specified exit status code.\n//\n// Deprecated: use [ExitStatus] directly.\n//\n//go:fix inline\nfunc NewExitStatus(status uint8) error {\n\treturn ExitStatus(status)\n}\n\n// IsExitStatus checks whether error contains an exit status and returns it.\n//\n// Deprecated: use [errors.As] with [ExitStatus] directly.\n//\n//go:fix inline\nfunc IsExitStatus(err error) (status uint8, ok bool) {\n\tvar es ExitStatus\n\tif errors.As(err, &es) {\n\t\treturn uint8(es), true\n\t}\n\treturn 0, false\n}\n\n// Run interprets a node, which can be a [*File], [*Stmt], or [Command]. If a non-nil\n// error is returned, it will typically contain a command's exit status, which\n// can be retrieved with [IsExitStatus].\n//\n// Run can be called multiple times synchronously to interpret programs\n// incrementally. To reuse a [Runner] without keeping the internal shell state,\n// call Reset.\n//\n// Calling Run on an entire [*File] implies an exit, meaning that an exit trap may\n// run.\nfunc (r *Runner) Run(ctx context.Context, node syntax.Node) error {\n\tif !r.didReset {\n\t\tr.Reset()\n\t}\n\tr.fillExpandConfig(ctx)\n\tr.exit = exitStatus{}\n\tr.filename = \"\"\n\tswitch node := node.(type) {\n\tcase *syntax.File:\n\t\tr.filename = node.Name\n\t\tr.stmts(ctx, node.Stmts)\n\tcase *syntax.Stmt:\n\t\tr.stmt(ctx, node)\n\tcase syntax.Command:\n\t\tr.cmd(ctx, node)\n\tdefault:\n\t\treturn fmt.Errorf(\"node can only be File, Stmt, or Command: %T\", node)\n\t}\n\tr.trapCallback(ctx, r.callbackExit, \"exit\")\n\tmaps.Insert(r.Vars, r.writeEnv.Each)\n\t// Return the first of: a fatal error, a non-fatal handler error, or the exit code.\n\tif err := r.exit.err; err != nil {\n\t\tif r.exit.code == 0 {\n\t\t\t// This should never happen; too much code relies on checking [exitStatus.code]\n\t\t\t// to see if the last command succeeded or failed. [exitStatus.err] should only be\n\t\t\t// additional information, so fail loudly if the invariant is broken.\n\t\t\tpanic(\"ended up with a non-nil exitStatus.err but a zero exitStatus.code\")\n\t\t}\n\t\treturn err\n\t}\n\tif code := r.exit.code; code != 0 {\n\t\treturn ExitStatus(code)\n\t}\n\treturn nil\n}\n\n// Exited reports whether the last Run call should exit an entire shell. This\n// can be triggered by the \"exit\" built-in command, for example.\n//\n// Note that this state is overwritten at every Run call, so it should be\n// checked immediately after each Run call.\nfunc (r *Runner) Exited() bool {\n\treturn r.exit.exiting\n}\n\n// Subshell makes a copy of the given [Runner], suitable for use concurrently\n// with the original. The copy will have the same environment, including\n// variables and functions, but they can all be modified without affecting the\n// original.\n//\n// Subshell is not safe to use concurrently with [Run]. Orchestrating this is\n// left up to the caller; no locking is performed.\n//\n// To replace e.g. stdin/out/err, do [StdIO](r.stdin, r.stdout, r.stderr)(r) on\n// the copy.\nfunc (r *Runner) Subshell() *Runner {\n\treturn r.subshell(true)\n}\n\n// subshell is like [Runner.subshell], but allows skipping some allocations and copies\n// when creating subshells which will not be used concurrently with the parent shell.\n// TODO(v4): we should expose this, e.g. SubshellForeground and SubshellBackground.\nfunc (r *Runner) subshell(background bool) *Runner {\n\tif !r.didReset {\n\t\tr.Reset()\n\t}\n\t// Keep in sync with the Runner type. Manually copy fields, to not copy\n\t// sensitive ones like [errgroup.Group], and to do deep copies of slices.\n\tr2 := &Runner{\n\t\tDir:            r.Dir,\n\t\ttempDir:        r.tempDir,\n\t\tParams:         r.Params,\n\t\tcallHandler:    r.callHandler,\n\t\texecHandler:    r.execHandler,\n\t\topenHandler:    r.openHandler,\n\t\treadDirHandler: r.readDirHandler,\n\t\tstatHandler:    r.statHandler,\n\t\tstdin:          r.stdin,\n\t\tstdout:         r.stdout,\n\t\tstderr:         r.stderr,\n\t\tfilename:       r.filename,\n\t\topts:           r.opts,\n\t\tusedNew:        r.usedNew,\n\t\texit:           r.exit,\n\t\tlastExit:       r.lastExit,\n\n\t\torigStdout: r.origStdout, // used for process substitutions\n\t}\n\tr2.writeEnv = newOverlayEnviron(r.writeEnv, background)\n\t// Funcs are copied, since they might be modified.\n\tr2.Funcs = maps.Clone(r.Funcs)\n\tr2.Vars = make(map[string]expand.Variable)\n\tr2.alias = maps.Clone(r.alias)\n\n\tr2.dirStack = append(r2.dirBootstrap[:0], r.dirStack...)\n\tr2.fillExpandConfig(r.ectx)\n\tr2.didReset = true\n\treturn r2\n}\n"
  },
  {
    "path": "interp/builtin.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"cmp\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"golang.org/x/term\"\n\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// TODO: given the categories below, perhaps this should be more like:\n//\n//   func IsBuiltin(lang syntax.LangVariant, name string) bool\n//\n// or perhaps some API that also lets the user iterate through the builtins?\n//\n// Also, should we move this to the syntax package too?\n// It's not a syntactical property strictly speaking,\n// but it's also odd to require importing the interp package for it.\n\n// IsBuiltin returns true if the given word is a POSIX Shell\n// or Bash builtin.\nfunc IsBuiltin(name string) bool {\n\tswitch name {\n\tcase\n\t\t// POSIX Shell builtins, from section 1.d obtained in September 2025 from:\n\t\t// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_01_01\n\t\t\"alias\",\n\t\t\"bg\",\n\t\t\"cd\",\n\t\t\"command\",\n\t\t\"false\",\n\t\t\"fc\",\n\t\t\"fg\",\n\t\t\"getopts\",\n\t\t\"hash\",\n\t\t\"jobs\",\n\t\t\"kill\",\n\t\t\"newgrp\",\n\t\t\"pwd\",\n\t\t\"read\",\n\t\t\"true\",\n\t\t\"umask\",\n\t\t\"unalias\",\n\t\t\"wait\",\n\n\t\t// POSIX Shell special built-ins, obtained in September 2025 from:\n\t\t// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_14\n\t\t\"break\",\n\t\t\":\",\n\t\t\"continue\",\n\t\t\".\",\n\t\t\"eval\",\n\t\t\"exec\",\n\t\t\"exit\",\n\t\t\"export\",   // NOTE: our parser treats this as a keyword\n\t\t\"readonly\", // NOTE: our parser treats this as a keyword\n\t\t\"return\",\n\t\t\"set\",\n\t\t\"shift\",\n\t\t\"times\",\n\t\t\"trap\",\n\t\t\"unset\",\n\n\t\t// Bash built-ins which are not present in POSIX, obtained in September 2025 from:\n\t\t// https://man.archlinux.org/man/bash.1.en#SHELL_BUILTIN_COMMANDS\n\t\t\"source\",\n\t\t\"bind\",\n\t\t\"builtin\",\n\t\t\"caller\",\n\t\t\"compgen\",\n\t\t\"complete\",\n\t\t\"compopt\",\n\t\t\"declare\", // NOTE: our parser treats this as a keyword\n\t\t\"typeset\", // NOTE: our parser treats this as a keyword\n\t\t\"dirs\",\n\t\t\"disown\",\n\t\t\"echo\", // TODO: surely this is POSIX? but why is it not in the main POSIX spec page?\n\t\t\"enable\",\n\t\t\"history\",\n\t\t\"help\",\n\t\t\"let\", // NOTE: our parser treats this as a keyword\n\t\t\"local\",\n\t\t\"logout\",\n\t\t\"mapfile\",\n\t\t\"readarray\",\n\t\t\"popd\",\n\t\t\"printf\", // TODO: surely this is POSIX? but why is it not in the main POSIX spec page?\n\t\t\"pushd\",\n\t\t\"shopt\",\n\t\t\"suspend\",\n\t\t\"test\",\n\t\t\"[\", // NOTE: an alias for \"test\", not explicitly listed\n\t\t\"type\",\n\t\t\"ulimit\":\n\t\treturn true\n\t}\n\treturn false\n}\n\n// TODO: atoi is duplicated in the expand package.\n\n// atoi is like [strconv.ParseInt](s, 10, 64), but it ignores errors and trims whitespace.\nfunc atoi(s string) int64 {\n\ts = strings.TrimSpace(s)\n\tn, _ := strconv.ParseInt(s, 10, 64)\n\treturn n\n}\n\ntype errBuiltinExitStatus exitStatus\n\nfunc (e errBuiltinExitStatus) Error() string {\n\treturn fmt.Sprintf(\"builtin exit status %d\", e.code)\n}\n\n// Builtin allows [ExecHandlerFunc] implementations to execute any builtin,\n// which can be useful for an exec handler to wrap or combine builtin calls.\n//\n// Note that a non-nil error may be returned in cases where the builtin\n// alters the control flow of the runner, even if the builtin did not fail.\n// For example, this is the case with `exit 0` or `return`.\nfunc (hc HandlerContext) Builtin(ctx context.Context, args []string) error {\n\tif hc.kind != handlerKindExec {\n\t\treturn fmt.Errorf(\"HandlerContext.Builtin can only be called via an ExecHandlerFunc\")\n\t}\n\texit := hc.runner.builtin(ctx, hc.Pos, args[0], args[1:])\n\tif exit != (exitStatus{}) {\n\t\treturn errBuiltinExitStatus(exit)\n\t}\n\treturn nil\n}\n\nfunc (r *Runner) builtin(ctx context.Context, pos syntax.Pos, name string, args []string) (exit exitStatus) {\n\tfailf := func(code uint8, format string, args ...any) exitStatus {\n\t\tr.errf(format, args...)\n\t\texit.code = code\n\t\treturn exit\n\t}\n\tswitch name {\n\tcase \":\", \"true\":\n\tcase \"false\":\n\t\texit.code = 1\n\tcase \"exit\":\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\t\texit = r.lastExit\n\t\tcase 1:\n\t\t\tn, err := strconv.Atoi(args[0])\n\t\t\tif err != nil {\n\t\t\t\treturn failf(2, \"invalid exit status code: %q\\n\", args[0])\n\t\t\t}\n\t\t\texit.code = uint8(n)\n\t\tdefault:\n\t\t\treturn failf(1, \"exit cannot take multiple arguments\\n\")\n\t\t}\n\t\texit.exiting = true\n\tcase \"set\":\n\t\tif err := Params(args...)(r); err != nil {\n\t\t\treturn failf(2, \"set: %v\\n\", err)\n\t\t}\n\t\tr.updateExpandOpts()\n\tcase \"shift\":\n\t\tn := 1\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\tcase 1:\n\t\t\tif n2, err := strconv.Atoi(args[0]); err == nil {\n\t\t\t\tn = n2\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfallthrough\n\t\tdefault:\n\t\t\treturn failf(2, \"usage: shift [n]\\n\")\n\t\t}\n\t\tif n >= len(r.Params) {\n\t\t\tr.Params = nil\n\t\t} else {\n\t\t\tr.Params = r.Params[n:]\n\t\t}\n\tcase \"unset\":\n\t\tvars := true\n\t\tfuncs := true\n\tunsetOpts:\n\t\tfor i, arg := range args {\n\t\t\tswitch arg {\n\t\t\tcase \"-v\":\n\t\t\t\tfuncs = false\n\t\t\tcase \"-f\":\n\t\t\t\tvars = false\n\t\t\tdefault:\n\t\t\t\targs = args[i:]\n\t\t\t\tbreak unsetOpts\n\t\t\t}\n\t\t}\n\n\t\tfor _, arg := range args {\n\t\t\tif vars && r.lookupVar(arg).IsSet() {\n\t\t\t\tr.delVar(arg)\n\t\t\t} else if _, ok := r.Funcs[arg]; ok && funcs {\n\t\t\t\tdelete(r.Funcs, arg)\n\t\t\t}\n\t\t}\n\tcase \"echo\":\n\t\tnewline, doExpand := true, false\n\techoOpts:\n\t\tfor len(args) > 0 {\n\t\t\tswitch args[0] {\n\t\t\tcase \"-n\":\n\t\t\t\tnewline = false\n\t\t\tcase \"-e\":\n\t\t\t\tdoExpand = true\n\t\t\tcase \"-E\": // default\n\t\t\tdefault:\n\t\t\t\tbreak echoOpts\n\t\t\t}\n\t\t\targs = args[1:]\n\t\t}\n\t\tfor i, arg := range args {\n\t\t\tif i > 0 {\n\t\t\t\tr.out(\" \")\n\t\t\t}\n\t\t\tif doExpand {\n\t\t\t\targ, _, _ = expand.Format(r.ecfg, arg, nil)\n\t\t\t}\n\t\t\tr.out(arg)\n\t\t}\n\t\tif newline {\n\t\t\tr.out(\"\\n\")\n\t\t}\n\tcase \"printf\":\n\t\tif len(args) == 0 {\n\t\t\treturn failf(2, \"usage: printf format [arguments]\\n\")\n\t\t}\n\t\tformat, args := args[0], args[1:]\n\t\tfor {\n\t\t\ts, n, err := expand.Format(r.ecfg, format, args)\n\t\t\tif err != nil {\n\t\t\t\treturn failf(1, \"%v\\n\", err)\n\t\t\t}\n\t\t\tr.out(s)\n\t\t\targs = args[n:]\n\t\t\tif n == 0 || len(args) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tcase \"break\", \"continue\":\n\t\tif !r.inLoop {\n\t\t\treturn failf(0, \"%s is only useful in a loop\\n\", name)\n\t\t}\n\t\tenclosing := &r.breakEnclosing\n\t\tif name == \"continue\" {\n\t\t\tenclosing = &r.contnEnclosing\n\t\t}\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\t\t*enclosing = 1\n\t\tcase 1:\n\t\t\tif n, err := strconv.Atoi(args[0]); err == nil {\n\t\t\t\t*enclosing = n\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfallthrough\n\t\tdefault:\n\t\t\treturn failf(2, \"usage: %s [n]\\n\", name)\n\t\t}\n\tcase \"pwd\":\n\t\tevalSymlinks := false\n\t\tfor len(args) > 0 {\n\t\t\tswitch args[0] {\n\t\t\tcase \"-L\":\n\t\t\t\tevalSymlinks = false\n\t\t\tcase \"-P\":\n\t\t\t\tevalSymlinks = true\n\t\t\tdefault:\n\t\t\t\treturn failf(2, \"invalid option: %q\\n\", args[0])\n\t\t\t}\n\t\t\targs = args[1:]\n\t\t}\n\t\tpwd := r.envGet(\"PWD\")\n\t\tif evalSymlinks {\n\t\t\tvar err error\n\t\t\tpwd, err = filepath.EvalSymlinks(pwd)\n\t\t\tif err != nil {\n\t\t\t\texit.fatal(err) // perhaps overly dramatic?\n\t\t\t\treturn exit\n\t\t\t}\n\t\t}\n\t\tr.outf(\"%s\\n\", pwd)\n\tcase \"cd\":\n\t\tvar path string\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\t\tpath = r.envGet(\"HOME\")\n\t\tcase 1:\n\t\t\tpath = args[0]\n\n\t\t\t// replicate the commonly implemented behavior of `cd -`\n\t\t\t// ref: https://www.man7.org/linux/man-pages/man1/cd.1p.html#OPERANDS\n\t\t\tif path == \"-\" {\n\t\t\t\tpath = r.envGet(\"OLDPWD\")\n\t\t\t\tr.outf(\"%s\\n\", path)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn failf(2, \"usage: cd [dir]\\n\")\n\t\t}\n\t\texit.code = r.changeDir(ctx, \"cd\", path)\n\tcase \"wait\":\n\t\tfp := flagParser{remaining: args}\n\t\tfor fp.more() {\n\t\t\tswitch flag := fp.flag(); flag {\n\t\t\tcase \"-n\", \"-p\":\n\t\t\t\treturn failf(2, \"wait: unsupported option %q\\n\", flag)\n\t\t\tdefault:\n\t\t\t\treturn failf(2, \"wait: invalid option %q\\n\", flag)\n\t\t\t}\n\t\t}\n\t\tif len(args) == 0 {\n\t\t\t// Note that \"wait\" without arguments always returns exit status zero.\n\t\t\tfor _, bg := range r.bgProcs {\n\t\t\t\t<-bg.done\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tfor _, arg := range args {\n\t\t\targ, ok := strings.CutPrefix(arg, \"g\")\n\t\t\tpid := atoi(arg)\n\t\t\tif !ok || pid <= 0 || pid > int64(len(r.bgProcs)) {\n\t\t\t\treturn failf(1, \"wait: pid %s is not a child of this shell\\n\", arg)\n\t\t\t}\n\t\t\tbg := r.bgProcs[pid-1]\n\t\t\t<-bg.done\n\t\t\texit = *bg.exit\n\t\t}\n\tcase \"builtin\":\n\t\tif len(args) < 1 {\n\t\t\tbreak\n\t\t}\n\t\tif !IsBuiltin(args[0]) {\n\t\t\texit.code = 1\n\t\t\treturn exit\n\t\t}\n\t\texit = r.builtin(ctx, pos, args[0], args[1:])\n\tcase \"type\":\n\t\tanyNotFound := false\n\t\tmode := \"\"\n\t\tfp := flagParser{remaining: args}\n\t\tfor fp.more() {\n\t\t\tswitch flag := fp.flag(); flag {\n\t\t\tcase \"-a\", \"-f\", \"--help\":\n\t\t\t\treturn failf(3, \"command: NOT IMPLEMENTED\\n\")\n\t\t\tcase \"-p\", \"-P\", \"-t\":\n\t\t\t\tmode = flag\n\t\t\tdefault:\n\t\t\t\treturn failf(2, \"command: invalid option %q\\n\", flag)\n\t\t\t}\n\t\t}\n\t\targs := fp.args()\n\t\tfor _, arg := range args {\n\t\t\tif mode == \"-p\" || mode == \"-P\" {\n\t\t\t\tif path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {\n\t\t\t\t\tr.outf(\"%s\\n\", path)\n\t\t\t\t} else {\n\t\t\t\t\tanyNotFound = true\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif syntax.IsKeyword(arg) {\n\t\t\t\tif mode == \"-t\" {\n\t\t\t\t\tr.out(\"keyword\\n\")\n\t\t\t\t} else {\n\t\t\t\t\tr.outf(\"%s is a shell keyword\\n\", arg)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif als, ok := r.alias[arg]; ok && r.opts[optExpandAliases] {\n\t\t\t\tvar buf bytes.Buffer\n\t\t\t\tif len(als.args) > 0 {\n\t\t\t\t\tprinter := syntax.NewPrinter()\n\t\t\t\t\tprinter.Print(&buf, &syntax.CallExpr{\n\t\t\t\t\t\tArgs: als.args,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tif als.blank {\n\t\t\t\t\tbuf.WriteByte(' ')\n\t\t\t\t}\n\t\t\t\tif mode == \"-t\" {\n\t\t\t\t\tr.out(\"alias\\n\")\n\t\t\t\t} else {\n\t\t\t\t\tr.outf(\"%s is aliased to `%s'\\n\", arg, &buf)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := r.Funcs[arg]; ok {\n\t\t\t\tif mode == \"-t\" {\n\t\t\t\t\tr.out(\"function\\n\")\n\t\t\t\t} else {\n\t\t\t\t\tr.outf(\"%s is a function\\n\", arg)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif IsBuiltin(arg) {\n\t\t\t\tif mode == \"-t\" {\n\t\t\t\t\tr.out(\"builtin\\n\")\n\t\t\t\t} else {\n\t\t\t\t\tr.outf(\"%s is a shell builtin\\n\", arg)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {\n\t\t\t\tif mode == \"-t\" {\n\t\t\t\t\tr.out(\"file\\n\")\n\t\t\t\t} else {\n\t\t\t\t\tr.outf(\"%s is %s\\n\", arg, path)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif mode != \"-t\" {\n\t\t\t\tr.errf(\"type: %s: not found\\n\", arg)\n\t\t\t}\n\t\t\tanyNotFound = true\n\t\t}\n\t\tif anyNotFound {\n\t\t\texit.code = 1\n\t\t}\n\tcase \"hash\":\n\t\t// TODO: implement. for now, having this as a no-op is better than nothing.\n\tcase \"eval\":\n\t\tsrc := strings.Join(args, \" \")\n\t\tp := syntax.NewParser()\n\t\tfile, err := p.Parse(strings.NewReader(src), \"\")\n\t\tif err != nil {\n\t\t\treturn failf(1, \"eval: %v\\n\", err)\n\t\t}\n\t\tr.stmts(ctx, file.Stmts)\n\t\texit = r.exit\n\tcase \"source\", \".\":\n\t\tif len(args) < 1 {\n\t\t\treturn failf(2, \"%v: source: need filename\\n\", pos)\n\t\t}\n\t\tpath, err := scriptFromPathDir(r.Dir, r.writeEnv, args[0])\n\t\tif err != nil {\n\t\t\t// If the script was not found in PATH or there was any error, pass\n\t\t\t// the source path to the open handler so it has a chance to look\n\t\t\t// at files it manages (eg: virtual filesystem), and also allow\n\t\t\t// it to look for the sourced script in the current directory.\n\t\t\tpath = args[0]\n\t\t}\n\t\tf, err := r.open(ctx, path, os.O_RDONLY, 0, false)\n\t\tif err != nil {\n\t\t\treturn failf(1, \"source: %v\\n\", err)\n\t\t}\n\t\tdefer f.Close()\n\t\tp := syntax.NewParser()\n\t\tfile, err := p.Parse(f, path)\n\t\tif err != nil {\n\t\t\treturn failf(1, \"source: %v\\n\", err)\n\t\t}\n\n\t\t// Keep the current versions of some fields we might modify.\n\t\toldParams := r.Params\n\t\toldSourceSetParams := r.sourceSetParams\n\t\toldInSource := r.inSource\n\n\t\t// If we run \"source file args...\", set said args as parameters.\n\t\t// Otherwise, keep the current parameters.\n\t\tsourceArgs := len(args[1:]) > 0\n\t\tif sourceArgs {\n\t\t\tr.Params = args[1:]\n\t\t\tr.sourceSetParams = false\n\t\t}\n\t\t// We want to track if the sourced file explicitly sets the\n\t\t// parameters.\n\t\tr.sourceSetParams = false\n\t\tr.inSource = true // know that we're inside a sourced script.\n\t\tr.stmts(ctx, file.Stmts)\n\n\t\t// If we modified the parameters and the sourced file didn't\n\t\t// explicitly set them, we restore the old ones.\n\t\tif sourceArgs && !r.sourceSetParams {\n\t\t\tr.Params = oldParams\n\t\t}\n\t\tr.sourceSetParams = oldSourceSetParams\n\t\tr.inSource = oldInSource\n\n\t\texit = r.exit\n\t\texit.returning = false\n\tcase \"[\":\n\t\tif len(args) == 0 || args[len(args)-1] != \"]\" {\n\t\t\treturn failf(2, \"%v: [: missing matching ]\\n\", pos)\n\t\t}\n\t\targs = args[:len(args)-1]\n\t\tfallthrough\n\tcase \"test\":\n\t\tparseErr := false\n\t\tp := testParser{\n\t\t\trem: args,\n\t\t\terr: func(err error) {\n\t\t\t\tr.errf(\"%v: %v\\n\", pos, err)\n\t\t\t\tparseErr = true\n\t\t\t},\n\t\t}\n\t\tp.next()\n\t\texpr := p.classicTest(\"[\", false)\n\t\tif parseErr {\n\t\t\texit.code = 2\n\t\t\treturn exit\n\t\t}\n\t\texit.oneIf(r.bashTest(ctx, expr, true) == \"\")\n\tcase \"exec\":\n\t\t// TODO: Consider unix.Exec, i.e. actually replacing\n\t\t// the process. It's in theory what a shell should do,\n\t\t// but in practice it would kill the entire Go process\n\t\t// and it's not available on Windows.\n\t\tif len(args) == 0 {\n\t\t\tr.keepRedirs = true\n\t\t\tbreak\n\t\t}\n\t\tr.exit.exiting = true\n\t\tr.exec(ctx, pos, args)\n\t\texit = r.exit\n\tcase \"command\":\n\t\tshow := false\n\t\tfp := flagParser{remaining: args}\n\t\tfor fp.more() {\n\t\t\tswitch flag := fp.flag(); flag {\n\t\t\tcase \"-v\":\n\t\t\t\tshow = true\n\t\t\tdefault:\n\t\t\t\treturn failf(2, \"command: invalid option %q\\n\", flag)\n\t\t\t}\n\t\t}\n\t\targs := fp.args()\n\t\tif len(args) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tif !show {\n\t\t\tif IsBuiltin(args[0]) {\n\t\t\t\treturn r.builtin(ctx, pos, args[0], args[1:])\n\t\t\t}\n\t\t\tr.exec(ctx, pos, args)\n\t\t\texit = r.exit\n\t\t\treturn exit\n\t\t}\n\t\tlast := uint8(0)\n\t\tfor _, arg := range args {\n\t\t\tlast = 0\n\t\t\tif r.Funcs[arg] != nil || IsBuiltin(arg) {\n\t\t\t\tr.outf(\"%s\\n\", arg)\n\t\t\t} else if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil {\n\t\t\t\tr.outf(\"%s\\n\", path)\n\t\t\t} else {\n\t\t\t\tlast = 1\n\t\t\t}\n\t\t}\n\t\texit.code = last\n\tcase \"dirs\":\n\t\tfor i, dir := range slices.Backward(r.dirStack) {\n\t\t\tr.outf(\"%s\", dir)\n\t\t\tif i > 0 {\n\t\t\t\tr.out(\" \")\n\t\t\t}\n\t\t}\n\t\tr.out(\"\\n\")\n\tcase \"pushd\":\n\t\tchange := true\n\t\tif len(args) > 0 && args[0] == \"-n\" {\n\t\t\tchange = false\n\t\t\targs = args[1:]\n\t\t}\n\t\tswap := func() string {\n\t\t\toldtop := r.dirStack[len(r.dirStack)-1]\n\t\t\ttop := r.dirStack[len(r.dirStack)-2]\n\t\t\tr.dirStack[len(r.dirStack)-1] = top\n\t\t\tr.dirStack[len(r.dirStack)-2] = oldtop\n\t\t\treturn top\n\t\t}\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\t\tif !change {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif len(r.dirStack) < 2 {\n\t\t\t\treturn failf(1, \"pushd: no other directory\\n\")\n\t\t\t}\n\t\t\tnewtop := swap()\n\t\t\tif code := r.changeDir(ctx, \"pushd\", newtop); code != 0 {\n\t\t\t\texit.code = code\n\t\t\t\treturn exit\n\t\t\t}\n\t\t\tr.builtin(ctx, syntax.Pos{}, \"dirs\", nil)\n\t\tcase 1:\n\t\t\tif change {\n\t\t\t\tif code := r.changeDir(ctx, \"pushd\", args[0]); code != 0 {\n\t\t\t\t\texit.code = code\n\t\t\t\t\treturn exit\n\t\t\t\t}\n\t\t\t\tr.dirStack = append(r.dirStack, r.Dir)\n\t\t\t} else {\n\t\t\t\tr.dirStack = append(r.dirStack, args[0])\n\t\t\t\tswap()\n\t\t\t}\n\t\t\tr.builtin(ctx, syntax.Pos{}, \"dirs\", nil)\n\t\tdefault:\n\t\t\treturn failf(2, \"pushd: too many arguments\\n\")\n\t\t}\n\tcase \"popd\":\n\t\tchange := true\n\t\tif len(args) > 0 && args[0] == \"-n\" {\n\t\t\tchange = false\n\t\t\targs = args[1:]\n\t\t}\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\t\tif len(r.dirStack) < 2 {\n\t\t\t\treturn failf(1, \"popd: directory stack empty\\n\")\n\t\t\t}\n\t\t\toldtop := r.dirStack[len(r.dirStack)-1]\n\t\t\tr.dirStack = r.dirStack[:len(r.dirStack)-1]\n\t\t\tif change {\n\t\t\t\tnewtop := r.dirStack[len(r.dirStack)-1]\n\t\t\t\tif code := r.changeDir(ctx, \"popd\", newtop); code != 0 {\n\t\t\t\t\texit.code = code\n\t\t\t\t\treturn exit\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tr.dirStack[len(r.dirStack)-1] = oldtop\n\t\t\t}\n\t\t\tr.builtin(ctx, syntax.Pos{}, \"dirs\", nil)\n\t\tdefault:\n\t\t\treturn failf(2, \"popd: invalid argument\\n\")\n\t\t}\n\tcase \"return\":\n\t\tif !r.inFunc && !r.inSource {\n\t\t\treturn failf(1, \"return: can only be done from a func or sourced script\\n\")\n\t\t}\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\tcase 1:\n\t\t\tn, err := strconv.Atoi(args[0])\n\t\t\tif err != nil {\n\t\t\t\treturn failf(2, \"invalid return status code: %q\\n\", args[0])\n\t\t\t}\n\t\t\texit.code = uint8(n)\n\t\tdefault:\n\t\t\treturn failf(2, \"return: too many arguments\\n\")\n\t\t}\n\t\texit.returning = true\n\tcase \"read\":\n\t\tvar prompt string\n\t\traw := false\n\t\tsilent := false\n\t\treadArray := false\n\t\tfp := flagParser{remaining: args}\n\t\tfor fp.more() {\n\t\t\tswitch flag := fp.flag(); flag {\n\t\t\tcase \"-s\":\n\t\t\t\tsilent = true\n\t\t\tcase \"-r\":\n\t\t\t\traw = true\n\t\t\tcase \"-a\":\n\t\t\t\treadArray = true\n\t\t\tcase \"-p\":\n\t\t\t\tprompt = fp.value()\n\t\t\t\tif prompt == \"\" {\n\t\t\t\t\treturn failf(2, \"read: -p: option requires an argument\\n\")\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn failf(2, \"read: invalid option %q\\n\", flag)\n\t\t\t}\n\t\t}\n\n\t\targs := fp.args()\n\t\tfor _, name := range args {\n\t\t\tif !syntax.ValidName(name) {\n\t\t\t\treturn failf(2, \"read: invalid identifier %q\\n\", name)\n\t\t\t}\n\t\t}\n\n\t\tif prompt != \"\" {\n\t\t\tr.out(prompt)\n\t\t}\n\n\t\tvar line []byte\n\t\tvar err error\n\t\tif silent {\n\t\t\t// Note that on Windows, syscall.Stdin is of type uintptr.\n\t\t\tline, err = term.ReadPassword(int(syscall.Stdin))\n\t\t} else {\n\t\t\tline, err = r.readLine(ctx, raw)\n\t\t}\n\t\tif readArray {\n\t\t\t// read -a arrayname: split line into fields and assign to indexed array.\n\t\t\tarrayName := shellReplyVar\n\t\t\tif len(args) > 0 {\n\t\t\t\tarrayName = args[0]\n\t\t\t}\n\t\t\t// Use -1 as max to get all fields without joining the last ones.\n\t\t\tvalues := expand.ReadFields(r.ecfg, string(line), -1, raw)\n\t\t\tr.setVar(arrayName, expand.Variable{\n\t\t\t\tSet:  true,\n\t\t\t\tKind: expand.Indexed,\n\t\t\t\tList: values,\n\t\t\t})\n\t\t} else {\n\t\t\tif len(args) == 0 {\n\t\t\t\targs = append(args, shellReplyVar)\n\t\t\t}\n\n\t\t\tvalues := expand.ReadFields(r.ecfg, string(line), len(args), raw)\n\t\t\tfor i, name := range args {\n\t\t\t\tval := \"\"\n\t\t\t\tif i < len(values) {\n\t\t\t\t\tval = values[i]\n\t\t\t\t}\n\t\t\t\tr.setVarString(name, val)\n\t\t\t}\n\t\t}\n\n\t\t// We can get data back from readLine and an error at the same time, so\n\t\t// check err after we process the data.\n\t\tif err != nil {\n\t\t\texit.code = 1\n\t\t\treturn exit\n\t\t}\n\n\tcase \"getopts\":\n\t\tif len(args) < 2 {\n\t\t\treturn failf(2, \"getopts: usage: getopts optstring name [arg ...]\\n\")\n\t\t}\n\t\toptind, _ := strconv.Atoi(r.envGet(\"OPTIND\"))\n\t\tif optind-1 != r.optState.argidx {\n\t\t\tif optind < 1 {\n\t\t\t\toptind = 1\n\t\t\t}\n\t\t\tr.optState = getopts{argidx: optind - 1}\n\t\t}\n\t\toptstr := args[0]\n\t\tname := args[1]\n\t\tif !syntax.ValidName(name) {\n\t\t\treturn failf(2, \"getopts: invalid identifier: %q\\n\", name)\n\t\t}\n\t\targs = args[2:]\n\t\tif len(args) == 0 {\n\t\t\targs = r.Params\n\t\t}\n\t\tdiagnostics := !strings.HasPrefix(optstr, \":\")\n\n\t\topt, optarg, done := r.optState.next(optstr, args)\n\n\t\tr.setVarString(name, string(opt))\n\t\tr.delVar(\"OPTARG\")\n\t\tswitch {\n\t\tcase opt == '?' && diagnostics && !done:\n\t\t\tr.errf(\"getopts: illegal option -- %q\\n\", optarg)\n\t\tcase opt == ':' && diagnostics:\n\t\t\tr.errf(\"getopts: option requires an argument -- %q\\n\", optarg)\n\t\tdefault:\n\t\t\tif optarg != \"\" {\n\t\t\t\tr.setVarString(\"OPTARG\", optarg)\n\t\t\t}\n\t\t}\n\t\tif optind-1 != r.optState.argidx {\n\t\t\tr.setVarString(\"OPTIND\", strconv.FormatInt(int64(r.optState.argidx+1), 10))\n\t\t}\n\n\t\texit.oneIf(done)\n\n\tcase \"shopt\":\n\t\tmode := \"\"\n\t\tposixOpts := false\n\t\tfp := flagParser{remaining: args}\n\t\tfor fp.more() {\n\t\t\tswitch flag := fp.flag(); flag {\n\t\t\tcase \"-s\", \"-u\":\n\t\t\t\tmode = flag\n\t\t\tcase \"-o\":\n\t\t\t\tposixOpts = true\n\t\t\tcase \"-p\", \"-q\":\n\t\t\t\tpanic(fmt.Sprintf(\"unhandled shopt flag: %s\", flag))\n\t\t\tdefault:\n\t\t\t\treturn failf(2, \"shopt: invalid option %q\\n\", flag)\n\t\t\t}\n\t\t}\n\t\targs := fp.args()\n\t\tif len(args) == 0 {\n\t\t\tif posixOpts {\n\t\t\t\tfor i, opt := range &posixOptsTable {\n\t\t\t\t\tr.printOptLine(opt.name, r.opts[i], true)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, opt := range bashOptsTable {\n\t\t\t\t\tr.printOptLine(opt.name, r.opts[len(posixOptsTable)+i], opt.supported)\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tfor _, arg := range args {\n\t\t\topt, supported := (*bool)(nil), true\n\t\t\tif posixOpts {\n\t\t\t\topt = r.posixOptByName(arg)\n\t\t\t} else {\n\t\t\t\topt, supported = r.bashOptByName(arg)\n\t\t\t}\n\t\t\tif opt == nil {\n\t\t\t\treturn failf(1, \"shopt: invalid option name %q\\n\", arg)\n\t\t\t}\n\n\t\t\tswitch mode {\n\t\t\tcase \"-s\", \"-u\":\n\t\t\t\tif !supported {\n\t\t\t\t\treturn failf(1, \"shopt: unsupported option %q\\n\", arg)\n\t\t\t\t}\n\t\t\t\t*opt = mode == \"-s\"\n\t\t\tdefault: // \"\"\n\t\t\t\tr.printOptLine(arg, *opt, supported)\n\t\t\t}\n\t\t}\n\t\tr.updateExpandOpts()\n\n\tcase \"alias\":\n\t\tshow := func(name string, als alias) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tif len(als.args) > 0 {\n\t\t\t\tprinter := syntax.NewPrinter()\n\t\t\t\tprinter.Print(&buf, &syntax.CallExpr{\n\t\t\t\t\tArgs: als.args,\n\t\t\t\t})\n\t\t\t}\n\t\t\tif als.blank {\n\t\t\t\tbuf.WriteByte(' ')\n\t\t\t}\n\t\t\tr.outf(\"alias %s='%s'\\n\", name, &buf)\n\t\t}\n\n\t\tif len(args) == 0 {\n\t\t\tfor name, als := range r.alias {\n\t\t\t\tshow(name, als)\n\t\t\t}\n\t\t}\n\targsLoop:\n\t\tfor _, arg := range args {\n\t\t\tname, src, ok := strings.Cut(arg, \"=\")\n\t\t\tif !ok {\n\t\t\t\tals, ok := r.alias[name]\n\t\t\t\tif !ok {\n\t\t\t\t\tr.errf(\"alias: %q not found\\n\", name)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tshow(name, als)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// TODO: parse any CallExpr perhaps, or even any Stmt\n\t\t\tparser := syntax.NewParser()\n\t\t\tvar words []*syntax.Word\n\t\t\tfor w, err := range parser.WordsSeq(strings.NewReader(src)) {\n\t\t\t\tif err != nil {\n\t\t\t\t\tr.errf(\"alias: could not parse %q: %v\\n\", src, err)\n\t\t\t\t\tcontinue argsLoop\n\t\t\t\t}\n\t\t\t\twords = append(words, w)\n\t\t\t}\n\n\t\t\tif r.alias == nil {\n\t\t\t\tr.alias = make(map[string]alias)\n\t\t\t}\n\t\t\tr.alias[name] = alias{\n\t\t\t\targs:  words,\n\t\t\t\tblank: strings.TrimRight(src, \" \\t\") != src,\n\t\t\t}\n\t\t}\n\tcase \"unalias\":\n\t\tfor _, name := range args {\n\t\t\tdelete(r.alias, name)\n\t\t}\n\n\tcase \"trap\":\n\t\tfp := flagParser{remaining: args}\n\t\tcallback := \"-\"\n\t\tfor fp.more() {\n\t\t\tswitch flag := fp.flag(); flag {\n\t\t\tcase \"-l\", \"-p\":\n\t\t\t\treturn failf(2, \"trap: %q: NOT IMPLEMENTED flag\\n\", flag)\n\t\t\tcase \"-\":\n\t\t\t\t// default signal\n\t\t\tdefault:\n\t\t\t\tr.errf(\"trap: %q: invalid option\\n\", flag)\n\t\t\t\tr.errf(\"trap: usage: trap [-lp] [[arg] signal_spec ...]\\n\")\n\t\t\t\texit.code = 2\n\t\t\t\treturn exit\n\t\t\t}\n\t\t}\n\t\targs := fp.args()\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\t\t// Print non-default signals\n\t\t\tif r.callbackExit != \"\" {\n\t\t\t\tr.outf(\"trap -- %q EXIT\\n\", r.callbackExit)\n\t\t\t}\n\t\t\tif r.callbackErr != \"\" {\n\t\t\t\tr.outf(\"trap -- %q ERR\\n\", r.callbackErr)\n\t\t\t}\n\t\tcase 1:\n\t\t\t// assume it's a signal, the default will be restored\n\t\tdefault:\n\t\t\tcallback = args[0]\n\t\t\targs = args[1:]\n\t\t}\n\t\t// For now, treat both empty and - the same since ERR and EXIT have no\n\t\t// default callback.\n\t\tif callback == \"-\" {\n\t\t\tcallback = \"\"\n\t\t}\n\t\tfor _, arg := range args {\n\t\t\tswitch arg {\n\t\t\tcase \"ERR\":\n\t\t\t\tr.callbackErr = callback\n\t\t\tcase \"EXIT\":\n\t\t\t\tr.callbackExit = callback\n\t\t\tdefault:\n\t\t\t\treturn failf(2, \"trap: %s: invalid signal specification\\n\", arg)\n\t\t\t}\n\t\t}\n\n\tcase \"readarray\", \"mapfile\":\n\t\tdropDelim := false\n\t\tdelim := \"\\n\"\n\t\tfp := flagParser{remaining: args}\n\t\tfor fp.more() {\n\t\t\tswitch flag := fp.flag(); flag {\n\t\t\tcase \"-t\":\n\t\t\t\t// Remove the delim from each line read\n\t\t\t\tdropDelim = true\n\t\t\tcase \"-d\":\n\t\t\t\tif len(fp.remaining) == 0 {\n\t\t\t\t\treturn failf(2, \"%s: -d: option requires an argument\\n\", name)\n\t\t\t\t}\n\t\t\t\tdelim = fp.value()\n\t\t\t\tif delim == \"\" {\n\t\t\t\t\t// Bash sets the delim to an ASCII NUL if provided with an empty\n\t\t\t\t\t// string.\n\t\t\t\t\tdelim = \"\\x00\"\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn failf(2, \"%s: invalid option %q\\n\", name, flag)\n\t\t\t}\n\t\t}\n\n\t\targs := fp.args()\n\t\tvar arrayName string\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\t\tarrayName = \"MAPFILE\"\n\t\tcase 1:\n\t\t\tif !syntax.ValidName(args[0]) {\n\t\t\t\treturn failf(2, \"%s: invalid identifier %q\\n\", name, args[0])\n\t\t\t}\n\t\t\tarrayName = args[0]\n\t\tdefault:\n\t\t\treturn failf(2, \"%s: Only one array name may be specified, %v\\n\", name, args)\n\t\t}\n\n\t\tvar vr expand.Variable\n\t\tvr.Kind = expand.Indexed\n\t\tscanner := bufio.NewScanner(r.stdin)\n\t\tscanner.Split(mapfileSplit(delim[0], dropDelim))\n\t\tfor scanner.Scan() {\n\t\t\tvr.List = append(vr.List, scanner.Text())\n\t\t}\n\t\tif err := scanner.Err(); err != nil {\n\t\t\treturn failf(2, \"%s: unable to read, %v\\n\", name, err)\n\t\t}\n\t\tr.setVar(arrayName, vr)\n\n\tdefault:\n\t\treturn failf(2, \"%s: unimplemented builtin\\n\", name)\n\t}\n\treturn exit\n}\n\n// mapfileSplit returns a suitable Split function for a [bufio.Scanner];\n// the code is mostly stolen from [bufio.ScanLines].\nfunc mapfileSplit(delim byte, dropDelim bool) bufio.SplitFunc {\n\treturn func(data []byte, atEOF bool) (advance int, token []byte, err error) {\n\t\tif atEOF && len(data) == 0 {\n\t\t\treturn 0, nil, nil\n\t\t}\n\t\tif i := bytes.IndexByte(data, delim); i >= 0 {\n\t\t\t// We have a full newline-terminated line.\n\t\t\tif dropDelim {\n\t\t\t\treturn i + 1, data[0:i], nil\n\t\t\t} else {\n\t\t\t\treturn i + 1, data[0 : i+1], nil\n\t\t\t}\n\t\t}\n\t\t// If we're at EOF, we have a final, non-terminated line. Return it.\n\t\tif atEOF {\n\t\t\treturn len(data), data, nil\n\t\t}\n\t\t// Request more data.\n\t\treturn 0, nil, nil\n\t}\n}\n\nfunc (r *Runner) printOptLine(name string, enabled, supported bool) {\n\tstate := r.optStatusText(enabled)\n\tif supported {\n\t\tr.outf(\"%s\\t%s\\n\", name, state)\n\t\treturn\n\t}\n\tr.outf(\"%s\\t%s\\t(%q not supported)\\n\", name, state, r.optStatusText(!enabled))\n}\n\nfunc (r *Runner) readLine(ctx context.Context, raw bool) ([]byte, error) {\n\tif r.stdin == nil {\n\t\treturn nil, errors.New(\"interp: can't read, there's no stdin\")\n\t}\n\n\tvar line []byte\n\tesc := false\n\n\tstopc := make(chan struct{})\n\tstop := context.AfterFunc(ctx, func() {\n\t\tr.stdin.SetReadDeadline(time.Now())\n\t\tclose(stopc)\n\t})\n\tdefer func() {\n\t\tif !stop() {\n\t\t\t// The AfterFunc was started.\n\t\t\t// Wait for it to complete, and reset the file's deadline.\n\t\t\t<-stopc\n\t\t\tr.stdin.SetReadDeadline(time.Time{})\n\t\t}\n\t}()\n\tfor {\n\t\tvar buf [1]byte\n\t\tn, err := r.stdin.Read(buf[:])\n\t\tif n > 0 {\n\t\t\tb := buf[0]\n\t\t\tswitch {\n\t\t\tcase !raw && b == '\\\\':\n\t\t\t\tline = append(line, b)\n\t\t\t\tesc = !esc\n\t\t\tcase !raw && b == '\\n' && esc:\n\t\t\t\t// line continuation\n\t\t\t\tline = line[len(line)-1:]\n\t\t\t\tesc = false\n\t\t\tcase b == '\\n':\n\t\t\t\treturn line, nil\n\t\t\tdefault:\n\t\t\t\tline = append(line, b)\n\t\t\t\tesc = false\n\t\t\t}\n\t\t}\n\t\tif err != nil {\n\t\t\treturn line, err\n\t\t}\n\t}\n}\n\nfunc (r *Runner) changeDir(ctx context.Context, cmd, path string) uint8 {\n\tpath = cmp.Or(path, \".\")\n\tapath := r.absPath(path)\n\tinfo, err := r.stat(ctx, apath)\n\tif err != nil || !info.IsDir() {\n\t\tr.errf(\"%s: no such file or directory: %q\\n\", cmd, path)\n\t\treturn 1\n\t}\n\tif r.access(ctx, apath, access_X_OK) != nil {\n\t\tr.errf(\"%s: permission denied: %q\\n\", cmd, path)\n\t\treturn 1\n\t}\n\tr.Dir = apath\n\tr.setVarString(\"OLDPWD\", r.envGet(\"PWD\"))\n\tr.setVarString(\"PWD\", apath)\n\treturn 0\n}\n\nfunc absPath(dir, path string) string {\n\tif path == \"\" {\n\t\treturn \"\"\n\t}\n\tif !filepath.IsAbs(path) {\n\t\tpath = filepath.Join(dir, path)\n\t}\n\treturn filepath.Clean(path) // TODO: this clean is likely unnecessary\n}\n\nfunc (r *Runner) absPath(path string) string {\n\treturn absPath(r.Dir, path)\n}\n\n// flagParser is used to parse builtin flags.\n//\n// It's similar to the getopts implementation, but with some key differences.\n// First, the API is designed for Go loops, making it easier to use directly.\n// Second, it doesn't require the awkward \":ab\" syntax that getopts uses.\n// Third, it supports \"-a\" flags as well as \"+a\".\ntype flagParser struct {\n\tcurrent   string\n\tremaining []string\n}\n\nfunc (p *flagParser) more() bool {\n\tif p.current != \"\" {\n\t\t// We're still parsing part of \"-ab\".\n\t\treturn true\n\t}\n\tif len(p.remaining) == 0 {\n\t\t// Nothing left.\n\t\tp.remaining = nil\n\t\treturn false\n\t}\n\targ := p.remaining[0]\n\tif arg == \"--\" {\n\t\t// We explicitly stop parsing flags.\n\t\tp.remaining = p.remaining[1:]\n\t\treturn false\n\t}\n\tif len(arg) == 0 || (arg[0] != '-' && arg[0] != '+') {\n\t\t// The next argument is not a flag.\n\t\treturn false\n\t}\n\t// More flags to come.\n\treturn true\n}\n\nfunc (p *flagParser) flag() string {\n\targ := p.current\n\tif arg == \"\" {\n\t\targ = p.remaining[0]\n\t\tp.remaining = p.remaining[1:]\n\t} else {\n\t\tp.current = \"\"\n\t}\n\tif len(arg) > 2 {\n\t\t// We have \"-ab\", so return \"-a\" and keep \"-b\".\n\t\tp.current = arg[:1] + arg[2:]\n\t\targ = arg[:2]\n\t}\n\treturn arg\n}\n\nfunc (p *flagParser) value() string {\n\tif len(p.remaining) == 0 {\n\t\treturn \"\"\n\t}\n\targ := p.remaining[0]\n\tp.remaining = p.remaining[1:]\n\treturn arg\n}\n\nfunc (p *flagParser) args() []string { return p.remaining }\n\ntype getopts struct {\n\targidx  int\n\truneidx int\n}\n\nfunc (g *getopts) next(optstr string, args []string) (opt rune, optarg string, done bool) {\n\tif len(args) == 0 || g.argidx >= len(args) {\n\t\treturn '?', \"\", true\n\t}\n\targ := []rune(args[g.argidx])\n\tif len(arg) < 2 || arg[0] != '-' || arg[1] == '-' {\n\t\treturn '?', \"\", true\n\t}\n\n\topts := arg[1:]\n\topt = opts[g.runeidx]\n\tif g.runeidx+1 < len(opts) {\n\t\tg.runeidx++\n\t} else {\n\t\tg.argidx++\n\t\tg.runeidx = 0\n\t}\n\n\ti := strings.IndexRune(optstr, opt)\n\tif i < 0 {\n\t\t// invalid option\n\t\treturn '?', string(opt), false\n\t}\n\n\tif i+1 < len(optstr) && optstr[i+1] == ':' {\n\t\tif g.argidx >= len(args) {\n\t\t\t// missing argument\n\t\t\treturn ':', string(opt), false\n\t\t}\n\t\toptarg = args[g.argidx]\n\t\tg.argidx++\n\t\tg.runeidx = 0\n\t}\n\n\treturn opt, optarg, false\n}\n\n// optStatusText returns a shell option's status text display\nfunc (r *Runner) optStatusText(status bool) string {\n\tif status {\n\t\treturn \"on\"\n\t}\n\treturn \"off\"\n}\n"
  },
  {
    "path": "interp/example_test.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/interp\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc Example() {\n\tsrc := `\n\t\tfoo=abc\n\t\tfor i in 1 2 3; do\n\t\t\tfoo+=$i\n\t\tdone\n\t\tlet bar=(2 + 3)\n\t\techo $foo $bar\n\t\techo $GLOBAL\n\t`\n\tfile, _ := syntax.NewParser().Parse(strings.NewReader(src), \"\")\n\trunner, _ := interp.New(\n\t\t// Use [interp.Interactive] to enable interactive shell defaults like expanding aliases.\n\t\tinterp.Env(expand.ListEnviron(\"GLOBAL=global_value\")),\n\t\tinterp.StdIO(nil, os.Stdout, os.Stdout),\n\t)\n\trunner.Run(context.TODO(), file)\n\t// Output:\n\t// abc123 5\n\t// global_value\n}\n\nfunc ExampleExecHandlers() {\n\tsrc := \"echo foo; join ! foo bar baz; missing-program bar\"\n\tfile, _ := syntax.NewParser().Parse(strings.NewReader(src), \"\")\n\n\texecJoin := func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\t\treturn func(ctx context.Context, args []string) error {\n\t\t\thc := interp.HandlerCtx(ctx)\n\t\t\tif args[0] == \"join\" {\n\t\t\t\tfmt.Fprintln(hc.Stdout, strings.Join(args[2:], args[1]))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn next(ctx, args)\n\t\t}\n\t}\n\texecNotInstalled := func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\t\treturn func(ctx context.Context, args []string) error {\n\t\t\thc := interp.HandlerCtx(ctx)\n\t\t\tif _, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]); err != nil {\n\t\t\t\tfmt.Printf(\"%s is not installed\\n\", args[0])\n\t\t\t\treturn interp.ExitStatus(1)\n\t\t\t}\n\t\t\treturn next(ctx, args)\n\t\t}\n\t}\n\trunner, _ := interp.New(\n\t\tinterp.StdIO(nil, os.Stdout, os.Stdout),\n\t\tinterp.ExecHandlers(execJoin, execNotInstalled),\n\t)\n\trunner.Run(context.TODO(), file)\n\t// Output:\n\t// foo\n\t// foo!bar!baz\n\t// missing-program is not installed\n}\n\ntype nopWriterCloser struct {\n\t*strings.Reader\n}\n\nfunc (nopWriterCloser) Write([]byte) (int, error) { return 0, io.EOF }\nfunc (nopWriterCloser) Close() error              { return nil }\n\nfunc ExampleOpenHandler() {\n\tsrc := \"echo $(</etc/hostname); echo bar >/dev/null\"\n\tfile, _ := syntax.NewParser().Parse(strings.NewReader(src), \"\")\n\n\topen := func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {\n\t\t// Hard-code the contents of the hostname file for all platforms.\n\t\tif path == \"/etc/hostname\" {\n\t\t\treturn nopWriterCloser{strings.NewReader(\"mymachine\")}, nil\n\t\t}\n\t\t// DefaultOpenHandler already redirects /dev/null to NUL on Windows.\n\t\treturn interp.DefaultOpenHandler()(ctx, path, flag, perm)\n\t}\n\trunner, _ := interp.New(\n\t\tinterp.StdIO(nil, os.Stdout, os.Stdout),\n\t\tinterp.OpenHandler(open),\n\t)\n\trunner.Run(context.TODO(), file)\n\t// Output:\n\t// mymachine\n}\n"
  },
  {
    "path": "interp/handler.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// HandlerCtx returns the [HandlerContext] value stored in ctx,\n// which is used when calling handler functions.\n// It panics if ctx has no HandlerContext stored.\nfunc HandlerCtx(ctx context.Context) HandlerContext {\n\thc, ok := ctx.Value(handlerCtxKey{}).(HandlerContext)\n\tif !ok {\n\t\tpanic(\"interp.HandlerCtx: no HandlerContext in ctx\")\n\t}\n\treturn hc\n}\n\ntype handlerCtxKey struct{}\n\ntype handlerKind int\n\nconst (\n\t_                  handlerKind = iota\n\thandlerKindExec                // [ExecHandlerFunc]\n\thandlerKindCall                // [CallHandlerFunc]\n\thandlerKindOpen                // [OpenHandlerFunc]\n\thandlerKindReadDir             // [ReadDirHandlerFunc2]\n)\n\n// HandlerContext is the data passed to all the handler functions via [context.WithValue].\n// It contains some of the current state of the [Runner].\ntype HandlerContext struct {\n\trunner *Runner // for internal use only, e.g. [HandlerContext.Builtin]\n\n\t// kind records which type of handler this context was built for.\n\tkind handlerKind\n\n\t// Env is a read-only version of the interpreter's environment,\n\t// including environment variables, global variables, and local function\n\t// variables.\n\tEnv expand.Environ\n\n\t// Dir is the interpreter's current directory.\n\tDir string\n\n\t// Pos is the source position which relates to the operation,\n\t// such as a [syntax.CallExpr] when calling an [ExecHandlerFunc].\n\t// It may be invalid if the operation has no relevant position information.\n\tPos syntax.Pos\n\n\t// TODO(v4): use an os.File for stdin below directly.\n\n\t// Stdin is the interpreter's current standard input reader.\n\t// It is always an [*os.File], but the type here remains an [io.Reader]\n\t// due to backwards compatibility.\n\tStdin io.Reader\n\t// Stdout is the interpreter's current standard output writer.\n\tStdout io.Writer\n\t// Stderr is the interpreter's current standard error writer.\n\tStderr io.Writer\n}\n\n// CallHandlerFunc is a handler which runs on every [syntax.CallExpr].\n// It is called once variable assignments and field expansion have occurred.\n// The context includes a [HandlerContext] value.\n//\n// The call's arguments are replaced by what the handler returns,\n// and then the call is executed by the Runner as usual.\n// The args slice is never empty.\n// At this time, returning an empty slice without an error is not supported.\n//\n// This handler is similar to [ExecHandlerFunc], but has two major differences:\n//\n// First, it runs for all simple commands, including function calls and builtins.\n//\n// Second, it is not expected to execute the simple command, but instead to\n// allow running custom code which allows replacing the argument list.\n// Shell builtins touch on many internals of the Runner, after all.\n//\n// Returning a non-nil error will halt the [Runner] and will be returned via the API.\ntype CallHandlerFunc func(ctx context.Context, args []string) ([]string, error)\n\n// TODO: consistently treat handler errors as non-fatal by default,\n// but have an interface or API to specify fatal errors which should make\n// the shell exit with a particular status code.\n\n// ExecHandlerFunc is a handler which executes simple commands.\n// It is called for all [syntax.CallExpr] nodes\n// where the first argument is neither a declared function nor a builtin.\n// The args slice is never empty.\n// The context includes a [HandlerContext] value.\n//\n// Returning a nil error means a zero exit status.\n// Other exit statuses can be set by returning or wrapping a [NewExitStatus] error,\n// and such an error is returned via the API if it is the last statement executed.\n// Any other error will halt the [Runner] and will be returned via the API.\ntype ExecHandlerFunc func(ctx context.Context, args []string) error\n\n// DefaultExecHandler returns the [ExecHandlerFunc] used by default.\n// It finds binaries in PATH and executes them.\n// When context is cancelled, an interrupt signal is sent to running processes.\n// killTimeout is a duration to wait before sending the kill signal.\n// A negative value means that a kill signal will be sent immediately.\n//\n// On Windows, the kill signal is always sent immediately,\n// because Go doesn't currently support sending Interrupt on Windows.\n// [Runner] defaults to a killTimeout of 2 seconds.\nfunc DefaultExecHandler(killTimeout time.Duration) ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\thc := HandlerCtx(ctx)\n\t\tpath, err := LookPathDir(hc.Dir, hc.Env, args[0])\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(hc.Stderr, err)\n\t\t\treturn ExitStatus(127)\n\t\t}\n\t\tcmd := exec.Cmd{\n\t\t\tPath:   path,\n\t\t\tArgs:   args,\n\t\t\tEnv:    execEnv(hc.Env),\n\t\t\tDir:    hc.Dir,\n\t\t\tStdin:  hc.Stdin,\n\t\t\tStdout: hc.Stdout,\n\t\t\tStderr: hc.Stderr,\n\t\t}\n\n\t\terr = cmd.Start()\n\t\tif err == nil {\n\t\t\tstopf := context.AfterFunc(ctx, func() {\n\t\t\t\tif killTimeout <= 0 || runtime.GOOS == \"windows\" {\n\t\t\t\t\t_ = cmd.Process.Signal(os.Kill)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t_ = cmd.Process.Signal(os.Interrupt)\n\t\t\t\t// TODO: don't sleep in this goroutine if the program\n\t\t\t\t// stops itself with the interrupt above.\n\t\t\t\ttime.Sleep(killTimeout)\n\t\t\t\t_ = cmd.Process.Signal(os.Kill)\n\t\t\t})\n\t\t\tdefer stopf()\n\n\t\t\terr = cmd.Wait()\n\t\t}\n\n\t\tswitch err := err.(type) {\n\t\tcase *exec.ExitError:\n\t\t\t// Windows and Plan9 do not have support for [syscall.WaitStatus]\n\t\t\t// with methods like Signaled and Signal, so for those, [waitStatus] is a no-op.\n\t\t\t// Note: [waitStatus] is an alias [syscall.WaitStatus]\n\t\t\tif status, ok := err.Sys().(waitStatus); ok && status.Signaled() {\n\t\t\t\tif ctx.Err() != nil {\n\t\t\t\t\treturn ctx.Err()\n\t\t\t\t}\n\t\t\t\treturn ExitStatus(128 + status.Signal())\n\t\t\t}\n\t\t\treturn ExitStatus(err.ExitCode())\n\t\tcase *exec.Error:\n\t\t\t// did not start\n\t\t\tfmt.Fprintf(hc.Stderr, \"%v\\n\", err)\n\t\t\treturn ExitStatus(127)\n\t\tdefault:\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc checkStat(dir, file string, checkExec bool) (string, error) {\n\tif !filepath.IsAbs(file) {\n\t\tfile = filepath.Join(dir, file)\n\t}\n\tinfo, err := os.Stat(file)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tm := info.Mode()\n\tif m.IsDir() {\n\t\treturn \"\", fmt.Errorf(\"is a directory\")\n\t}\n\tif checkExec && runtime.GOOS != \"windows\" && m&0o111 == 0 {\n\t\treturn \"\", fmt.Errorf(\"permission denied\")\n\t}\n\treturn file, nil\n}\n\nfunc winHasExt(file string) bool {\n\ti := strings.LastIndex(file, \".\")\n\tif i < 0 {\n\t\treturn false\n\t}\n\treturn strings.LastIndexAny(file, `:\\/`) < i\n}\n\n// findExecutable returns the path to an existing executable file.\nfunc findExecutable(dir, file string, exts []string) (string, error) {\n\tif len(exts) == 0 {\n\t\t// non-windows\n\t\treturn checkStat(dir, file, true)\n\t}\n\tif winHasExt(file) {\n\t\tif file, err := checkStat(dir, file, true); err == nil {\n\t\t\treturn file, nil\n\t\t}\n\t}\n\tfor _, e := range exts {\n\t\tf := file + e\n\t\tif f, err := checkStat(dir, f, true); err == nil {\n\t\t\treturn f, nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"not found\")\n}\n\n// findFile returns the path to an existing file.\nfunc findFile(dir, file string, _ []string) (string, error) {\n\treturn checkStat(dir, file, false)\n}\n\n// LookPath is deprecated; see [LookPathDir].\nfunc LookPath(env expand.Environ, file string) (string, error) {\n\treturn LookPathDir(env.Get(\"PWD\").String(), env, file)\n}\n\n// LookPathDir is similar to [os/exec.LookPath], with the difference that it uses the\n// provided environment. env is used to fetch relevant environment variables\n// such as PWD and PATH.\n//\n// If no error is returned, the returned path must be valid.\nfunc LookPathDir(cwd string, env expand.Environ, file string) (string, error) {\n\treturn lookPathDir(cwd, env, file, findExecutable)\n}\n\n// findAny defines a function to pass to [lookPathDir].\ntype findAny = func(dir string, file string, exts []string) (string, error)\n\nfunc lookPathDir(cwd string, env expand.Environ, file string, find findAny) (string, error) {\n\tif find == nil {\n\t\tpanic(\"no find function found\")\n\t}\n\n\tpathList := filepath.SplitList(env.Get(\"PATH\").String())\n\tif len(pathList) == 0 {\n\t\tpathList = []string{\"\"}\n\t}\n\tchars := `/`\n\tif runtime.GOOS == \"windows\" {\n\t\tchars = `:\\/`\n\t}\n\texts := pathExts(env)\n\tif strings.ContainsAny(file, chars) {\n\t\treturn find(cwd, file, exts)\n\t}\n\tfor _, elem := range pathList {\n\t\tvar path string\n\t\tswitch elem {\n\t\tcase \"\", \".\":\n\t\t\t// otherwise \"foo\" won't be \"./foo\"\n\t\t\tpath = \".\" + string(filepath.Separator) + file\n\t\tdefault:\n\t\t\tpath = filepath.Join(elem, file)\n\t\t}\n\t\tif f, err := find(cwd, path, exts); err == nil {\n\t\t\treturn f, nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"%q: executable file not found in $PATH\", file)\n}\n\n// scriptFromPathDir is similar to [LookPathDir], with the difference that it looks\n// for both executable and non-executable files.\nfunc scriptFromPathDir(cwd string, env expand.Environ, file string) (string, error) {\n\treturn lookPathDir(cwd, env, file, findFile)\n}\n\nfunc pathExts(env expand.Environ) []string {\n\tif runtime.GOOS != \"windows\" {\n\t\treturn nil\n\t}\n\tpathext := env.Get(\"PATHEXT\").String()\n\tif pathext == \"\" {\n\t\treturn []string{\".com\", \".exe\", \".bat\", \".cmd\"}\n\t}\n\tvar exts []string\n\tfor e := range strings.SplitSeq(strings.ToLower(pathext), `;`) {\n\t\tif e == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif e[0] != '.' {\n\t\t\te = \".\" + e\n\t\t}\n\t\texts = append(exts, e)\n\t}\n\treturn exts\n}\n\n// OpenHandlerFunc is a handler which opens files.\n// It is called for all files that are opened directly by the shell,\n// such as in redirects, except for named pipes created by process substitutions.\n// The context includes a [HandlerContext] value.\n// Files opened by executed programs are not included.\n//\n// The path parameter may be relative to the current directory,\n// which can be fetched via [HandlerCtx].\n//\n// Use a return error of type [*os.PathError] to have the error printed to\n// stderr and the exit status set to 1.\n// Any other error will halt the [Runner] and will be returned via the API.\n//\n// Note that implementations which do not return [os.File] will cause\n// extra files and goroutines for input redirections; see [StdIO].\ntype OpenHandlerFunc func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error)\n\n// TODO: paths passed to [OpenHandlerFunc] should be cleaned.\n\n// DefaultOpenHandler returns the [OpenHandlerFunc] used by default.\n// It uses [os.OpenFile] to open files.\n//\n// For the sake of portability, /dev/null opens NUL on Windows.\nfunc DefaultOpenHandler() OpenHandlerFunc {\n\treturn func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {\n\t\tmc := HandlerCtx(ctx)\n\t\tif runtime.GOOS == \"windows\" && path == \"/dev/null\" {\n\t\t\tpath = \"NUL\"\n\t\t\t// Note that even though https://go.dev/issue/71752 was resolved for Windows,\n\t\t\t// the workaround here seems to still be required for Wine as of 10.14.\n\t\t\t// TODO(mvdan): Why? Is this Wine's fault?\n\t\t\tflag &^= os.O_TRUNC\n\t\t} else if path != \"\" && !filepath.IsAbs(path) {\n\t\t\tpath = filepath.Join(mc.Dir, path)\n\t\t}\n\t\treturn os.OpenFile(path, flag, perm)\n\t}\n}\n\n// TODO(v4): if this is kept in v4, it most likely needs to use [io/fs.DirEntry] for efficiency\n\n// ReadDirHandlerFunc is a handler which reads directories. It is called during\n// shell globbing, if enabled.\n//\n// Deprecated: use [ReadDirHandlerFunc2], which uses [fs.DirEntry].\ntype ReadDirHandlerFunc func(ctx context.Context, path string) ([]fs.FileInfo, error)\n\n// ReadDirHandlerFunc2 is a handler which reads directories. It is called during\n// shell globbing, if enabled.\n// The context includes a [HandlerContext] value.\ntype ReadDirHandlerFunc2 func(ctx context.Context, path string) ([]fs.DirEntry, error)\n\n// DefaultReadDirHandler returns the [ReadDirHandlerFunc] used by default.\n// It makes use of [ioutil.ReadDir].\nfunc DefaultReadDirHandler() ReadDirHandlerFunc {\n\treturn func(ctx context.Context, path string) ([]fs.FileInfo, error) {\n\t\treturn ioutil.ReadDir(path)\n\t}\n}\n\n// DefaultReadDirHandler2 returns the [ReadDirHandlerFunc2] used by default.\n// It uses [os.ReadDir].\nfunc DefaultReadDirHandler2() ReadDirHandlerFunc2 {\n\treturn func(ctx context.Context, path string) ([]fs.DirEntry, error) {\n\t\treturn os.ReadDir(path)\n\t}\n}\n\n// StatHandlerFunc is a handler which gets a file's information.\n// The context includes a [HandlerContext] value.\ntype StatHandlerFunc func(ctx context.Context, name string, followSymlinks bool) (fs.FileInfo, error)\n\n// DefaultStatHandler returns the [StatHandlerFunc] used by default.\n// It makes use of [os.Stat] and [os.Lstat], depending on followSymlinks.\nfunc DefaultStatHandler() StatHandlerFunc {\n\treturn func(ctx context.Context, path string, followSymlinks bool) (fs.FileInfo, error) {\n\t\tif !followSymlinks {\n\t\t\treturn os.Lstat(path)\n\t\t} else {\n\t\t\treturn os.Stat(path)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "interp/handler_test.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp_test\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"mvdan.cc/sh/v3/interp\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc blocklistOneExec(name string) func(interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\t\treturn func(ctx context.Context, args []string) error {\n\t\t\tif args[0] == name {\n\t\t\t\treturn fmt.Errorf(\"%s: blocklisted program\", name)\n\t\t\t}\n\t\t\treturn next(ctx, args)\n\t\t}\n\t}\n}\n\nfunc blocklistAllExec(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\treturn fmt.Errorf(\"blocklisted: %s\", args[0])\n\t}\n}\n\nfunc blocklistNondevOpen(ctx context.Context, path string, flags int, mode os.FileMode) (io.ReadWriteCloser, error) {\n\tif path != \"/dev/null\" {\n\t\treturn nil, fmt.Errorf(\"non-dev: %s\", path)\n\t}\n\n\treturn interp.DefaultOpenHandler()(ctx, path, flags, mode)\n}\n\nfunc mockFileOpen(ctx context.Context, path string, flags int, mode os.FileMode) (io.ReadWriteCloser, error) {\n\treturn nopWriterCloser{strings.NewReader(fmt.Sprintf(\"body of %s\", path))}, nil\n}\n\nfunc blocklistGlob(ctx context.Context, path string) ([]fs.FileInfo, error) {\n\treturn nil, fmt.Errorf(\"blocklisted: glob\")\n}\n\nfunc execPrint(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\thc := interp.HandlerCtx(ctx)\n\t\tfmt.Fprintf(hc.Stdout, \"would run: %s\\n\", args)\n\t\treturn nil\n\t}\n}\n\nfunc execExitStatus5(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\treturn interp.ExitStatus(5)\n\t}\n}\n\nfunc execCustomError(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\treturn fmt.Errorf(\"custom error\")\n\t}\n}\n\nfunc execCustomExitStatus5(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\treturn fmt.Errorf(\"custom error: %w\", interp.ExitStatus(5))\n\t}\n}\n\nfunc execDotRunnerBuiltin(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\tif name, ok := strings.CutPrefix(args[0], \".\"); ok {\n\t\t\thc := interp.HandlerCtx(ctx)\n\t\t\targs[0] = name\n\t\t\terr := hc.Builtin(ctx, args)\n\t\t\tfmt.Fprintf(hc.Stdout, \"ran builtin: %s\\n\", args)\n\t\t\treturn err\n\t\t}\n\t\treturn next(ctx, args)\n\t}\n}\n\n// runnerCtx allows us to give handler functions access to the Runner, if needed.\nvar runnerCtx = new(int)\n\nfunc execPrintWouldExec(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\trunner, ok := ctx.Value(runnerCtx).(*interp.Runner)\n\t\tif ok && runner.Exited() {\n\t\t\treturn fmt.Errorf(\"would exec via builtin: %s\", args)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// TODO: join with TestRunnerOpts?\nvar modCases = []struct {\n\tname string\n\topts []interp.RunnerOption\n\tsrc  string\n\twant string\n}{\n\t{\n\t\tname: \"ExecBlocklistOne\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(blocklistOneExec(\"sleep\")),\n\t\t},\n\t\tsrc:  \"echo foo; sleep 1\",\n\t\twant: \"foo\\nRunner.Run error: sleep: blocklisted program\",\n\t},\n\t{\n\t\tname: \"ExecBlocklistOneSubshell\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(\n\t\t\t\tblocklistOneExec(\"faa\"),\n\t\t\t\ttestExecHandler, // sed is used below\n\t\t\t),\n\t\t},\n\t\tsrc:  \"a=$(echo foo | sed 's/o/a/g'); echo $a; $a args\",\n\t\twant: \"faa\\nRunner.Run error: faa: blocklisted program\",\n\t},\n\t{\n\t\tname: \"ExecBlocklistAllSubshell\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(blocklistAllExec),\n\t\t},\n\t\tsrc:  \"(malicious)\",\n\t\twant: \"Runner.Run error: blocklisted: malicious\",\n\t},\n\t{\n\t\tname: \"ExecPipe\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(blocklistAllExec),\n\t\t},\n\t\tsrc:  \"malicious | echo foo\",\n\t\twant: \"foo\\nRunner.Run error: blocklisted: malicious\",\n\t},\n\t{\n\t\tname: \"ExecPipeFail\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(blocklistAllExec),\n\t\t},\n\t\tsrc:  \"set -o pipefail; malicious | echo foo\",\n\t\twant: \"foo\\nRunner.Run error: blocklisted: malicious\",\n\t},\n\t{\n\t\tname: \"ExecCmdSubst\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(blocklistAllExec),\n\t\t},\n\t\tsrc:  \"a=$(malicious)\",\n\t\twant: \"blocklisted: malicious\\nRunner.Run error: blocklisted: malicious\",\n\t},\n\t{\n\t\tname: \"ExecBackground\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(blocklistAllExec),\n\t\t},\n\t\tsrc: \"{ malicious; true; } & { malicious; true; } & wait\",\n\t\t// Note that \"wait\" with no arguments always succeeds.\n\t\twant: \"\",\n\t},\n\t{\n\t\tname: \"ExecPrintWouldExec\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execPrintWouldExec),\n\t\t},\n\t\tsrc:  \"exec /bin/sh\",\n\t\twant: \"Runner.Run error: would exec via builtin: [/bin/sh]\",\n\t},\n\t{\n\t\tname: \"ExecPrintAndBlocklist\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(\n\t\t\t\texecPrint,\n\t\t\t\tblocklistOneExec(\"foo\"),\n\t\t\t),\n\t\t},\n\t\tsrc:  \"foo\",\n\t\twant: \"would run: [foo]\\n\",\n\t},\n\t{\n\t\tname: \"ExecPrintAndBlocklistSeparate\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execPrint),\n\t\t\tinterp.ExecHandlers(blocklistOneExec(\"foo\")),\n\t\t},\n\t\tsrc:  \"foo\",\n\t\twant: \"would run: [foo]\\n\",\n\t},\n\t{\n\t\tname: \"ExecBlocklistAndPrint\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(\n\t\t\t\tblocklistOneExec(\"foo\"),\n\t\t\t\texecPrint,\n\t\t\t),\n\t\t},\n\t\tsrc:  \"foo\",\n\t\twant: \"Runner.Run error: foo: blocklisted program\",\n\t},\n\t{\n\t\tname: \"ExecExitStatus5\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execExitStatus5),\n\t\t},\n\t\tsrc:  \"foo\",\n\t\twant: \"Runner.Run error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomError\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomError),\n\t\t},\n\t\tsrc:  \"foo\",\n\t\twant: \"Runner.Run error: custom error\",\n\t},\n\t{\n\t\tname: \"ExecCustomErrorContinuation\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomError),\n\t\t},\n\t\tsrc:  \"foo; echo next\",\n\t\twant: \"Runner.Run error: custom error\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"foo\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5Continuation\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"foo; echo next\",\n\t\twant: \"next\\n\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5Pipefail\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"set -o pipefail; foo | true\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5ErrExit\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"set -o errexit; foo\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5Exec\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"exec foo; echo never-run\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5Negated\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"! foo; echo custom-error-wiped; exit 1\",\n\t\twant: \"custom-error-wiped\\nRunner.Run error: exit status 1\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5CmdSubst\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"x=$(foo)\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5Subshell\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"(foo)\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5Wait\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"foo & bg=$!; wait $bg\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5Exit\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"foo; exit\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecCustomExitStatus5Source\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execCustomExitStatus5),\n\t\t},\n\t\tsrc:  \"echo 'foo' >a; source ./a\",\n\t\twant: \"Runner.Run error: custom error: exit status 5\",\n\t},\n\t{\n\t\tname: \"ExecDotRunnerBuiltin\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execDotRunnerBuiltin, execExitStatus5),\n\t\t},\n\t\tsrc:  \".true; foo; echo $?; .false\",\n\t\twant: \"ran builtin: [true]\\n5\\nran builtin: [false]\\nRunner.Run error: exit status 1\",\n\t},\n\t{\n\t\tname: \"ExecDotRunnerBuiltinExiting\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ExecHandlers(execDotRunnerBuiltin, execExitStatus5),\n\t\t},\n\t\tsrc:  \"echo before; .exit 0; echo after\",\n\t\twant: \"before\\nran builtin: [exit 0]\\n\",\n\t},\n\t{\n\t\tname: \"NonExecBuiltin\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.CallHandler(func(ctx context.Context, args []string) ([]string, error) {\n\t\t\t\thc := interp.HandlerCtx(ctx)\n\t\t\t\terr := hc.Builtin(ctx, append([]string{\"echo\"}, args...))\n\t\t\t\treturn nil, err\n\t\t\t}),\n\t\t},\n\t\tsrc:  \"foo; bar\",\n\t\twant: \"Runner.Run error: HandlerContext.Builtin can only be called via an ExecHandlerFunc\",\n\t},\n\t{\n\t\tname: \"OpenForbidNonDev\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.OpenHandler(blocklistNondevOpen),\n\t\t},\n\t\tsrc:  \"echo foo >/dev/null; echo bar >/tmp/x\",\n\t\twant: \"Runner.Run error: non-dev: /tmp/x\",\n\t},\n\t{\n\t\tname: \"OpenMockFile\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.OpenHandler(mockFileOpen),\n\t\t},\n\t\tsrc:  \"echo $(<foo); echo $(< <(echo bar))\",\n\t\twant: \"body of foo\\nbar\\n\",\n\t},\n\t{\n\t\tname: \"CallReplaceWithBlank\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.OpenHandler(blocklistNondevOpen),\n\t\t\tinterp.CallHandler(func(ctx context.Context, args []string) ([]string, error) {\n\t\t\t\treturn []string{\"echo\", \"blank\"}, nil\n\t\t\t}),\n\t\t},\n\t\tsrc:  \"echo foo >/dev/null; { bar; } && baz\",\n\t\twant: \"blank\\nblank\\n\",\n\t},\n\t{\n\t\tname: \"CallDryRun\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.CallHandler(func(ctx context.Context, args []string) ([]string, error) {\n\t\t\t\treturn append([]string{\"echo\", \"run:\"}, args...), nil\n\t\t\t}),\n\t\t},\n\t\tsrc:  \"cd some-dir; cat foo; exit 1\",\n\t\twant: \"run: cd some-dir\\nrun: cat foo\\nrun: exit 1\\n\",\n\t},\n\t{\n\t\tname: \"CallError\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.CallHandler(func(ctx context.Context, args []string) ([]string, error) {\n\t\t\t\tif args[0] == \"echo\" && len(args) > 2 {\n\t\t\t\t\treturn nil, fmt.Errorf(\"refusing to run echo builtin with multiple args\")\n\t\t\t\t}\n\t\t\t\treturn args, nil\n\t\t\t}),\n\t\t},\n\t\tsrc:  \"echo foo; echo foo bar\",\n\t\twant: \"foo\\nRunner.Run error: refusing to run echo builtin with multiple args\",\n\t},\n\t{\n\t\tname: \"GlobForbid\",\n\t\topts: []interp.RunnerOption{\n\t\t\tinterp.ReadDirHandler(blocklistGlob),\n\t\t},\n\t\tsrc:  \"echo *\",\n\t\twant: \"blocklisted: glob\\n\",\n\t},\n}\n\nfunc TestRunnerHandlers(t *testing.T) {\n\tt.Parallel()\n\n\tp := syntax.NewParser()\n\tfor _, tc := range modCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tskipIfUnsupported(t, tc.src)\n\t\t\tfile := parse(t, p, tc.src)\n\t\t\ttdir := t.TempDir()\n\t\t\tvar cb concBuffer\n\t\t\tr, err := interp.New(interp.Dir(tdir), interp.StdIO(nil, &cb, &cb))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tfor _, opt := range tc.opts {\n\t\t\t\topt(r)\n\t\t\t}\n\t\t\tctx := context.WithValue(context.Background(), runnerCtx, r)\n\t\t\tif err := r.Run(ctx, file); err != nil {\n\t\t\t\tfmt.Fprintf(&cb, \"Runner.Run error: %v\", err)\n\t\t\t}\n\t\t\tgot := cb.String()\n\t\t\tif got != tc.want {\n\t\t\t\tt.Fatalf(\"want:\\n%q\\ngot:\\n%q\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype readyBuffer struct {\n\tbuf       bytes.Buffer\n\tseenReady sync.WaitGroup\n}\n\nfunc (b *readyBuffer) Write(p []byte) (n int, err error) {\n\tif string(p) == \"ready\\n\" {\n\t\tb.seenReady.Done()\n\t\treturn len(p), nil\n\t}\n\treturn b.buf.Write(p)\n}\n\nfunc TestKillTimeout(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"sleeps and timeouts are slow\")\n\t}\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"skipping trap tests on windows\")\n\t}\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tsrc         string\n\t\twant        string\n\t\tkillTimeout time.Duration\n\t\tforcedKill  bool\n\t}{\n\t\t// killed immediately\n\t\t{\n\t\t\t`sh -c \"trap 'echo trapped; exit 0' INT; echo ready; for i in \\$(seq 1 100); do sleep 0.01; done\"`,\n\t\t\t\"\",\n\t\t\t-1,\n\t\t\ttrue,\n\t\t},\n\t\t// interrupted first, and stops itself in time\n\t\t{\n\t\t\t`sh -c \"trap 'echo trapped; exit 0' INT; echo ready; for i in \\$(seq 1 100); do sleep 0.01; done\"`,\n\t\t\t\"trapped\\n\",\n\t\t\ttime.Second,\n\t\t\tfalse,\n\t\t},\n\t\t// interrupted first, but does not stop itself in time\n\t\t{\n\t\t\t`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\"`,\n\t\t\t\"trapped\\n\",\n\t\t\t20 * time.Millisecond,\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tfile := parse(t, nil, test.src)\n\t\t\tattempt := 0\n\t\t\tfor {\n\t\t\t\tvar rbuf readyBuffer\n\t\t\t\trbuf.seenReady.Add(1)\n\t\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\t\tr, err := interp.New(\n\t\t\t\t\tinterp.StdIO(nil, &rbuf, &rbuf),\n\t\t\t\t\tinterp.ExecHandler(interp.DefaultExecHandler(test.killTimeout)),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tgo func() {\n\t\t\t\t\trbuf.seenReady.Wait()\n\t\t\t\t\tcancel()\n\t\t\t\t}()\n\t\t\t\terr = r.Run(ctx, file)\n\t\t\t\tif test.forcedKill {\n\t\t\t\t\tif errors.As(err, new(interp.ExitStatus)) || err == nil {\n\t\t\t\t\t\tt.Error(\"command was not force-killed\")\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif err != nil && err != context.Canceled && err != context.DeadlineExceeded {\n\t\t\t\t\t\tt.Errorf(\"execution errored: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tgot := rbuf.buf.String()\n\t\t\t\tif got != test.want {\n\t\t\t\t\tif attempt < 3 && got == \"\" && test.killTimeout > 0 {\n\t\t\t\t\t\tattempt++\n\t\t\t\t\t\ttest.killTimeout *= 2\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tt.Fatalf(\"want:\\n%s\\ngot:\\n%s\", test.want, got)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestKillSignal(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"skipping signal tests on windows\")\n\t}\n\ttests := []struct {\n\t\tsignal os.Signal\n\t\twant   error\n\t}{\n\t\t{syscall.SIGINT, interp.ExitStatus(130)},  // 128 + 2\n\t\t{syscall.SIGKILL, interp.ExitStatus(137)}, // 128 + 9\n\t\t{syscall.SIGTERM, interp.ExitStatus(143)}, // 128 + 15\n\t}\n\n\t// pid_and_hang is implemented in TestMain; we use it to have the\n\t// interpreter spawn a process, and easily grab its PID to send it a\n\t// signal directly. The program prints its PID and hangs forever.\n\tfile := parse(t, nil, \"GOSH_CMD=pid_and_hang $GOSH_PROG\")\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"signal-%d\", test.signal), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second)\n\t\t\tdefer cancel()\n\n\t\t\toutReader, outWriter := io.Pipe()\n\t\t\tstderr := new(bytes.Buffer)\n\t\t\tr, _ := interp.New(interp.StdIO(nil, outWriter, stderr))\n\t\t\terrch := make(chan error, 1)\n\t\t\tgo func() {\n\t\t\t\terrch <- r.Run(ctx, file)\n\t\t\t\toutWriter.Close()\n\t\t\t}()\n\n\t\t\tbr := bufio.NewReader(outReader)\n\t\t\tline, err := br.ReadString('\\n')\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tpid, err := strconv.Atoi(strings.TrimSpace(line))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tproc, err := os.FindProcess(pid)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif err := proc.Signal(test.signal); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif got := <-errch; got != test.want {\n\t\t\t\tt.Fatalf(\"want error %v, got %v. stderr: %s\", test.want, got, stderr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "interp/interp_test.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/bits\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-quicktest/qt\"\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/internal\"\n\t\"mvdan.cc/sh/v3/interp\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// runnerRunTimeout is the context timeout used by any tests calling [Runner.Run].\n// The timeout saves us from hangs or burning too much CPU if there are bugs.\n// All the test cases are designed to be inexpensive and stop in a very short\n// amount of time, so 5s should be plenty even for busy machines.\nconst runnerRunTimeout = 5 * time.Second\n\n// Some program which should be in $PATH. Needs to run before runTests is\n// initialized (so an init function wouldn't work), because runTest uses it.\nvar pathProg = func() string {\n\tif runtime.GOOS == \"windows\" {\n\t\treturn \"cmd\"\n\t}\n\treturn \"sh\"\n}()\n\nfunc parse(tb testing.TB, parser *syntax.Parser, src string) *syntax.File {\n\tif parser == nil {\n\t\tparser = syntax.NewParser()\n\t}\n\tfile, err := parser.Parse(strings.NewReader(src), \"\")\n\tif err != nil {\n\t\ttb.Fatal(err)\n\t}\n\treturn file\n}\n\nfunc BenchmarkRun(b *testing.B) {\n\tb.ReportAllocs()\n\n\tsrc := `\necho a b c d\necho ./$foo/etc $(echo foo bar)\nfoo=\"bar\"\nx=y :\nfn() {\n\tlocal a=b\n\tfor i in 1 2 3; do\n\t\techo $i | cat\n\tdone\n}\n[[ $foo == bar ]] && fn\necho a{b,c}d *.go\nlet i=(2 + 3)\n`\n\tfile := parse(b, nil, src)\n\tr, _ := interp.New()\n\tctx := context.Background()\n\n\tfor b.Loop() {\n\t\tr.Reset()\n\t\tif err := r.Run(ctx, file); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nvar hasBash52 bool\n\nfunc TestMain(m *testing.M) {\n\tif os.Getenv(\"GOSH_PROG\") != \"\" {\n\t\tswitch os.Getenv(\"GOSH_CMD\") {\n\t\tcase \"exit_0\":\n\t\t\tos.Exit(0)\n\t\tcase \"exit_5\":\n\t\t\tos.Exit(5)\n\t\tcase \"print_ok\":\n\t\t\tfmt.Printf(\"exec ok\\n\")\n\t\t\tos.Exit(0)\n\t\tcase \"print_fail\":\n\t\t\tfmt.Printf(\"exec fail\\n\")\n\t\t\tos.Exit(1)\n\t\tcase \"pid_and_hang\":\n\t\t\tfmt.Println(os.Getpid())\n\t\t\ttime.Sleep(time.Hour)\n\t\t\tos.Exit(0)\n\t\tcase \"foo_null_bar\":\n\t\t\tfmt.Println(\"foo\\x00bar\")\n\t\t\tos.Exit(0)\n\t\tcase \"lookpath\":\n\t\t\t_, err := exec.LookPath(pathProg)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tfmt.Printf(\"%s found\\n\", pathProg)\n\t\t\tos.Exit(0)\n\t\t}\n\t\tr := strings.NewReader(os.Args[1])\n\t\tfile, err := syntax.NewParser().Parse(r, \"\")\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\trunner, _ := interp.New(\n\t\t\tinterp.StdIO(os.Stdin, os.Stdout, os.Stderr),\n\t\t\tinterp.ExecHandlers(testExecHandler),\n\t\t)\n\t\tctx := context.Background()\n\t\tif err := runner.Run(ctx, file); err != nil {\n\t\t\tvar es interp.ExitStatus\n\t\t\tif errors.As(err, &es) {\n\t\t\t\tos.Exit(int(es))\n\t\t\t}\n\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tos.Exit(0)\n\t}\n\n\tprog, err := os.Executable()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tos.Setenv(\"GOSH_PROG\", prog)\n\n\tinternal.TestMainSetup()\n\n\thasBash52 = checkBash()\n\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tos.Setenv(\"GO_TEST_DIR\", wd)\n\n\tos.Setenv(\"INTERP_GLOBAL\", \"value\")\n\tos.Setenv(\"MULTILINE_INTERP_GLOBAL\", \"\\nwith\\nnewlines\\n\\n\")\n\n\t// Double check that env vars on Windows are case insensitive.\n\tif runtime.GOOS == \"windows\" {\n\t\tos.Setenv(\"mixedCase_INTERP_GLOBAL\", \"value\")\n\t} else {\n\t\tos.Setenv(\"MIXEDCASE_INTERP_GLOBAL\", \"value\")\n\t}\n\n\tos.Setenv(\"PATH_PROG\", pathProg)\n\n\t// To print env vars. Only a builtin on Windows.\n\tif runtime.GOOS == \"windows\" {\n\t\tos.Setenv(\"ENV_PROG\", \"cmd /c set\")\n\t} else {\n\t\tos.Setenv(\"ENV_PROG\", \"env\")\n\t}\n\n\tm.Run()\n}\n\nfunc checkBash() bool {\n\tout, err := exec.Command(\"bash\", \"-c\", \"echo -n $BASH_VERSION\").Output()\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn strings.HasPrefix(string(out), \"5.2\")\n}\n\n// concBuffer wraps a [bytes.Buffer] in a mutex so that concurrent writes\n// to it don't upset the race detector.\ntype concBuffer struct {\n\tbuf bytes.Buffer\n\tsync.Mutex\n}\n\nfunc (c *concBuffer) Write(p []byte) (int, error) {\n\tc.Lock()\n\tn, err := c.buf.Write(p)\n\tc.Unlock()\n\treturn n, err\n}\n\nfunc (c *concBuffer) WriteString(s string) (int, error) {\n\tc.Lock()\n\tn, err := c.buf.WriteString(s)\n\tc.Unlock()\n\treturn n, err\n}\n\nfunc (c *concBuffer) String() string {\n\tc.Lock()\n\ts := c.buf.String()\n\tc.Unlock()\n\treturn s\n}\n\nfunc (c *concBuffer) Reset() {\n\tc.Lock()\n\tc.buf.Reset()\n\tc.Unlock()\n}\n\ntype runTest struct {\n\tin, want string\n}\n\nvar runTests = []runTest{\n\t// no-op programs\n\t{\"\", \"\"},\n\t{\"true\", \"\"},\n\t{\":\", \"\"},\n\t{\"exit\", \"\"},\n\t{\"exit 0\", \"\"},\n\t{\"{ :; }\", \"\"},\n\t{\"(:)\", \"\"},\n\n\t// exit status codes\n\t{\"exit 1\", \"exit status 1\"},\n\t{\"exit -1\", \"exit status 255\"},\n\t{\"exit 300\", \"exit status 44\"},\n\t{\"false\", \"exit status 1\"},\n\t{\"false foo\", \"exit status 1\"},\n\t{\"! false\", \"\"},\n\t{\"true foo\", \"\"},\n\t{\": foo\", \"\"},\n\t{\"! true\", \"exit status 1\"},\n\t{\"false; true\", \"\"},\n\t{\"false; exit\", \"exit status 1\"},\n\t{\"exit; echo foo\", \"\"},\n\t{\"exit 0; echo foo\", \"\"},\n\t{\"printf\", \"usage: printf format [arguments]\\nexit status 2 #JUSTERR\"},\n\t{\"break\", \"break is only useful in a loop\\n #JUSTERR\"},\n\t{\"continue\", \"continue is only useful in a loop\\n #JUSTERR\"},\n\t{\"cd a b\", \"usage: cd [dir]\\nexit status 2 #JUSTERR\"},\n\t{\"shift a\", \"usage: shift [n]\\nexit status 2 #JUSTERR\"},\n\t{\n\t\t\"shouldnotexist\",\n\t\t\"\\\"shouldnotexist\\\": executable file not found in $PATH\\nexit status 127 #JUSTERR\",\n\t},\n\t{\n\t\t\"for i in 1; do continue a; done\",\n\t\t\"usage: continue [n]\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"for i in 1; do break a; done\",\n\t\t\"usage: break [n]\\nexit status 2 #JUSTERR\",\n\t},\n\t{\"false; a=b\", \"\"},\n\t{\"false; false &\", \"\"},\n\t{\n\t\t\"GOSH_CMD=exit_0 $GOSH_PROG; echo next\",\n\t\t\"next\\n\",\n\t},\n\t{\n\t\t\"GOSH_CMD=exit_5 $GOSH_PROG; echo next\",\n\t\t\"next\\n\",\n\t},\n\t{\n\t\t\"! GOSH_CMD=exit_0 $GOSH_PROG\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"! GOSH_CMD=exit_5 $GOSH_PROG\",\n\t\t\"\",\n\t},\n\n\t// we don't need to follow bash error strings\n\t{\"exit a\", \"invalid exit status code: \\\"a\\\"\\nexit status 2 #JUSTERR\"},\n\t{\"exit 1 2\", \"exit cannot take multiple arguments\\nexit status 1 #JUSTERR\"},\n\t{\"f() { return a; }; f\", \"invalid return status code: \\\"a\\\"\\nexit status 2 #JUSTERR\"},\n\n\t// echo\n\t{\"echo\", \"\\n\"},\n\t{\"echo a b c\", \"a b c\\n\"},\n\t{\"echo -n foo\", \"foo\"},\n\t{`echo -e '\\t'`, \"\\t\\n\"},\n\t{`echo -E '\\t'`, \"\\\\t\\n\"},\n\t{`echo -e 'before\\x00after'`, \"before\\x00after\\n\"},\n\t{\"echo -x foo\", \"-x foo\\n\"},\n\t{\"echo -e -x -e foo\", \"-x -e foo\\n\"},\n\n\t// printf\n\t{\"printf foo\", \"foo\"},\n\t{\"printf %%\", \"%\"},\n\t{\"printf %\", \"missing format char\\nexit status 1 #JUSTERR\"},\n\t{\"printf %; echo foo\", \"missing format char\\nfoo\\n #IGNORE\"},\n\t{\"printf %1\", \"missing format char\\nexit status 1 #JUSTERR\"},\n\t{\"printf %+\", \"missing format char\\nexit status 1 #JUSTERR\"},\n\t{\"printf %B foo\", \"invalid format char: B\\nexit status 1 #JUSTERR\"},\n\t{\"printf %12-s foo\", \"invalid format char: -\\nexit status 1 #JUSTERR\"},\n\t{\"printf ' %s \\n' bar\", \" bar \\n\"},\n\t{\"printf '\\\\A'\", \"\\\\A\"},\n\t{\"printf %s foo\", \"foo\"},\n\t{\"printf %s\", \"\"},\n\t{\"printf %d,%i 3 4\", \"3,4\"},\n\t{\"printf %d\", \"0\"},\n\t{\"printf %d,%d 010 0x10\", \"8,16\"},\n\t{\"printf %c,%c,%c foo àa\", \"f,\\xc3,\\x00\"}, // TODO: use a rune?\n\t{\"printf %3s a\", \"  a\"},\n\t{\"printf %3i 1\", \"  1\"},\n\t{\"printf %+i%+d 1 -3\", \"+1-3\"},\n\t{\"printf %-5x 10\", \"a    \"},\n\t{\"printf %02x 1\", \"01\"},\n\t{\"printf 'a% 5s' a\", \"a    a\"},\n\t{\"printf 'nofmt' 1 2 3\", \"nofmt\"},\n\t{\"printf '%d_' 1 2 3\", \"1_2_3_\"},\n\t{\"printf '%02d %02d\\n' 1 2 3\", \"01 02\\n03 00\\n\"},\n\t{`printf '0%s1' 'a\\bc'`, `0a\\bc1`},\n\t{`printf '0%b1' 'a\\bc'`, \"0a\\bc1\"},\n\t{\"printf 'a%bc'\", \"ac\"},\n\t{\"printf 'before\\\\x00after'\", \"before\\x00after\"},\n\n\t// printf escape sequences at end of format string (must not panic)\n\t{\"printf '\\\\0'\", \"\\x00\"},\n\t{\"printf '\\\\01'\", \"\\x01\"},\n\t{\"printf '\\\\x'\", \"\\\\x #IGNORE bash prints a warning to stderr\"},\n\t{\"printf 'a\\\\0'\", \"a\\x00\"},\n\t{\"printf '\\\\\\\\'\", \"\\\\\"},\n\n\t// words and quotes\n\t{\"echo  foo \", \"foo\\n\"},\n\t{\"echo ' foo '\", \" foo \\n\"},\n\t{`echo \" foo \"`, \" foo \\n\"},\n\t{`echo a'b'c\"d\"e`, \"abcde\\n\"},\n\t{`a=\" b c \"; echo $a`, \"b c\\n\"},\n\t{`a=\" b c \"; echo \"$a\"`, \" b c \\n\"},\n\t{`a=\" b c \"; echo foo${a}bar`, \"foo b c bar\\n\"},\n\t{`a=\"b    c\"; echo foo${a}bar`, \"foob cbar\\n\"},\n\t{`echo \"$(echo ' b c ')\"`, \" b c \\n\"},\n\t{\"echo ''\", \"\\n\"},\n\t{`$(echo)`, \"\"},\n\t{`echo -n '\\\\'`, `\\\\`},\n\t{`echo -n \"\\\\\"`, `\\`},\n\t{`set -- a b c; x=\"$@\"; echo \"$x\"`, \"a b c\\n\"},\n\t{`set -- b c; echo a\"$@\"d`, \"ab cd\\n\"},\n\t{`count() { echo $#; }; set --; count \"$@\"`, \"0\\n\"},\n\t{`count() { echo $#; }; set -- \"\"; count \"$@\"`, \"1\\n\"},\n\t{`count() { echo $#; }; set -- \"\"; shift; count \"$@\"`, \"0\\n\"},\n\t{`count() { echo $#; }; a=(); count \"${a[@]}\"`, \"0\\n\"},\n\t{`count() { echo $#; }; count \"${unset_var[@]}\"`, \"0\\n\"},\n\t{`count() { echo $#; }; a=(\"\"); count \"${a[@]}\"`, \"1\\n\"},\n\t{`echo $1 $3; set -- a b c; echo $1 $3`, \"\\na c\\n\"},\n\t{`[[ $0 == \"bash\" || $0 == \"gosh\" ]]`, \"\"},\n\n\t// dollar quotes\n\t{`echo $'foo\\nbar'`, \"foo\\nbar\\n\"},\n\t{`echo $'\\r\\t\\\\'`, \"\\r\\t\\\\\\n\"},\n\t{`echo $\"foo\\nbar\"`, \"foo\\\\nbar\\n\"},\n\t{`echo $'%s'`, \"%s\\n\"},\n\t{`a=$'\\r\\t\\\\'; echo \"$a\"`, \"\\r\\t\\\\\\n\"},\n\t{`a=$\"foo\\nbar\"; echo \"$a\"`, \"foo\\\\nbar\\n\"},\n\t{`echo $'\\a\\b\\e\\E\\f\\v'`, \"\\a\\b\\x1b\\x1b\\f\\v\\n\"},\n\t{`echo $'\\\\\\'\\\"\\?'`, \"\\\\'\\\"?\\n\"},\n\t{`echo $'\\1\\45\\12345\\777\\9'`, \"\\x01%S45\\xff\\\\9\\n\"},\n\t{`echo $'\\x\\xf\\x09\\xAB'`, \"\\\\x\\x0f\\x09\\xab\\n\"},\n\t{`echo $'\\u\\uf\\u09\\uABCD\\u00051234'`, \"\\\\u\\u000f\\u0009\\uabcd\\u00051234\\n\"},\n\t{`echo $'\\U\\Uf\\U09\\UABCD\\U00051234'`, \"\\\\U\\u000f\\u0009\\uabcd\\U00051234\\n\"},\n\t{\n\t\t\"echo 'before\\x00after'\",\n\t\t\"beforeafter\\n\",\n\t},\n\t{\n\t\t\"echo \\\"before\\x00after\\\"\",\n\t\t\"beforeafter\\n\",\n\t},\n\t{\n\t\t\"echo $'before\\x00after'\",\n\t\t\"beforeafter\\n\",\n\t},\n\t{\n\t\t\"echo $'before\\\\x00after'\",\n\t\t\"before\\n\",\n\t},\n\t{\n\t\t\"echo $'before\\\\xafter'\",\n\t\t\"before\\xafter\\n\",\n\t},\n\t{\n\t\t\"a='before\\x00after'; eval \\\"echo -n ${a} ${a@Q}\\\";\",\n\t\t\"beforeafter beforeafter\",\n\t},\n\t{\n\t\t\"a=$'before\\\\x00after'; eval \\\"echo -n ${a} ${a@Q}\\\";\",\n\t\t\"before before\",\n\t},\n\t{\n\t\t\"i\\x00f true; then echo before\\x00; \\x00fi\",\n\t\t\"before\\n\",\n\t},\n\t{\n\t\t\"echo $(GOSH_CMD=foo_null_bar $GOSH_PROG)\",\n\t\t\"foobar\\n #IGNORE\",\n\t},\n\t// See the TODO where foo_NULL_BAR is set.\n\t// {\n\t// \t\"echo $foo_NULL_BAR \\\"${foo_NULL_BAR}\\\"\",\n\t// \t\"foo\\n\",\n\t// },\n\n\t// escaped chars\n\t{\"echo a\\\\b\", \"ab\\n\"},\n\t{\"echo a\\\\ b\", \"a b\\n\"},\n\t{\"echo \\\\$a\", \"$a\\n\"},\n\t{\"echo \\\"a\\\\b\\\"\", \"a\\\\b\\n\"},\n\t{\"echo 'a\\\\b'\", \"a\\\\b\\n\"},\n\t{\"echo \\\"a\\\\\\nb\\\"\", \"ab\\n\"},\n\t{\"echo 'a\\\\\\nb'\", \"a\\\\\\nb\\n\"},\n\t{`echo \"\\\"\"`, \"\\\"\\n\"},\n\t{`echo \\\\`, \"\\\\\\n\"},\n\t{`echo \\\\\\\\`, \"\\\\\\\\\\n\"},\n\t{`echo \\`, \"\\n\"},\n\n\t// escape characters in double quote literal\n\t{`echo \"\\\\\"`, \"\\\\\\n\"},     // special character is preserved\n\t{`echo \"\\b\"`, \"\\\\b\\n\"},    // non-special character has both characters preserved\n\t{`echo \"\\\\\\\\\"`, \"\\\\\\\\\\n\"}, // sequential backslashes (escape characters repeated sequentially)\n\n\t// vars\n\t{\"foo=bar; echo $foo\", \"bar\\n\"},\n\t{\"foo=bar foo=etc; echo $foo\", \"etc\\n\"},\n\t{\"foo=bar; foo=etc; echo $foo\", \"etc\\n\"},\n\t{\"foo=bar; foo=; echo $foo\", \"\\n\"},\n\t{\"unset foo; echo $foo\", \"\\n\"},\n\t{\"foo=bar; unset foo; echo $foo\", \"\\n\"},\n\t{\"echo $INTERP_GLOBAL\", \"value\\n\"},\n\t{\"INTERP_GLOBAL=; echo $INTERP_GLOBAL\", \"\\n\"},\n\t{\"unset INTERP_GLOBAL; echo $INTERP_GLOBAL\", \"\\n\"},\n\t{\"echo $MIXEDCASE_INTERP_GLOBAL\", \"value\\n\"},\n\t{\"foo=bar; foo=x true; echo $foo\", \"bar\\n\"},\n\t{\"foo=bar; foo=x true; echo $foo\", \"bar\\n\"},\n\t{\"foo=bar; $ENV_PROG | grep '^foo='\", \"exit status 1\"},\n\t{\"foo=bar $ENV_PROG | grep '^foo='\", \"foo=bar\\n\"},\n\t{\"foo=a foo=b $ENV_PROG | grep '^foo='\", \"foo=b\\n\"},\n\t{\"$ENV_PROG | grep -i '^interp_global='\", \"INTERP_GLOBAL=value\\n\"},\n\t{\"INTERP_GLOBAL=new; $ENV_PROG | grep -i '^interp_global='\", \"INTERP_GLOBAL=new\\n\"},\n\t{\"INTERP_GLOBAL=; $ENV_PROG | grep -i '^interp_global='\", \"INTERP_GLOBAL=\\n\"},\n\t{\"unset INTERP_GLOBAL; $ENV_PROG | grep -i '^interp_global='\", \"exit status 1\"},\n\t{\"a=b; a+=c x+=y; echo $a $x\", \"bc y\\n\"},\n\t{`a=\" x  y\"; b=$a c=\"$a\"; echo $b; echo $c`, \"x y\\nx y\\n\"},\n\t{`a=\" x  y\"; b=$a c=\"$a\"; echo \"$b\"; echo \"$c\"`, \" x  y\\n x  y\\n\"},\n\t{`arr=(\"foo\" \"bar\" \"lala\" \"foobar\"); echo ${arr[@]:2}; echo ${arr[*]:2}`, \"lala foobar\\nlala foobar\\n\"},\n\t{`arr=(\"foo\" \"bar\" \"lala\" \"foobar\"); echo ${arr[@]:2:4}; echo ${arr[*]:1:4}`, \"lala foobar\\nbar lala foobar\\n\"},\n\t{`arr=(\"foo\" \"bar\"); echo ${arr[@]}; echo ${arr[*]}`, \"foo bar\\nfoo bar\\n\"},\n\t{`arr=(\"foo\"); echo ${arr[@]:99}`, \"\\n\"},\n\t{`echo ${arr[@]:1:99}; echo ${arr[*]:1:99}`, \"\\n\\n\"},\n\t{`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\"},\n\n\t// quoted array slicing\n\t{`a=(1 2 3 4 5); echo \"${a[@]:2:2}\"`, \"3 4\\n\"},\n\t{`a=(1 2 3 4 5); echo \"${a[*]:2:2}\"`, \"3 4\\n\"},\n\t{`a=(1 2 3 4 5); b=(\"${a[@]:2:2}\"); echo ${#b[@]}`, \"2\\n\"},\n\t{`a=(1 2 3 4 5); echo \"${a[@]:3}\"`, \"4 5\\n\"},\n\t{`a=(1 2 3 4 5); echo \"${a[@]: -2}\"`, \"4 5\\n\"},\n\t{`a=(1 2 3 4 5); echo \"${a[@]: -99}\"`, \"\\n\"},\n\n\t// positional parameter slicing (1-based offset, $0 at offset 0)\n\t{`f() { echo \"${@:2:2}\"; }; f a b c d e`, \"b c\\n\"},\n\t{`f() { echo ${@:2:2}; }; f a b c d e`, \"b c\\n\"},\n\t{`f() { echo \"${@:1}\"; }; f a b c`, \"a b c\\n\"},\n\t{`f() { echo \"${*:2:2}\"; }; f a b c d e`, \"b c\\n\"},\n\t{`f() { echo \"${@: -2}\"; }; f a b c d e`, \"d e\\n\"},\n\t{`f() { echo \"${@: -3:2}\"; }; f a b c d e`, \"c d\\n\"},\n\t{`f() { echo \"${@:1:0}\"; }; f a b c`, \"\\n\"},\n\t{`f() { echo \"${@:99}\"; }; f a b c`, \"\\n\"},\n\t{`set -- a b c; v=(\"${@:0:2}\"); echo \"${#v[@]}\"`, \"2\\n\"},\n\t{`f() { for x in \"${@:2:2}\"; do echo \"$x\"; done; }; f a b c d e`, \"b\\nc\\n\"},\n\t{`set --; v=(\"${@:0}\"); echo \"${#v[@]}\"`, \"1\\n\"},\n\t{`f() { echo \"${@: -10}\"; }; f a b c`, \"\\n\"},\n\n\t{`echo ${foo[@]}; echo ${foo[*]}`, \"\\n\\n\"},\n\t// TODO: reenable once we figure out the broken pipe error\n\t//{`$ENV_PROG | while read line; do if test -z \"$line\"; then echo empty; fi; break; done`, \"\"}, // never begin with an empty element\n\n\t// inline variables have special scoping\n\t{\n\t\t\"f() { echo $inline; inline=bar true; echo $inline; }; inline=foo f\",\n\t\t\"foo\\nfoo\\n\",\n\t},\n\t{\"v=x; read v <<< 'y'; echo $v\", \"y\\n\"},\n\t{\"v=x; v=inline read v <<< 'y'; echo $v\", \"x\\n\"},\n\t{\"v=x; v=inline unset v; echo $v\", \"x\\n\"},\n\t{\"v=x; echo 'v=y' >f; v=inline source ./f; echo $v\", \"x\\n\"},\n\t{\"declare -n v=v2; v=inline true; echo $v $v2\", \"\\n\"},\n\t{\"f() { echo $v; }; v=x; v=y f; f\", \"y\\nx\\n\"},\n\t{\"f() { echo $v; }; v=x; v+=y f; f\", \"xy\\nx\\n\"},\n\t{\"f() { echo $v; }; declare -n v=v2; v2=x; v=y f; f\", \"y\\nx\\n\"},\n\t{\"f() { echo ${v[@]}; }; v=(e1 e2); v=y f; f\", \"y\\ne1 e2\\n\"},\n\n\t// special vars\n\t{\"echo $?; false; echo $?\", \"0\\n1\\n\"},\n\t{\"for i in 1 2; do\\necho $LINENO\\necho $LINENO\\ndone\", \"2\\n3\\n2\\n3\\n\"},\n\t{\"[[ -n $$ && $$ -gt 0 ]]\", \"\"},\n\t{\"[[ $$ -eq $PPID ]]\", \"exit status 1\"},\n\t{\"[[ $RANDOM -eq $RANDOM ]]\", \"exit status 1\"},   // 1 in 32k chance of a collision, 0.003%\n\t{\"[[ $SRANDOM -eq $SRANDOM ]]\", \"exit status 1\"}, // 1 in 2**32 chance of a collision,\n\n\t// Ensure that we consistently use 64 bits even on 32-bit platforms.\n\t// Bash doesn't do this, but we do, for portability and consistency.\n\t{\"[[ 1000000000123 -lt 100 ]]\", \"exit status 1\"},\n\t{\"[[ 1000000000123 -eq 1000000000456 ]]\", \"exit status 1\"},\n\t{\"[[ 1000000000123 < 100 ]]\", \"exit status 1\"},\n\t{\"((1000000000123 == 1000000000456))\", \"exit status 1\"},\n\n\t// var manipulation\n\t{\"echo ${#a} ${#a[@]}\", \"0 0\\n\"},\n\t{\"a=bar; echo ${#a} ${#a[@]}\", \"3 1\\n\"},\n\t{\"a=世界; echo ${#a}\", \"2\\n\"},\n\t{\"a=(a bcd); echo ${#a} ${#a[@]} ${#a[*]} ${#a[1]}\", \"1 2 2 3\\n\"},\n\t{\n\t\t\"a=($(echo a bcd)); echo ${#a} ${#a[@]} ${#a[*]} ${#a[1]}\",\n\t\t\"1 2 2 3\\n\",\n\t},\n\t{\n\t\t\"a=([0]=$(echo a b) $(echo c d)); echo ${#a} ${#a[@]} ${#a[*]} ${#a[0]}\",\n\t\t\"3 3 3 3\\n\",\n\t},\n\t{\"set -- a bc; echo ${#@} ${#*} $#\", \"2 2 2\\n\"},\n\t{\n\t\t\"echo ${!a}; echo more\",\n\t\t\"invalid indirect expansion\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"a=b; echo ${!a}; b=c; echo ${!a}\",\n\t\t\"\\nc\\n\",\n\t},\n\t{\n\t\t\"a=foo_very_long; echo ${a:1}; echo ${a: -1}; echo ${a: -10}; echo ${a:5}\",\n\t\t\"oo_very_long\\ng\\n_very_long\\nery_long\\n\",\n\t},\n\t{\n\t\t\"a=foo_very_long; echo ${a::2}; echo ${a::-1}; echo ${a: -10}; echo ${a::5}\",\n\t\t\"fo\\nfoo_very_lon\\n_very_long\\nfoo_v\\n\",\n\t},\n\t{\n\t\t\"a=abc; echo ${a:1:1}\",\n\t\t\"b\\n\",\n\t},\n\t{\n\t\t\"a=foo; echo ${a/no/x} ${a/o/i} ${a//o/i} ${a/fo/}\",\n\t\t\"foo fio fii o\\n\",\n\t},\n\t{\n\t\t\"a=foo; echo ${a/*/xx} ${a//?/na} ${a/o*}\",\n\t\t\"xx nanana f\\n\",\n\t},\n\t{\n\t\t\"a=12345; echo ${a//[42]} ${a//[^42]} ${a//[!42]}\",\n\t\t\"135 24 24\\n\",\n\t},\n\t{\"a=0123456789; echo ${a//[1-35-8]}\", \"049\\n\"},\n\t{\"a=]abc]; echo ${a//[]b]}\", \"ac\\n\"},\n\t{\"a=-abc-; echo ${a//[-b]}\", \"ac\\n\"},\n\t{`a='x\\y'; echo ${a//\\\\}`, \"xy\\n\"},\n\t{\"a=']'; echo ${a//[}\", \"]\\n\"},\n\t{\"a=']'; echo ${a//[]}\", \"]\\n\"},\n\t{\"a=']'; echo ${a//[]]}\", \"\\n\"},\n\t{\"a='['; echo ${a//[[]}\", \"\\n\"},\n\t{\"a=']'; echo ${a//[xy}\", \"]\\n\"},\n\t{\"a='abc123'; echo ${a//[[:digit:]]}\", \"abc\\n\"},\n\t{\"a='[[:wrong:]]'; echo ${a//[[:wrong:]]}\", \"[[:wrong:]]\\n\"},\n\t{\"a='[[:wrong:]]'; echo ${a//[[:}\", \"[[:wrong:]]\\n\"},\n\t{\"a='abcx1y'; echo ${a//x[[:digit:]]y}\", \"abc\\n\"},\n\t{`a=xyz; echo \"${a/y/a  b}\"`, \"xa  bz\\n\"},\n\t{\"a='foo/bar'; echo ${a//o*a/}\", \"fr\\n\"},\n\t{\"a=foobar; echo ${a//a/} ${a///b} ${a///}\", \"foobr foobar foobar\\n\"},\n\t{\n\t\t\"echo ${a:-b}; echo $a; a=; echo ${a:-b}; a=c; echo ${a:-b}\",\n\t\t\"b\\n\\nb\\nc\\n\",\n\t},\n\t{\n\t\t\"echo ${#:-never} ${?:-never} ${LINENO:-never}\",\n\t\t\"0 0 1\\n\",\n\t},\n\t{\n\t\t\"echo ${1-one} ${2-two} ${3-three}\",\n\t\t\"one two three\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${1}\",\n\t\t\"1: unbound variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"echo ${a-b}; echo $a; a=; echo ${a-b}; a=c; echo ${a-b}\",\n\t\t\"b\\n\\n\\nc\\n\",\n\t},\n\t{\n\t\t\"echo ${a:=b}; echo $a; a=; echo ${a:=b}; a=c; echo ${a:=b}\",\n\t\t\"b\\nb\\nb\\nc\\n\",\n\t},\n\t{\n\t\t\"echo ${a=b}; echo $a; a=; echo ${a=b}; a=c; echo ${a=b}\",\n\t\t\"b\\nb\\n\\nc\\n\",\n\t},\n\t{\n\t\t\"echo ${a:+b}; echo $a; a=; echo ${a:+b}; a=c; echo ${a:+b}\",\n\t\t\"\\n\\n\\nb\\n\",\n\t},\n\t{\n\t\t\"echo ${a+b}; echo $a; a=; echo ${a+b}; a=c; echo ${a+b}\",\n\t\t\"\\n\\nb\\nb\\n\",\n\t},\n\t{\n\t\t\"a=b; echo ${a:?err1}; a=; echo ${a:?err2}; unset a; echo ${a:?err3}\",\n\t\t\"b\\na: err2\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"a=b; echo ${a?err1}; a=; echo ${a?err2}; unset a; echo ${a?err3}\",\n\t\t\"b\\n\\na: err3\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"echo ${a:?%s}\",\n\t\t\"a: %s\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"x=aaabccc; echo ${x#*a}; echo ${x##*a}\",\n\t\t\"aabccc\\nbccc\\n\",\n\t},\n\t{\n\t\t\"x=(__a _b c_); echo ${x[@]#_}\",\n\t\t\"_a b c_\\n\",\n\t},\n\t{\n\t\t\"x=(a__ b_ _c); echo ${x[@]%%_}\",\n\t\t\"a_ b _c\\n\",\n\t},\n\t{\n\t\t\"x=aaabccc; echo ${x%c*}; echo ${x%%c*}\",\n\t\t\"aaabcc\\naaab\\n\",\n\t},\n\t{\n\t\t\"x=aaabccc; echo ${x%%[bc}\",\n\t\t\"aaabccc\\n\",\n\t},\n\t{\n\t\t\"a='àÉñ bAr'; echo ${a^}; echo ${a^^}\",\n\t\t\"ÀÉñ bAr\\nÀÉÑ BAR\\n\",\n\t},\n\t{\n\t\t\"a='àÉñ bAr'; echo ${a,}; echo ${a,,}\",\n\t\t\"àÉñ bAr\\nàéñ bar\\n\",\n\t},\n\t{\n\t\t\"a='àÉñ bAr'; echo ${a^?}; echo ${a^^[br]}\",\n\t\t\"ÀÉñ bAr\\nàÉñ BAR\\n\",\n\t},\n\t{\n\t\t\"a='àÉñ bAr'; echo ${a,?}; echo ${a,,[br]}\",\n\t\t\"àÉñ bAr\\nàÉñ bAr\\n\",\n\t},\n\t{\n\t\t\"a=(àÉñ bAr); echo ${a[@]^}; echo ${a[*],,}\",\n\t\t\"ÀÉñ BAr\\nàéñ bar\\n\",\n\t},\n\t{\n\t\t\"INTERP_X_1=a INTERP_X_2=b; echo ${!INTERP_X_*}\",\n\t\t\"INTERP_X_1 INTERP_X_2\\n\",\n\t},\n\t{\n\t\t\"INTERP_X_2=b INTERP_X_1=a; echo ${!INTERP_*}\",\n\t\t\"INTERP_GLOBAL INTERP_X_1 INTERP_X_2\\n\",\n\t},\n\t{\n\t\t`INTERP_X_2=b INTERP_X_1=a; set -- ${!INTERP_*}; echo $#`,\n\t\t\"3\\n\",\n\t},\n\t{\n\t\t`INTERP_X_2=b INTERP_X_1=a; set -- \"${!INTERP_*}\"; echo $#`,\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t`INTERP_X_2=b INTERP_X_1=a; set -- ${!INTERP_@}; echo $#`,\n\t\t\"3\\n\",\n\t},\n\t{\n\t\t`INTERP_X_2=b INTERP_X_1=a; set -- \"${!INTERP_@}\"; echo $#`,\n\t\t\"3\\n\",\n\t},\n\t{\n\t\t`a='b  c'; eval \"echo -n ${a} ${a@Q}\"`,\n\t\t`b c b  c`,\n\t},\n\t{\n\t\t`a='\"\\n'; printf \"%s %s\" \"${a}\" \"${a@E}\"`,\n\t\t\"\\\"\\\\n \\\"\\n\",\n\t},\n\n\t// ${var@a} and ${var@A}\n\t{\n\t\t`a=foo; echo \"<${a@a}>\"`,\n\t\t\"<>\\n\",\n\t},\n\t{\n\t\t`declare -a arr=(1 2 3); echo \"${arr@a}\"`,\n\t\t\"a\\n\",\n\t},\n\t{\n\t\t`declare -A map=([k]=v); echo \"${map@a}\"`,\n\t\t\"A\\n\",\n\t},\n\t{\n\t\t`export e=1; echo \"${e@a}\"`,\n\t\t\"x\\n\",\n\t},\n\t{\n\t\t`readonly ro=1; echo \"${ro@a}\"`,\n\t\t\"r\\n\",\n\t},\n\t{\n\t\t`declare -a arr=(1); export arr; echo \"${arr@a}\"`,\n\t\t\"ax\\n\",\n\t},\n\t{\n\t\t`a=hello; echo \"${a@A}\"`,\n\t\t\"a=hello\\n #IGNORE bash always single-quotes\",\n\t},\n\t{\n\t\t`export e=1; echo \"${e@A}\"`,\n\t\t\"declare -x e=1\\n #IGNORE bash always single-quotes\",\n\t},\n\t{\n\t\t\"declare a; a+=(b); echo ${a[@]} ${#a[@]}\",\n\t\t\"b 1\\n\",\n\t},\n\t{\n\t\t`a=\"\"; a+=(b); echo ${a[@]} ${#a[@]}`,\n\t\t\"b 2\\n\",\n\t},\n\t{\n\t\t\"f() { local a; a=bad; a=good; echo $a; }; f\",\n\t\t\"good\\n\",\n\t},\n\t{\n\t\t`declare x; [[ -v x ]] && echo set || echo unset`,\n\t\t\"unset\\n\",\n\t},\n\t{\n\t\t`declare x=; [[ -v x ]] && echo set || echo unset`,\n\t\t\"set\\n\",\n\t},\n\t{\n\t\t`declare -a x; [[ -v x ]] && echo set || echo unset`,\n\t\t\"unset\\n\",\n\t},\n\t{\n\t\t`declare -A x; [[ -v x ]] && echo set || echo unset`,\n\t\t\"unset\\n\",\n\t},\n\t{\n\t\t`declare -r -x x; [[ -v x ]] && echo set || echo unset`,\n\t\t\"unset\\n\",\n\t},\n\t{\n\t\t`declare -n x; [[ -v x ]] && echo set || echo unset`,\n\t\t\"unset\\n\",\n\t},\n\n\t// declare -f and declare -p\n\t{\n\t\t`f() { echo hello; }; declare -f f`,\n\t\t\"f()\\n{ echo hello; }\\n #IGNORE output format differs from bash\",\n\t},\n\t{\n\t\t`declare -f nonexistent 2>/dev/null; echo \"exit: $?\"`,\n\t\t\"exit: 1\\n\",\n\t},\n\t{\n\t\t`f() { echo hello; }; declare -f f >/dev/null && echo \"f exists\"`,\n\t\t\"f exists\\n\",\n\t},\n\t{\n\t\t`a=hello; declare -p a`,\n\t\t\"declare -- a=\\\"hello\\\"\\n\",\n\t},\n\t{\n\t\t`declare -a arr=(1 2 3); declare -p arr`,\n\t\t\"declare -a arr=([0]=\\\"1\\\" [1]=\\\"2\\\" [2]=\\\"3\\\")\\n\",\n\t},\n\t{\n\t\t`export e=1; declare -p e`,\n\t\t\"declare -x e=\\\"1\\\"\\n\",\n\t},\n\t{\n\t\t`readonly c=immutable; declare -p c`,\n\t\t\"declare -r c=\\\"immutable\\\"\\n\",\n\t},\n\t{\n\t\t`declare -p nonexistent 2>/dev/null; echo \"exit: $?\"`,\n\t\t\"exit: 1\\n\",\n\t},\n\n\t// if\n\t{\n\t\t\"if true; then echo foo; fi\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"if false; then echo foo; fi\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"if GOSH_CMD=print_fail $GOSH_PROG; then echo foo; fi\",\n\t\t\"exec fail\\n\",\n\t},\n\t{\n\t\t\"if true; then echo foo; else echo bar; fi\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"if false; then echo foo; else echo bar; fi\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"if true; then false; fi\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"if false; then :; else false; fi\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"if false; then :; elif true; then echo foo; fi\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"if false; then :; elif false; then :; elif true; then echo foo; fi\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"if false; then :; elif false; then :; else echo foo; fi\",\n\t\t\"foo\\n\",\n\t},\n\n\t// while\n\t{\n\t\t\"while false; do echo foo; done\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"while GOSH_CMD=print_fail $GOSH_PROG; do echo foo; done\",\n\t\t\"exec fail\\n\",\n\t},\n\t{\n\t\t\"while true; do exit 1; done\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"while true; do break; done\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"while true; do while true; do break 2; done; done\",\n\t\t\"\",\n\t},\n\n\t// until\n\t{\n\t\t\"until true; do echo foo; done\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"until false; do exit 1; done\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"until false; do break; done\",\n\t\t\"\",\n\t},\n\n\t// for\n\t{\n\t\t\"for i in 1 2 3; do echo $i; done\",\n\t\t\"1\\n2\\n3\\n\",\n\t},\n\t{\n\t\t\"for i in 1 2 3; do echo $i; exit; done\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"for i in 1 2 3; do echo $i; false; done\",\n\t\t\"1\\n2\\n3\\nexit status 1\",\n\t},\n\t{\n\t\t\"for i in 1 2 3; do echo $i; break; done\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"for i in 1 2 3; do echo $i; continue; echo foo; done\",\n\t\t\"1\\n2\\n3\\n\",\n\t},\n\t{\n\t\t\"for i in 1 2; do for j in a b; do echo $i $j; continue 2; done; done\",\n\t\t\"1 a\\n2 a\\n\",\n\t},\n\t{\n\t\t\"for ((i=0; i<3; i++)); do echo $i; done\",\n\t\t\"0\\n1\\n2\\n\",\n\t},\n\t// for, with missing Init, Cond, Post\n\t{\n\t\t\"i=0; for ((; i<3; i++)); do echo $i; done\",\n\t\t\"0\\n1\\n2\\n\",\n\t},\n\t{\n\t\t\"for ((i=0;; i++)); do if [ $i -ge 3 ]; then break; fi; echo $i; done\",\n\t\t\"0\\n1\\n2\\n\",\n\t},\n\t{\n\t\t\"for ((i=0; i<3;)); do echo $i; i=$((i+1)); done\",\n\t\t\"0\\n1\\n2\\n\",\n\t},\n\t{\n\t\t\"i=0; for ((;;)); do if [ $i -ge 3 ]; then break; fi; echo $i; i=$((i+1)); done\",\n\t\t\"0\\n1\\n2\\n\",\n\t},\n\t// TODO: uncomment once expandEnv.Set starts returning errors\n\t// {\n\t// \t\"readonly i; for ((i=0; i<3; i++)); do echo $i; done\",\n\t// \t\"0\\n1\\n2\\n\",\n\t// },\n\t{\n\t\t\"for ((i=5; i>0; i--)); do echo $i; break; done\",\n\t\t\"5\\n\",\n\t},\n\t{\n\t\t\"for i in 1 2; do for j in a b; do echo $i $j; done; break; done\",\n\t\t\"1 a\\n1 b\\n\",\n\t},\n\t{\n\t\t\"for i in 1 2 3; do :; done; echo $i\",\n\t\t\"3\\n\",\n\t},\n\t{\n\t\t\"for ((i=0; i<3; i++)); do :; done; echo $i\",\n\t\t\"3\\n\",\n\t},\n\t{\n\t\t\"set -- a 'b c'; for i in; do echo $i; done\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"set -- a 'b c'; for i; do echo $i; done\",\n\t\t\"a\\nb c\\n\",\n\t},\n\n\t// block\n\t{\n\t\t\"{ echo foo; }\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"{ false; }\",\n\t\t\"exit status 1\",\n\t},\n\n\t// subshell\n\t{\n\t\t\"(echo foo)\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"(false)\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"(exit 1)\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"(false); echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"(exit 0); echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"(exit 1); echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"(foo=bar; echo $foo); echo $foo\",\n\t\t\"bar\\n\\n\",\n\t},\n\t{\n\t\t\"(echo() { printf 'bar\\n'; }; echo); echo\",\n\t\t\"bar\\n\\n\",\n\t},\n\t{\n\t\t\"unset INTERP_GLOBAL & echo $INTERP_GLOBAL\",\n\t\t\"value\\n\",\n\t},\n\t{\n\t\t\"(fn() { :; }) & pwd >/dev/null\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"x[0]=x; (echo ${x[0]}; x[0]=y; echo ${x[0]}); echo ${x[0]}\",\n\t\t\"x\\ny\\nx\\n\",\n\t},\n\t{\n\t\t`x[3]=x; (x[3]=y); echo ${x[3]}`,\n\t\t\"x\\n\",\n\t},\n\t{\n\t\t\"shopt -s expand_aliases; alias f='echo x'\\nf\\n(f\\nalias f='echo y'\\neval f\\n)\\nf\\n\",\n\t\t\"x\\nx\\ny\\nx\\n\",\n\t},\n\t{\n\t\t\"set -- a; echo $1; (echo $1; set -- b; echo $1); echo $1\",\n\t\t\"a\\na\\nb\\na\\n\",\n\t},\n\t{\"false; ( echo $? )\", \"1\\n\"},\n\n\t// cd/pwd\n\t{\"[[ fo~ == 'fo~' ]]\", \"\"},\n\t{`[[ 'ab\\c' == *\\\\* ]]`, \"\"},\n\t{`[[ foo/bar == foo* ]]`, \"\"},\n\t{\"[[ a == [ab ]]\", \"exit status 1\"},\n\t{`HOME='/*'; echo ~; echo \"$HOME\"`, \"/*\\n/*\\n\"},\n\t{`test -d ~`, \"\"},\n\t{\n\t\t`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`,\n\t\t`b1 c1 d1 e1 f1 g1 h1 k1 L1 p1 r1 s1 S1 u1 w1 x1 `,\n\t},\n\t{`foo=~; test -d $foo`, \"\"},\n\t{`foo=~; test -d \"$foo\"`, \"\"},\n\t{`foo='~'; test -d $foo`, \"exit status 1\"},\n\t{`foo='~'; [ $foo == '~' ]`, \"\"},\n\t{\n\t\t`[[ ~ == \"$HOME\" ]] && [[ ~/foo == \"$HOME/foo\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`HOME=$PWD/home; mkdir home; touch home/f; [[ -e ~/f ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`HOME=$PWD/home; mkdir home; touch home/f; [[ ~/f -ef $HOME/f ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ ~noexist == '~noexist' ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t`w=\"$HOME\"; cd; [[ $PWD == \"$w\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`mkdir test.cd; cd test.cd; cd ''; [[ \"$PWD\" == \"$OLDPWD\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`HOME=/foo; echo $HOME`,\n\t\t\"/foo\\n\",\n\t},\n\t{\n\t\t\"cd noexist\",\n\t\t\"cd: no such file or directory: \\\"noexist\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"mkdir -p a/b && cd a && cd b && cd ../..\",\n\t\t\"\",\n\t},\n\t{\n\t\t\">a && cd a\",\n\t\t\"cd: no such file or directory: \\\"a\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t`[[ $PWD == \"$(pwd)\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t\"PWD=changed; [[ $PWD == changed ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"PWD=changed; mkdir a; cd a; [[ $PWD == changed ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`mkdir %s; old=\"$PWD\"; cd %s; [[ $old == \"$PWD\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`old=\"$PWD\"; mkdir a; cd a; cd ..; [[ $old == \"$PWD\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`[[ $PWD == \"$OLDPWD\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`old=\"$PWD\"; mkdir a; cd a; [[ $old == \"$OLDPWD\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`mkdir a; ln -s a b; [[ $(cd a && pwd) == \"$(cd b && pwd)\" ]]; echo $?`,\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t`pwd -a`,\n\t\t\"invalid option: \\\"-a\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t`pwd -L -P -a`,\n\t\t\"invalid option: \\\"-a\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t`mkdir a; ln -s a b; [[ \"$(cd a && pwd -P)\" == \"$(cd b && pwd -P)\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`mkdir a; ln -s a b; [[ \"$(cd a && pwd -P)\" == \"$(cd b && pwd -L)\" ]]; echo $?`,\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t`orig=\"$PWD\"; mkdir a; cd a; cd - >/dev/null; [[ \"$PWD\" == \"$orig\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`orig=\"$PWD\"; mkdir a; cd a; [[ $(cd -) == \"$orig\" ]]`,\n\t\t\"\",\n\t},\n\n\t// dirs/pushd/popd\n\t{\"set -- $(dirs); echo $# ${#DIRSTACK[@]}\", \"1 1\\n\"},\n\t{\"pushd\", \"pushd: no other directory\\nexit status 1 #JUSTERR\"},\n\t{\"pushd -n\", \"\"},\n\t{\"pushd foo bar\", \"pushd: too many arguments\\nexit status 2 #JUSTERR\"},\n\t{\"pushd does-not-exist; set -- $(dirs); echo $#\", \"pushd: no such file or directory: \\\"does-not-exist\\\"\\n1\\n #IGNORE\"},\n\t{\"mkdir a; pushd a >/dev/null; set -- $(dirs); echo $#\", \"2\\n\"},\n\t{\"mkdir a; set -- $(pushd a); echo $#\", \"2\\n\"},\n\t{\n\t\t`mkdir a; pushd a >/dev/null; set -- $(dirs); [[ $1 == \"$HOME\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`mkdir a; pushd a >/dev/null; [[ ${DIRSTACK[0]} == \"$HOME\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`old=$(dirs); mkdir a; pushd a >/dev/null; pushd >/dev/null; set -- $(dirs); [[ $1 == \"$old\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`old=$(dirs); mkdir a; pushd a >/dev/null; pushd -n >/dev/null; set -- $(dirs); [[ $1 == \"$old\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"mkdir a; pushd a >/dev/null; pushd >/dev/null; rm -r a; pushd\",\n\t\t\"pushd: no such file or directory: ABS_PATH_A\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t`old=$(dirs); mkdir a; pushd -n a >/dev/null; set -- $(dirs); [[ $1 == \"$old\" ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`old=$(dirs); mkdir a; pushd -n a >/dev/null; pushd >/dev/null; set -- $(dirs); [[ $1 == \"$old\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\"popd\", \"popd: directory stack empty\\nexit status 1 #JUSTERR\"},\n\t{\"popd -n\", \"popd: directory stack empty\\nexit status 1 #JUSTERR\"},\n\t{\"popd foo\", \"popd: invalid argument\\nexit status 2 #JUSTERR\"},\n\t{\"old=$(dirs); mkdir a; pushd a >/dev/null; set -- $(popd); echo $#\", \"1\\n\"},\n\t{\n\t\t`old=$(dirs); mkdir a; pushd a >/dev/null; popd >/dev/null; [[ $(dirs) == \"$old\" ]]`,\n\t\t\"\",\n\t},\n\t{\"old=$(dirs); mkdir a; pushd a >/dev/null; set -- $(popd -n); echo $#\", \"1\\n\"},\n\t{\n\t\t`old=$(dirs); mkdir a; pushd a >/dev/null; popd -n >/dev/null; [[ $(dirs) == \"$old\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"mkdir a; pushd a >/dev/null; pushd >/dev/null; rm -r a; popd\",\n\t\t\"popd: no such file or directory: ABS_PATH_A\\nexit status 1 #JUSTERR\",\n\t},\n\n\t// binary cmd\n\t{\n\t\t\"true && echo foo || echo bar\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"false && echo foo || echo bar\",\n\t\t\"bar\\n\",\n\t},\n\n\t// func\n\t{\n\t\t\"foo() { echo bar; }; foo\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"foo() { echo $1; }; foo\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"foo() { echo $1; }; foo a b\",\n\t\t\"a\\n\",\n\t},\n\t{\n\t\t\"foo() { echo $1; bar c d; echo $2; }; bar() { echo $2; }; foo a b\",\n\t\t\"a\\nd\\nb\\n\",\n\t},\n\t{\n\t\t`foo() { echo $#; }; foo; foo 1 2 3; foo \"a b\"; echo $#`,\n\t\t\"0\\n3\\n1\\n0\\n\",\n\t},\n\t{\n\t\t`foo() { for a in $*; do echo \"$a\"; done }; foo 'a  1' 'b  2'`,\n\t\t\"a\\n1\\nb\\n2\\n\",\n\t},\n\t{\n\t\t`foo() { for a in \"$*\"; do echo \"$a\"; done }; foo 'a  1' 'b  2'`,\n\t\t\"a  1 b  2\\n\",\n\t},\n\t{\n\t\t`foo() { for a in \"foo$*\"; do echo \"$a\"; done }; foo 'a  1' 'b  2'`,\n\t\t\"fooa  1 b  2\\n\",\n\t},\n\t{\n\t\t`foo() { for a in $@; do echo \"$a\"; done }; foo 'a  1' 'b  2'`,\n\t\t\"a\\n1\\nb\\n2\\n\",\n\t},\n\t{\n\t\t`foo() { for a in \"$@\"; do echo \"$a\"; done }; foo 'a  1' 'b  2'`,\n\t\t\"a  1\\nb  2\\n\",\n\t},\n\n\t// alias (note the input newlines)\n\t{\n\t\t\"alias foo; alias foo=echo; alias foo; alias foo=; alias foo\",\n\t\t\"alias: \\\"foo\\\" not found\\nalias foo='echo'\\nalias foo=''\\n #IGNORE\",\n\t},\n\t{\n\t\t\"shopt -s expand_aliases; alias foo=echo\\nfoo foo; foo bar\",\n\t\t\"foo\\nbar\\n\",\n\t},\n\t{\n\t\t\"shopt -s expand_aliases; alias true=echo\\ntrue foo; unalias true\\ntrue bar\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"shopt -s expand_aliases; alias echo='echo a'\\necho b c\",\n\t\t\"a b c\\n\",\n\t},\n\t{\n\t\t\"shopt -s expand_aliases; alias foo='echo '\\nfoo foo; foo bar\",\n\t\t\"echo\\nbar\\n\",\n\t},\n\n\t// case\n\t{\n\t\t\"case b in x) echo foo ;; a|b) echo bar ;; esac\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"case b in x) echo foo ;; y|z) echo bar ;; esac\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"case foo in bar) echo foo ;; *) echo bar ;; esac\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"case foo in *o*) echo bar ;; esac\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"case foo in '*') echo x ;; f*) echo y ;; esac\",\n\t\t\"y\\n\",\n\t},\n\n\t// exec\n\t{\n\t\t\"$GOSH_PROG 'echo foo'\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"$GOSH_PROG 'echo foo >&2' >/dev/null\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"echo foo | $GOSH_PROG 'cat >&2' >/dev/null\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"$GOSH_PROG 'exit 1'\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"exec >/dev/null; echo foo\",\n\t\t\"\",\n\t},\n\n\t// return\n\t{\"return\", \"return: can only be done from a func or sourced script\\nexit status 1 #JUSTERR\"},\n\t{\"f() { return; }; f\", \"\"},\n\t{\"f() { return 2; }; f\", \"exit status 2\"},\n\t{\"f() { echo foo; return; echo bar; }; f\", \"foo\\n\"},\n\t{\"f1() { :; }; f2() { f1; return; }; f2\", \"\"},\n\t{\"echo 'return' >a; source ./a\", \"\"},\n\t{\"echo 'return' >a; source ./a; return\", \"return: can only be done from a func or sourced script\\nexit status 1 #JUSTERR\"},\n\t{\"echo 'return 2' >a; source ./a\", \"exit status 2\"},\n\t{\"echo 'echo foo; return; echo bar' >a; source ./a\", \"foo\\n\"},\n\n\t// command\n\t{\"command\", \"\"},\n\t{\"command -o echo\", \"command: invalid option \\\"-o\\\"\\nexit status 2 #JUSTERR\"},\n\t{\"command -vo echo\", \"command: invalid option \\\"-o\\\"\\nexit status 2 #JUSTERR\"},\n\t{\"echo() { :; }; echo foo\", \"\"},\n\t{\"echo() { :; }; command echo foo\", \"foo\\n\"},\n\t{\"command -v does-not-exist\", \"exit status 1\"},\n\t{\"foo() { :; }; command -v foo\", \"foo\\n\"},\n\t{\"foo() { :; }; command -v does-not-exist foo\", \"foo\\n\"},\n\t{\"command -v echo\", \"echo\\n\"},\n\t{\"[[ $(command -v $PATH_PROG) == $PATH_PROG ]]\", \"exit status 1\"},\n\n\t// cmd substitution\n\t{\n\t\t\"echo foo $(printf bar)\",\n\t\t\"foo bar\\n\",\n\t},\n\t{\n\t\t\"echo foo $(echo bar)\",\n\t\t\"foo bar\\n\",\n\t},\n\t{\n\t\t\"$(echo echo foo bar)\",\n\t\t\"foo bar\\n\",\n\t},\n\t{\n\t\t\"for i in 1 $(echo 2 3) 4; do echo $i; done\",\n\t\t\"1\\n2\\n3\\n4\\n\",\n\t},\n\t{\n\t\t\"echo 1$(echo 2 3)4\",\n\t\t\"12 34\\n\",\n\t},\n\t{\n\t\t`mkdir d; [[ $(cd d && pwd) == \"$(pwd)\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"a=sub true & { a=main $ENV_PROG | grep '^a='; }\",\n\t\t\"a=main\\n\",\n\t},\n\t{\n\t\t\"echo foo >f; echo $(cat f); echo $(<f)\",\n\t\t\"foo\\nfoo\\n\",\n\t},\n\t{\n\t\t\"echo foo >f; echo $(<f; echo bar)\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"$(false); echo $?; $(exit 3); echo $?; $(true); echo $?\",\n\t\t\"1\\n3\\n0\\n\",\n\t},\n\t{\n\t\t\"foo=$(false); echo $?; echo foo $(false); echo $?\",\n\t\t\"1\\nfoo\\n0\\n\",\n\t},\n\t{\n\t\t\"$(false) $(true); echo $?; $(true) $(false); echo $?\",\n\t\t\"0\\n1\\n\",\n\t},\n\t{\n\t\t\"foo=$(false) $(true); echo $?; foo=$(true) $(false); echo $?\",\n\t\t\"1\\n0\\n\",\n\t},\n\n\t// pipes\n\t{\n\t\t\"echo foo | sed 's/o/a/g'\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t\"echo foo | false | true\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"true $(true) | true\", // used to panic\n\t\t\"\",\n\t},\n\t{\n\t\t// The first command in the block used to consume stdin, even\n\t\t// though it shouldn't be. We just want to run any arbitrary\n\t\t// non-builtin program that doesn't consume stdin.\n\t\t\"echo foo | { $ENV_PROG >/dev/null; cat; }\",\n\t\t\"foo\\n\",\n\t},\n\n\t// redirects\n\t{\n\t\t\"echo foo >&1 | sed 's/o/a/g'\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t\"echo foo >&2 | sed 's/o/a/g'\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t// TODO: why does bash need a block here?\n\t\t\"{ echo foo >&2; } |& sed 's/o/a/g'\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t\"echo foo >/dev/null; echo bar\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\">a; echo foo >>b; wc -c <a >>b; cat b | tr -d ' '\",\n\t\t\"foo\\n0\\n\",\n\t},\n\t{\n\t\t\"echo foo >a; <a\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"echo foo >a; mkdir b; cd b; cat <../a\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"echo foo >a; wc -c <a | tr -d ' '\",\n\t\t\"4\\n\",\n\t},\n\t{\n\t\t\"echo foo >>a; echo bar &>>a; wc -c <a | tr -d ' '\",\n\t\t\"8\\n\",\n\t},\n\t{\n\t\t\"{ echo a; echo b >&2; } &>/dev/null\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"sed 's/o/a/g' <<EOF\\nfoo$foo\\nEOF\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t\"sed 's/o/a/g' <<'EOF'\\nfoo$foo\\nEOF\",\n\t\t\"faa$faa\\n\",\n\t},\n\t{\n\t\t\"sed 's/o/a/g' <<EOF\\n\\tfoo\\nEOF\",\n\t\t\"\\tfaa\\n\",\n\t},\n\t{\n\t\t\"sed 's/o/a/g' <<EOF\\nfoo\\nEOF\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t\"cat <<EOF\\n~/foo\\nEOF\",\n\t\t\"~/foo\\n\",\n\t},\n\t{\n\t\t\"sed 's/o/a/g' <<<foo$foo\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t\"cat <<-EOF\\n\\tfoo\\nEOF\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"cat <<-EOF\\n\\tfoo\\n\\nEOF\",\n\t\t\"foo\\n\\n\",\n\t},\n\t{\n\t\t\"cat <<EOF\\nfoo\\\\\\nbar\\nEOF\",\n\t\t\"foobar\\n\",\n\t},\n\t{\n\t\t\"cat <<'EOF'\\nfoo\\\\\\nbar\\nEOF\",\n\t\t\"foo\\\\\\nbar\\n\",\n\t},\n\t{\n\t\t\"cat <<EOF\\nfoo\\\\\\\"bar\\\\baz\\nEOF\",\n\t\t\"foo\\\\\\\"bar\\\\baz\\n\",\n\t},\n\t{\n\t\t\"cat <<EOF\\n \\\\\\\\ \\\\$ \\\\` \\nEOF\",\n\t\t\" \\\\ $ ` \\n\",\n\t},\n\t{\n\t\t\"mkdir a; echo foo >a |& grep -q 'is a directory'\",\n\t\t\" #IGNORE bash prints a warning\",\n\t},\n\t{\n\t\t\"echo foo 1>&1 | sed 's/o/a/g'\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t\"echo foo 2>&2 |& sed 's/o/a/g'\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t\"printf 2>&1 | sed 's/.*usage.*/foo/'\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"mkdir a && cd a && echo foo >b && cd .. && cat a/b\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"echo foo 2>&-; :\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t// `>&-` closes stdout or stderr. Note that any writes result in errors.\n\t\t\"echo foo >&- 2>&-; :\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"echo foo | sed $(read line 2>/dev/null; echo 's/o/a/g')\",\n\t\t\"\",\n\t},\n\t{\n\t\t// `<&-` closes stdin, to e.g. ensure that a subshell does not consume\n\t\t// the standard input shared with the parent shell.\n\t\t// Note that any reads result in errors.\n\t\t\"echo foo | sed $(exec <&-; read line 2>/dev/null; echo 's/o/a/g')\",\n\t\t\"faa\\n\",\n\t},\n\t{\n\t\t// Concurrent pipe commands used to cause races when modifying the environment.\n\t\t\"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 :\",\n\t\t\"\",\n\t},\n\n\t// background/wait\n\t{\"wait\", \"\"},\n\t{\"wait foo\", \"wait: pid foo is not a child of this shell\\nexit status 1 #JUSTERR\"},\n\t{\"{ true; } & wait\", \"\"},\n\t{\"{ false; } & wait\", \"\"},\n\t{\"{ sleep 0.01; true; } & wait\", \"\"},\n\t{\"{ sleep 0.01; false; } & wait\", \"\"},\n\t{\n\t\t\"{ echo foo; } & wait; echo bar\",\n\t\t\"foo\\nbar\\n\",\n\t},\n\t{\n\t\t\"{ echo foo & wait; } & wait; echo bar\",\n\t\t\"foo\\nbar\\n\",\n\t},\n\t{`mkdir d; old=$PWD; cd d & wait; [[ $old == \"$PWD\" ]]`, \"\"},\n\t{\n\t\t\"f() { echo 1; }; { sleep 0.01; f; } & f() { echo 2; }; wait\",\n\t\t\"1\\n\",\n\t},\n\t{\"[[ -n $! ]]\", \"exit status 1\"},\n\t{\"true & [[ -n $! ]]\", \"\"},\n\t{\"true & true;  [[ -n $! ]]\", \"\"},\n\t{\"true & pid=$!; wait $pid\", \"\"},\n\t{\"false & pid=$!; wait $pid\", \"exit status 1\"},\n\t{\"{ sleep 0.01; true; } & pid=$!; wait $pid\", \"\"},\n\t{\"{ sleep 0.01; false; } & pid=$!; wait $pid\", \"exit status 1\"},\n\t{\"(true) & ok=$!; (false) & fail=$!; wait $ok $fail\", \"exit status 1\"},\n\t{\"(true) & ok=$!; (false) & ignore=$!; wait $ok\", \"\"},\n\t{\"echo foo | true | false & wait $!\", \"exit status 1\"},\n\t{\"echo foo | false | true & wait $!\", \"\"},\n\t{\"f() { false & true; }; f; wait $!\", \"exit status 1\"},\n\t// The parent and child shells should not cause data races when setting env vars.\n\t// Note that we can't use `echo $var`, as it seems to write newlines separately,\n\t// which can cause them to get mixed up between concurrent subshells.\n\t{\n\t\t\"{ for n in {0..9}; do { echo -n $n$'\\n'; } & done; wait; } | sort\",\n\t\t\"0\\n1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9\\n\",\n\t},\n\t{\n\t\t\"outer=val; for n in {0..9}; do { echo -n $outer$'\\n'; } & outer=val; done; wait\",\n\t\t\"val\\nval\\nval\\nval\\nval\\nval\\nval\\nval\\nval\\nval\\n\",\n\t},\n\t{\n\t\t\"for n in {0..9}; do { inner=val; } & echo $inner; done\",\n\t\t\"\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\",\n\t},\n\t{\n\t\t\"exit 2 & bg1=$!; exit 0 & bg2=$!; wait $bg1 $bg2; echo $?\",\n\t\t\"0\\n\",\n\t},\n\t{\n\t\t\"exit 2 & bg1=$!; exit 4 & bg2=$!; wait $bg1 $bg2; echo $?\",\n\t\t\"4\\n\",\n\t},\n\n\t// bash test\n\t{\n\t\t\"[[ a ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ '' ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ '' ]]; [[ a ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ ! (a == b) ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ a != b ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ a && '' ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ a || '' ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ a > 3 ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ a < 3 ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ 3 == 03 ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ a -eq b ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ 3 -eq 03 ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ 3 -ne 4 ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ 3 -le 4 ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ 3 -ge 4 ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ 3 -ge 3 ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ 3 -lt 4 ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ ' 3' -lt '4 ' ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ 3 -gt 4 ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ 3 -gt 3 ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ a -nt a || a -ot a ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"touch -t 202111050000.30 a b; [[ a -nt b || a -ot b ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"touch -t 202111050200.00 a; touch -t 202111060100.00 b; [[ a -nt b ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"touch -t 202111050000.00 a; touch -t 202111060000.00 b; [[ a -ot b ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ a -ef b ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\">a >b; [[ a -ef b ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\">a; [[ a -ef a ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\">a; ln a b; [[ a -ef b ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\">a; ln -s a b; [[ a -ef b ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ -z 'foo' || -n '' ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ -z '' && -n 'foo' ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"a=x b=''; [[ -v a && -v b && ! -v c ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ abc == *b* ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ abc != *b* ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ *b = '*b' ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ ab == a. ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`x='*b*'; [[ abc == $x ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`x='*b*'; [[ abc == \"$x\" ]]`,\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`[[ abc == \\a\\bc ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ abc != *b'*' ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ a =~ b ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"[[ foo =~ foo && foo =~ .* && foo =~ f.o ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ foo =~ oo ]] && echo foo; [[ foo =~ ^oo$ ]] && echo bar || true\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"[[ a =~ [ ]]\",\n\t\t\"exit status 2\",\n\t},\n\t{\n\t\t\"[[ a__b__c =~ _*(b_*) ]]; echo ${BASH_REMATCH[0]}; echo ${BASH_REMATCH[1]}\",\n\t\t\"__b__\\nb__\\n\",\n\t},\n\t{\n\t\t\"[[ -e a ]] && echo x; >a; [[ -e a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"ln -s b a; [[ -e a ]] && echo x; >b; [[ -e a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -f a ]] && echo x; >a; [[ -f a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -e a ]] && echo x; mkdir a; [[ -e a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -d a ]] && echo x; mkdir a; [[ -d a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -r a ]] && echo x; >a; [[ -r a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -w a ]] && echo x; >a; [[ -w a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -s a ]] && echo x; echo body >a; [[ -s a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -L a ]] && echo x; ln -s b a; [[ -L a ]] && echo y;\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ \\\"multiline\\ntext\\\" == *text* ]] && echo x; [[ \\\"multiline\\ntext\\\" == *multiline* ]] && echo y\",\n\t\t\"x\\ny\\n\",\n\t},\n\t// * should match a newline\n\t{\n\t\t\"[[ \\\"multiline\\ntext\\\" == multiline*text ]] && echo x\",\n\t\t\"x\\n\",\n\t},\n\t{\n\t\t\"[[ \\\"multiline\\ntext\\\" == text ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`case $'a\\nb' in a*b) echo match ;; esac`,\n\t\t\"match\\n\",\n\t},\n\t{\n\t\t`a=$'a\\nb'; echo \"${a/a*b/sub}\"`,\n\t\t\"sub\\n\",\n\t},\n\t{\n\t\t\"mkdir a; cd a; test -f b && echo x; >b; test -f b && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\">a; [[ -b a ]] && echo block; [[ -c a ]] && echo char; true\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"[[ -e /dev/sda ]] || { echo block; exit; }; [[ -b /dev/sda ]] && echo block; [[ -c /dev/sda ]] && echo char; true\",\n\t\t\"block\\n\",\n\t},\n\t{\n\t\t\"[[ -e /dev/nvme0n1 ]] || { echo block; exit; }; [[ -b /dev/nvme0n1 ]] && echo block; [[ -c /dev/nvme0n1 ]] && echo char; true\",\n\t\t\"block\\n\",\n\t},\n\t{\n\t\t\"[[ -e /dev/tty ]] || { echo char; exit; }; [[ -b /dev/tty ]] && echo block; [[ -c /dev/tty ]] && echo char; true\",\n\t\t\"char\\n\",\n\t},\n\t{\"[[ -t 1 ]]\", \"exit status 1\"},\n\t{\"[[ -t 1234 ]]\", \"exit status 1\"},\n\t{\"[[ -o wrong ]]\", \"exit status 1\"},\n\t{\"[[ -o errexit ]]\", \"exit status 1\"},\n\t{\"set -e; [[ -o errexit ]]\", \"\"},\n\t{\"[[ -o noglob ]]\", \"exit status 1\"},\n\t{\"set -f; [[ -o noglob ]]\", \"\"},\n\t{\"[[ -o allexport ]]\", \"exit status 1\"},\n\t{\"set -a; [[ -o allexport ]]\", \"\"},\n\t{\"[[ -o nounset ]]\", \"exit status 1\"},\n\t{\"set -u; [[ -o nounset ]]\", \"\"},\n\t{\"[[ -o noexec ]]\", \"exit status 1\"},\n\t{\"set -n; [[ -o noexec ]]\", \"\"}, // actually does nothing, but oh well\n\t{\"[[ -o pipefail ]]\", \"exit status 1\"},\n\t{\"set -o pipefail; [[ -o pipefail ]]\", \"\"},\n\t// TODO: we don't implement precedence of && over ||.\n\t// {\"[[ a == x && b == x || c == c ]]\", \"\"},\n\t{\"[[ (a == x && b == x) || c == c ]]\", \"\"},\n\t{\"[[ a == x && (b == x || c == c) ]]\", \"exit status 1\"},\n\n\t// classic test\n\t{\n\t\t\"[\",\n\t\t\"1:1: [: missing matching ]\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"[ a\",\n\t\t\"1:1: [: missing matching ]\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"[ a b c ]\",\n\t\t\"1:1: not a valid test operator: `b`\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"[ a -a ]\",\n\t\t\"1:1: -a must be followed by an expression\\nexit status 2 #JUSTERR\",\n\t},\n\t{\"[ a ]\", \"\"},\n\t{\"[ -n ]\", \"\"},\n\t{\"[ '-n' ]\", \"\"},\n\t{\"[ -z ]\", \"\"},\n\t{\"[ ! ]\", \"\"},\n\t{\"[ a != b ]\", \"\"},\n\t{\"[ ! a '==' a ]\", \"exit status 1\"},\n\t{\"[ a -a 0 -gt 1 ]\", \"exit status 1\"},\n\t{\"[ 0 -gt 1 -o 1 -gt 0 ]\", \"\"},\n\t{\"[ 3 -gt 4 ]\", \"exit status 1\"},\n\t{\"[ 3 -lt 4 ]\", \"\"},\n\t{\"[ ' 3' -lt '4 ' ]\", \"\"},\n\t{\n\t\t\"[ -e a ] && echo x; >a; [ -e a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"test 3 -gt 4\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"test 3 -lt 4\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"test 3 -lt\",\n\t\t\"1:1: -lt must be followed by a word\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"touch -t 202111050000.00 a; touch -t 202111060000.00 b; [ a -nt b ]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"touch -t 202111050000.00 a; touch -t 202111060000.00 b; [ a -ot b ]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\">a; [ a -ef a ]\",\n\t\t\"\",\n\t},\n\t{\"[ 3 -eq 04 ]\", \"exit status 1\"},\n\t{\"[ 3 -eq 03 ]\", \"\"},\n\t{\"[ 3 -ne 03 ]\", \"exit status 1\"},\n\t{\"[ 3 -le 4 ]\", \"\"},\n\t{\"[ 3 -ge 4 ]\", \"exit status 1\"},\n\t{\n\t\t\"[ -d a ] && echo x; mkdir a; [ -d a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[ -r a ] && echo x; >a; [ -r a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[ -w a ] && echo x; >a; [ -w a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t// A directory is readable, writable, and executable.\n\t\t\"mkdir d; [ -r d ] && echo r; [ -w d ] && echo w; [ -x d ] && echo x\",\n\t\t\"r\\nw\\nx\\n\",\n\t},\n\t{\n\t\t\"[ -s a ] && echo x; echo body >a; [ -s a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[ -L a ] && echo x; ln -s b a; [ -L a ] && echo y;\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\">a; [ -b a ] && echo block; [ -c a ] && echo char; true\",\n\t\t\"\",\n\t},\n\t{\"[ -t 1 ]\", \"exit status 1\"},\n\t{\"[ -t 1234 ]\", \"exit status 1\"},\n\t{\"[ -o wrong ]\", \"exit status 1\"},\n\t{\"[ -o errexit ]\", \"exit status 1\"},\n\t{\"set -e; [ -o errexit ]\", \"\"},\n\t{\"a=x b=''; [ -v a -a -v b -a ! -v c ]\", \"\"},\n\t{\"[ a = a ]\", \"\"},\n\t{\"[ a != a ]\", \"exit status 1\"},\n\t{\"[ abc = ab* ]\", \"exit status 1\"},\n\t{\"[ abc != ab* ]\", \"\"},\n\t// TODO: we don't implement precedence of -a over -o.\n\t// {\"[ a = x -a b = x -o c = c ]\", \"\"},\n\t{`[ \\( a = x -a b = x \\) -o c = c ]`, \"\"},\n\t{`[ a = x -a \\( b = x -o c = c \\) ]`, \"exit status 1\"},\n\n\t// arithm\n\t{\n\t\t\"echo $((1 == +1))\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"echo $((!0))\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"echo $((!3))\",\n\t\t\"0\\n\",\n\t},\n\t{\n\t\t\"echo $((~0))\",\n\t\t\"-1\\n\",\n\t},\n\t{\n\t\t\"echo $((~3))\",\n\t\t\"-4\\n\",\n\t},\n\t{\n\t\t\"echo $((1 + 2 - 3))\",\n\t\t\"0\\n\",\n\t},\n\t{\n\t\t\"echo $((-1 * 6 / 2))\",\n\t\t\"-3\\n\",\n\t},\n\t{\n\t\t\"a=2; echo $(( a + $a + c ))\",\n\t\t\"4\\n\",\n\t},\n\t{\n\t\t\"a=b; b=c; c=5; echo $((a % 3))\",\n\t\t\"2\\n\",\n\t},\n\t{\n\t\t\"echo $((2 > 2 || 2 < 2))\",\n\t\t\"0\\n\",\n\t},\n\t{\n\t\t\"echo $((2 >= 2 && 2 <= 2))\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"echo $(((1 & 2) != (1 | 2)))\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"echo $a; echo $((a = 3 ^ 2)); echo $a\",\n\t\t\"\\n1\\n1\\n\",\n\t},\n\t{\n\t\t\"echo $((a += 1, a *= 2, a <<= 2, a >> 1))\",\n\t\t\"4\\n\",\n\t},\n\t{\n\t\t\"echo $((a -= 10, a /= 2, a >>= 1, a << 1))\",\n\t\t\"-6\\n\",\n\t},\n\t{\n\t\t\"echo $((a |= 3, a &= 1, a ^= 8, a %= 5, a))\",\n\t\t\"4\\n\",\n\t},\n\t{\n\t\t\"echo $((a = 3, ++a, a--))\",\n\t\t\"4\\n\",\n\t},\n\t{\n\t\t\"echo $((2 ** 3)) $((1234 ** 4567))\",\n\t\t\"8 0\\n\",\n\t},\n\t{\n\t\t\"echo $((1 ? 2 : 3)) $((0 ? 2 : 3))\",\n\t\t\"2 3\\n\",\n\t},\n\t{\n\t\t\"((1))\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"((3 == 4))\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"let i=(3+4); let i++; echo $i; let i--; echo $i\",\n\t\t\"8\\n7\\n\",\n\t},\n\t{\n\t\t\"let 3==4\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"a=1; let a++; echo $a\",\n\t\t\"2\\n\",\n\t},\n\t{\n\t\t\"a=$((1 + 2)); echo $a\",\n\t\t\"3\\n\",\n\t},\n\t{\n\t\t\"x=3; echo $(($x)) $((x))\",\n\t\t\"3 3\\n\",\n\t},\n\t{\n\t\t\"set -- 1; echo $(($@))\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"a=b b=a; echo $(($a))\",\n\t\t\"0\\n #IGNORE bash prints a warning\",\n\t},\n\t{\n\t\t\"let x=3; let 3/0; ((3/0)); echo $((x/y)); let x/=0\",\n\t\t\"division by zero\\ndivision by zero\\ndivision by zero\\ndivision by zero\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"let x=3; let 3%0; ((3%0)); echo $((x%y)); let x%=0\",\n\t\t\"division by zero\\ndivision by zero\\ndivision by zero\\ndivision by zero\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"let x=' 3'; echo $x\",\n\t\t\"3\\n\",\n\t},\n\t{\n\t\t\"x=' 3'; let x++; echo \\\"$x\\\"\",\n\t\t\"4\\n\",\n\t},\n\n\t// set/shift\n\t{\n\t\t\"echo $#; set foo bar; echo $#\",\n\t\t\"0\\n2\\n\",\n\t},\n\t{\n\t\t\"shift; set a b c; shift; echo $@\",\n\t\t\"b c\\n\",\n\t},\n\t{\n\t\t\"shift 2; set a b c; shift 2; echo $@\",\n\t\t\"c\\n\",\n\t},\n\t{\n\t\t`echo $#; set '' \"\"; echo $#`,\n\t\t\"0\\n2\\n\",\n\t},\n\t{\n\t\t\"set -- a b; echo $#\",\n\t\t\"2\\n\",\n\t},\n\t{\n\t\t\"set -U\",\n\t\t\"set: invalid option: \\\"-U\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"set -e; false; echo foo\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"set -e; shouldnotexist; echo foo\",\n\t\t\"\\\"shouldnotexist\\\": executable file not found in $PATH\\nexit status 127 #JUSTERR\",\n\t},\n\t{\n\t\t\"set -e; set +e; false; echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"set -e; ! false; echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"set -e; ! true; echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"set -e; if false; then echo never; fi; echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"set -e; while false; do echo never; done; echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"set -e; false || true; echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"set -e; false && true; echo foo\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"set -e; true && false; echo foo\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"false | :\",\n\t\t\"\",\n\t},\n\t{\n\t\t// Important that we don't print in these, as otherwise we get \"broken pipe\" errors.\n\t\t\"GOSH_CMD=exit_5 $GOSH_PROG | GOSH_CMD=exit_0 $GOSH_PROG\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"set -o pipefail; false | :\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"set -o pipefail; GOSH_CMD=exit_5 $GOSH_PROG | GOSH_CMD=exit_0 $GOSH_PROG\",\n\t\t\"exit status 5\",\n\t},\n\t{\n\t\t\"set -o pipefail; true | false | true | :\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"set -o pipefail; set -M 2>/dev/null | false\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"set -o pipefail; false | :; echo next\",\n\t\t\"next\\n\",\n\t},\n\t{\n\t\t\"set -o pipefail; exit 0 | :; echo next\",\n\t\t\"next\\n\",\n\t},\n\t{\n\t\t\"set -o pipefail; exit 1 | :; echo next\",\n\t\t\"next\\n\",\n\t},\n\t{\n\t\t\"set -e -o pipefail; false | :; echo next\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"exit 0 && true; echo foo\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"exit 1 && true; echo foo\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"set -f; >a.x; echo *.x;\",\n\t\t\"*.x\\n\",\n\t},\n\t{\n\t\t\"set -f; set +f; >a.x; echo *.x;\",\n\t\t\"a.x\\n\",\n\t},\n\t{\n\t\t\"set -a; foo=bar; $ENV_PROG | grep ^foo=\",\n\t\t\"foo=bar\\n\",\n\t},\n\t{\n\t\t\"set -a; foo=(b a r); $ENV_PROG | grep ^foo=\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"foo=bar; set -a; $ENV_PROG | grep ^foo=\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"a=b; echo $a; set -u; echo $a\",\n\t\t\"b\\nb\\n\",\n\t},\n\t{\n\t\t\"echo $a; set -u; echo $a; echo extra\",\n\t\t\"\\na: unbound variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"foo=bar; set -u; echo ${foo/bar/}\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"foo=bar; set -u; echo ${foo#bar}\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo/bar/}\",\n\t\t\"foo: unbound variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo#bar}\",\n\t\t\"foo: unbound variable\\nexit status 1 #JUSTERR\",\n\t},\n\t// TODO: detect this case as unset\n\t// {\n\t// \t\"set -u; foo=(bar); echo $foo; echo ${foo[3]}\",\n\t// \t\"bar\\nfoo: unbound variable\\nexit status 1 #JUSTERR\",\n\t// },\n\t{\n\t\t\"set -u; foo=(''); echo ${foo[0]}\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${#foo}\",\n\t\t\"foo: unbound variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo+bar}\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo:+bar}\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo-bar}\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo:-bar}\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo=bar}\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo:=bar}\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo?bar}\",\n\t\t\"foo: bar\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"set -u; echo ${foo:?bar}\",\n\t\t\"foo: bar\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"set -ue; set -ueo pipefail\",\n\t\t\"\",\n\t},\n\t{\"set -n; echo foo\", \"\"},\n\t{\"set -n; [ wrong\", \"\"},\n\t{\"set -n; set +n; echo foo\", \"\"},\n\t{\n\t\t\"set -o foobar\",\n\t\t\"set: invalid option: \\\"foobar\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\"set -o noexec; echo foo\", \"\"},\n\t{\"set +o noexec; echo foo\", \"foo\\n\"},\n\t{\"set -e; set -o | grep -E 'errexit|noexec' | wc -l | tr -d ' '\", \"2\\n\"},\n\t{\"set -e; set -o | grep -E 'errexit|noexec' | grep 'on$' | wc -l | tr -d ' '\", \"1\\n\"},\n\t{\n\t\t\"set -a; set +o\",\n\t\t`set -o allexport\nset +o errexit\nset +o noexec\nset +o noglob\nset +o nounset\nset +o xtrace\nset +o pipefail\n #IGNORE`,\n\t},\n\t{`set - foobar; echo $@; set -; echo $@`, \"foobar\\nfoobar\\n\"},\n\n\t// unset\n\t{\n\t\t\"a=1; echo $a; unset a; echo $a\",\n\t\t\"1\\n\\n\",\n\t},\n\t{\n\t\t\"notinpath() { echo func; }; notinpath; unset -f notinpath; notinpath\",\n\t\t\"func\\n\\\"notinpath\\\": executable file not found in $PATH\\nexit status 127 #JUSTERR\",\n\t},\n\t{\n\t\t\"a=1; a() { echo func; }; unset -f a; echo $a\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"a=1; a() { echo func; }; unset -v a; a; echo $a\",\n\t\t\"func\\n\\n\",\n\t},\n\t{\n\t\t\"notinpath=1; notinpath() { echo func; }; notinpath; echo $notinpath; unset notinpath; notinpath; echo $notinpath; unset notinpath; notinpath\",\n\t\t\"func\\n1\\nfunc\\n\\n\\\"notinpath\\\": executable file not found in $PATH\\nexit status 127 #JUSTERR\",\n\t},\n\t{\n\t\t\"unset PATH; [[ $PATH == '' ]]\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"readonly a=1; echo $a; unset a; echo $a\",\n\t\t\"1\\na: readonly variable\\n1\\n #IGNORE bash prints a warning\",\n\t},\n\t{\n\t\t\"f() { local a=1; echo $a; unset a; echo $a; }; f\",\n\t\t\"1\\n\\n\",\n\t},\n\t{\n\t\t`a=b eval 'echo $a; unset a; echo $a'`,\n\t\t\"b\\n\\n\",\n\t},\n\t{\n\t\t`$(unset INTERP_GLOBAL); echo $INTERP_GLOBAL; unset INTERP_GLOBAL; echo $INTERP_GLOBAL`,\n\t\t\"value\\n\\n\",\n\t},\n\t{\n\t\t`x=orig; f() { local x=local; unset x; x=still_local; }; f; echo $x`,\n\t\t\"orig\\n\",\n\t},\n\t{\n\t\t`x=orig; f() { local x=local; unset x; [[ -v x ]] && echo set || echo unset; }; f`,\n\t\t\"unset\\n\",\n\t},\n\t{\n\t\t`PS3=\"pick one: \"; select opt in foo bar baz; do echo \"Selected $opt\"; break; done <<< 3`,\n\t\t\"1) foo\\n2) bar\\n3) baz\\npick one: Selected baz\\n\",\n\t},\n\t{\n\t\t`opts=(foo bar baz); select opt in ${opts[@]}; do echo \"Selected $opt\"; break; done <<< 99`,\n\t\t\"1) foo\\n2) bar\\n3) baz\\n#? Selected \\n\",\n\t},\n\t{\n\t\t`select opt in foo; do\n\tcase $opt in\n\tfoo) echo \"option 1\"; break;;\n\t*) echo \"invalid option $REPLY\"; break;;\n\tesac\ndone <<< 2`,\n\t\t\"1) foo\\n#? invalid option 2\\n\",\n\t},\n\n\t// shopt\n\t{\"set -e; shopt -o | grep -E '^(errexit|noexec)' | wc -l | tr -d ' '\", \"2\\n\"},\n\t{\"set -e; shopt -o | grep -E '^(errexit|noexec)' | grep 'on$' | wc -l | tr -d ' '\", \"1\\n\"},\n\t{\"set -e; shopt | grep -E '^(errexit|noexec)' | wc -l | tr -d ' '\", \"0\\n\"},\n\t{\"shopt -s -o noexec; echo foo\", \"\"},\n\t{\"shopt -so noexec; echo foo\", \"\"},\n\t{\"shopt -u -o noexec; echo foo\", \"foo\\n\"},\n\t{\"shopt -u globstar; shopt globstar | grep 'off$' | wc -l | tr -d ' '\", \"1\\n\"},\n\t{\"shopt -s globstar; shopt globstar | grep 'off$' | wc -l | tr -d ' '\", \"0\\n\"},\n\t{\"shopt extglob | grep 'off' | wc -l | tr -d ' '\", \"1\\n\"},\n\t{\n\t\t\"shopt inherit_errexit\",\n\t\t\"inherit_errexit\\ton\\t(\\\"off\\\" not supported)\\n #JUSTERR\",\n\t},\n\t{\n\t\t\"shopt -o -s pipefail; shopt -o pipefail | grep -q 'on$'\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"shopt -o -u pipefail; shopt -o pipefail | grep -q 'on$'\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"shopt pipefail\",\n\t\t\"shopt: invalid option name \\\"pipefail\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"shopt -s pipefail\",\n\t\t\"shopt: invalid option name \\\"pipefail\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"shopt -o -s extglob\",\n\t\t\"shopt: invalid option name \\\"extglob\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"shopt -s login_shell\",\n\t\t\"shopt: unsupported option \\\"login_shell\\\"\\nexit status 1 #IGNORE\",\n\t},\n\t{\n\t\t\"shopt -s interactive_comments\",\n\t\t\"shopt: unsupported option \\\"interactive_comments\\\"\\nexit status 1 #IGNORE\",\n\t},\n\t{\n\t\t\"shopt -s nosuchname\",\n\t\t\"shopt: invalid option name \\\"nosuchname\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"shopt -o -s nosuchname\",\n\t\t\"shopt: invalid option name \\\"nosuchname\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"touch a .b ..c; shopt -u dotglob; echo *\",\n\t\t\"a\\n\",\n\t},\n\t{\n\t\t\"touch a .b ..c; shopt -s dotglob; echo *\",\n\t\t\"..c .b a\\n\",\n\t},\n\t{\n\t\t\"mkdir sub .sub2; touch {sub,.sub2}/{a,.b}; shopt -s globstar; shopt -u dotglob; echo **/* | sed 's@\\\\\\\\@/@g'\",\n\t\t\"sub sub/a\\n\",\n\t},\n\t{\n\t\t\"mkdir sub .sub2; touch {sub,.sub2}/{a,.b}; shopt -s globstar; shopt -s dotglob; echo **/* | sed 's@\\\\\\\\@/@g'\",\n\t\t\".sub2 .sub2/.b .sub2/a sub sub/.b sub/a\\n\",\n\t},\n\t{\n\t\t// Beware that macOS file systems are by default case-preserving but\n\t\t// case-insensitive, so e.g. \"touch x X\" creates only one file.\n\t\t\"touch a ab Ac Ad; shopt -u nocaseglob; echo a*\",\n\t\t\"a ab\\n\",\n\t},\n\t{\n\t\t\"touch a ab Ac Ad; shopt -s nocaseglob; echo a*\",\n\t\t\"Ac Ad a ab\\n\",\n\t},\n\t{\n\t\t\"touch a ab abB Ac Ad; shopt -u nocaseglob; echo *b\",\n\t\t\"ab\\n\",\n\t},\n\t{\n\t\t\"touch a ab abB Ac Ad; shopt -s nocaseglob; echo *b\",\n\t\t\"ab abB\\n\",\n\t},\n\n\t// IFS\n\t{`echo -n \"$IFS\"`, \" \\t\\n\"},\n\t{`a=\"x:y:z\"; IFS=:; echo $a`, \"x y z\\n\"},\n\t{`a=(x y z); IFS=-; echo ${a[*]}`, \"x y z\\n\"},\n\t{`a=(x y z); IFS=-; echo ${a[@]}`, \"x y z\\n\"},\n\t{`a=(x y z); IFS=-; echo \"${a[*]}\"`, \"x-y-z\\n\"},\n\t{`a=(x y z); IFS=-; echo \"${a[@]}\"`, \"x y z\\n\"},\n\t{`a=\"  x y z\"; IFS=; echo $a`, \"  x y z\\n\"},\n\t{`a=(x y z); IFS=; echo \"${a[*]}\"`, \"xyz\\n\"},\n\t{`a=(x y z); IFS=-; echo \"${!a[@]}\"`, \"0 1 2\\n\"},\n\t{`set -- x y z; IFS=-; echo $*`, \"x y z\\n\"},\n\t{`set -- x y z; IFS=-; echo \"$*\"`, \"x-y-z\\n\"},\n\t{`set -- x y z; IFS=; echo $*`, \"x y z\\n\"},\n\t{`set -- x y z; IFS=; echo \"$*\"`, \"xyz\\n\"},\n\n\t// builtin\n\t{\"builtin\", \"\"},\n\t{\"builtin noexist\", \"exit status 1 #JUSTERR\"},\n\t{\"builtin echo foo\", \"foo\\n\"},\n\t{\n\t\t\"echo() { printf 'bar\\n'; }; echo foo; builtin echo foo\",\n\t\t\"bar\\nfoo\\n\",\n\t},\n\n\t// type\n\t{\"type\", \"\"},\n\t{\"type for\", \"for is a shell keyword\\n\"},\n\t{\"type echo\", \"echo is a shell builtin\\n\"},\n\t{\"echo() { :; }; type echo | grep 'is a function'\", \"echo is a function\\n\"},\n\t{\"type $PATH_PROG | grep -q -E ' is (/|[A-Z]:)'\", \"\"},\n\t{\"type noexist\", \"type: noexist: not found\\nexit status 1 #JUSTERR\"},\n\t{\"PATH=/; type $PATH_PROG\", \"type: \" + pathProg + \": not found\\nexit status 1 #JUSTERR\"},\n\t{\"shopt -s expand_aliases; alias interp_foo='bar baz'\\ntype interp_foo\", \"interp_foo is aliased to `bar baz'\\n\"},\n\t{\"alias interp_foo='bar baz'\\ntype interp_foo\", \"type: interp_foo: not found\\nexit status 1 #JUSTERR\"},\n\t{\"type -p $PATH_PROG | grep -q -E '^(/|[A-Z]:)'\", \"\"},\n\t{\"PATH=/; type -p $PATH_PROG\", \"exit status 1\"},\n\t// TODO: type -P should force PATH lookup even for builtins, unlike type -p.\n\t{\"type -P $PATH_PROG | grep -q -E '^(/|[A-Z]:)'\", \"\"},\n\t{\"PATH=/; type -P $PATH_PROG\", \"exit status 1\"},\n\t{\"shopt -s expand_aliases; alias interp_foo='bar'; type -t interp_foo\", \"alias\\n\"},\n\t{\"type -t case\", \"keyword\\n\"},\n\t{\"interp_foo(){ :; }; type -t interp_foo\", \"function\\n\"},\n\t{\"type -t type\", \"builtin\\n\"},\n\t{\"type -t $PATH_PROG\", \"file\\n\"},\n\t{\"type -t inexisting_dfgsdgfds\", \"exit status 1\"},\n\n\t// hash\n\t{\"hash $PATH_PROG\", \"\"},\n\n\t// trap\n\t{\"trap 'echo at_exit' EXIT; true\", \"at_exit\\n\"},\n\t{\"trap 'echo on_err' ERR; false; echo FAIL\", \"on_err\\nFAIL\\n\"},\n\t{\"trap 'echo on_err' ERR; false || true; echo OK\", \"OK\\n\"},\n\t{\"trap 'echo at_exit' EXIT; trap - EXIT; echo OK\", \"OK\\n\"},\n\t{\"set -e; trap 'echo A' ERR EXIT; false; echo FAIL\", \"A\\nA\\nexit status 1\"},\n\t{\"trap 'foobar' UNKNOWN\", \"trap: UNKNOWN: invalid signal specification\\nexit status 2 #JUSTERR\"},\n\t// TODO: our builtin appears to not receive the piped bytes?\n\t// {\"trap 'echo on_err' ERR; trap | grep -q '.*echo on_err.*'\", \"trap -- \\\"echo on_err\\\" ERR\\n\"},\n\t{\"trap 'false' ERR EXIT; false\", \"exit status 1\"},\n\n\t// eval\n\t{\"eval\", \"\"},\n\t{\"eval ''\", \"\"},\n\t{\"eval echo foo\", \"foo\\n\"},\n\t{\"eval 'echo foo'\", \"foo\\n\"},\n\t{\"eval 'exit 1'\", \"exit status 1\"},\n\t{\"eval '(x'\", \"eval: 1:1: reached EOF without matching `(` with `)`\\nexit status 1 #JUSTERR\"},\n\t{\"set a b; eval 'echo $@'\", \"a b\\n\"},\n\t{\"eval 'a=foo'; echo $a\", \"foo\\n\"},\n\t{`a=b eval \"echo $a\"`, \"\\n\"},\n\t{`a=b eval 'echo $a'`, \"b\\n\"},\n\t{`eval 'echo \"\\$a\"'`, \"$a\\n\"},\n\t{`a=b eval 'x=y eval \"echo \\$a \\$x\"'`, \"b y\\n\"},\n\t{`a=b eval 'a=y eval \"echo $a \\$a\"'`, \"b y\\n\"},\n\t{\"a=b eval '(echo $a)'\", \"b\\n\"},\n\n\t// source\n\t{\n\t\t\"source\",\n\t\t\"1:1: source: need filename\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"echo 'echo foo' >a; source ./a; . ./a\",\n\t\t\"foo\\nfoo\\n\",\n\t},\n\t{\n\t\t\"echo 'echo $@' >a; source ./a; source ./a b c; echo $@\",\n\t\t\"\\nb c\\n\\n\",\n\t},\n\t{\n\t\t\"echo 'foo=bar' >a; source ./a; echo $foo\",\n\t\t\"bar\\n\",\n\t},\n\n\t// source from PATH\n\t{\n\t\t\"mkdir test; echo 'echo foo' >test/a; PATH=$PWD/test source a; . test/a\",\n\t\t\"foo\\nfoo\\n\",\n\t},\n\n\t// source with set and shift\n\t{\n\t\t\"echo 'set -- d e f' >a; source ./a; echo $@\",\n\t\t\"d e f\\n\",\n\t},\n\t{\n\t\t\"echo 'echo $@' >a; set -- b c; source ./a; echo $@\",\n\t\t\"b c\\nb c\\n\",\n\t},\n\t{\n\t\t\"echo 'echo $@' >a; set -- b c; source ./a d e; echo $@\",\n\t\t\"d e\\nb c\\n\",\n\t},\n\t{\n\t\t\"echo 'shift; echo $@' >a; set -- b c; source ./a d e; echo $@\",\n\t\t\"e\\nb c\\n\",\n\t},\n\t{\n\t\t\"echo 'shift' >a; set -- b c; source ./a; echo $@\",\n\t\t\"c\\n\",\n\t},\n\t{\n\t\t\"echo 'shift; set -- $@' >a; set -- b c; source ./a d e; echo $@\",\n\t\t\"e\\n\",\n\t},\n\t{\n\t\t\"echo 'set -- g f'>b; echo 'set -- d e f; echo $@; source ./b;' >a; source ./a; echo $@\",\n\t\t\"d e f\\ng f\\n\",\n\t},\n\t{\n\t\t\"echo 'set -- g f'>b; echo 'echo $@; set -- d e f; source ./b;' >a; source ./a b c; echo $@\",\n\t\t\"b c\\ng f\\n\",\n\t},\n\t{\n\t\t\"echo 'shift; echo $@' >b; echo 'shift; echo $@; source ./b' >a; source ./a b c d; echo $@\",\n\t\t\"c d\\nd\\n\\n\",\n\t},\n\t{\n\t\t\"echo 'set -- b c d' >b; echo 'source ./b' >a; set -- a; source ./a; echo $@\",\n\t\t\"b c d\\n\",\n\t},\n\t{\n\t\t\"echo 'echo $@' >b; echo 'set -- b c d; source ./b' >a; set -- a; source ./a; echo $@\",\n\t\t\"b c d\\nb c d\\n\",\n\t},\n\t{\n\t\t\"echo 'shift; echo $@' >b; echo 'shift; echo $@; source ./b c d' >a; set -- a b; source ./a; echo $@\",\n\t\t\"b\\nd\\nb\\n\",\n\t},\n\t{\n\t\t\"echo 'set -- a b c' >b; echo 'echo $@; source ./b; echo $@' >a; source ./a; echo $@\",\n\t\t\"\\na b c\\na b c\\n\",\n\t},\n\n\t// indexed arrays\n\t{\n\t\t\"a=foo; echo ${a[0]} ${a[@]} ${a[x]}; echo ${a[1]}\",\n\t\t\"foo foo foo\\n\\n\",\n\t},\n\t{\n\t\t\"a=(); echo ${a[0]} ${a[@]} ${a[x]} ${a[1]}\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"a=(b c); echo $a; echo ${a[0]}; echo ${a[1]}; echo ${a[x]}\",\n\t\t\"b\\nb\\nc\\nb\\n\",\n\t},\n\t{\n\t\t\"a=(b c); echo ${a[@]}; echo ${a[*]}\",\n\t\t\"b c\\nb c\\n\",\n\t},\n\t{\n\t\t\"a=(1 2 3); echo ${a[2-1]}; echo $((a[1+1]))\",\n\t\t\"2\\n3\\n\",\n\t},\n\t{\n\t\t\"a=(1 2) x=(); a+=b x+=c; echo ${a[@]}; echo ${x[@]}\",\n\t\t\"1b 2\\nc\\n\",\n\t},\n\t{\n\t\t\"a=(1 2) x=(); a+=(b c) x+=(d e); echo ${a[@]}; echo ${x[@]}\",\n\t\t\"1 2 b c\\nd e\\n\",\n\t},\n\t{\n\t\t\"a=bbb; a+=(c d); echo ${a[@]}\",\n\t\t\"bbb c d\\n\",\n\t},\n\t{\n\t\t`a=('a  1' 'b  2'); for e in ${a[@]}; do echo \"$e\"; done`,\n\t\t\"a\\n1\\nb\\n2\\n\",\n\t},\n\t{\n\t\t`a=('a  1' 'b  2'); for e in \"${a[*]}\"; do echo \"$e\"; done`,\n\t\t\"a  1 b  2\\n\",\n\t},\n\t{\n\t\t`a=('a  1' 'b  2'); for e in \"${a[@]}\"; do echo \"$e\"; done`,\n\t\t\"a  1\\nb  2\\n\",\n\t},\n\t{\n\t\t`declare -a a; a[0]='a  1'; a[1]='b  2'; for e in \"${a[@]}\"; do echo \"$e\"; done`,\n\t\t\"a  1\\nb  2\\n\",\n\t},\n\t{\n\t\t`a=([1]=y [0]=x); echo ${a[0]}`,\n\t\t\"x\\n\",\n\t},\n\t{\n\t\t`a=(y); a[2]=x; echo ${a[2]}`,\n\t\t\"x\\n\",\n\t},\n\t{\n\t\t`a=\"y\"; a[2]=x; echo ${a[2]}`,\n\t\t\"x\\n\",\n\t},\n\t{\n\t\t`declare -a a=(x y); echo ${a[1]}`,\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t`a=b; echo \"${a[@]}\"`,\n\t\t\"b\\n\",\n\t},\n\t{\n\t\t`a=(b); echo ${a[3]}`,\n\t\t\"\\n\",\n\t},\n\t{\n\t\t`a=(b); echo ${a[-2]}`,\n\t\t\"negative array index\\n #JUSTERR\",\n\t},\n\t// TODO: also test with gaps in arrays.\n\t{\n\t\t`a=([0]=' x ' [1]=' y '); for v in \"${a[@]}\"; do echo \"$v\"; done`,\n\t\t\" x \\n y \\n\",\n\t},\n\t{\n\t\t`a=([0]=' x ' [1]=' y '); for v in \"${a[*]}\"; do echo \"$v\"; done`,\n\t\t\" x   y \\n\",\n\t},\n\t{\n\t\t`a=([0]=' x ' [1]=' y '); for v in \"${!a[@]}\"; do echo \"$v\"; done`,\n\t\t\"0\\n1\\n\",\n\t},\n\t{\n\t\t`a=([0]=' x ' [1]=' y '); for v in \"${!a[*]}\"; do echo \"$v\"; done`,\n\t\t\"0 1\\n\",\n\t},\n\n\t// associative arrays\n\t{\n\t\t`a=foo; echo ${a[\"\"]} ${a[\"x\"]}`,\n\t\t\"foo foo\\n\",\n\t},\n\t{\n\t\t`declare -A a=(); echo ${a[0]} ${a[@]} ${a[1]} ${a[\"x\"]}`,\n\t\t\"\\n\",\n\t},\n\t{\n\t\t`declare -A a=([x]=b [y]=c); echo $a; echo ${a[0]}; echo ${a[\"x\"]}; echo ${a[\"_\"]}`,\n\t\t\"\\n\\nb\\n\\n\",\n\t},\n\t{\n\t\t`declare -Ag a=([x]=y); echo ${a[\"x\"]}`,\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t`declare -A a=([x]=b [y]=c); for e in ${a[@]}; do echo $e; done | sort`,\n\t\t\"b\\nc\\n\",\n\t},\n\t{\n\t\t`declare -A a=([y]=b [x]=c); for e in ${a[*]}; do echo $e; done | sort`,\n\t\t\"b\\nc\\n\",\n\t},\n\t{\n\t\t`declare -A a=([x]=a); a[\"y\"]=d; a[\"x\"]=c; for e in ${a[@]}; do echo $e; done | sort`,\n\t\t\"c\\nd\\n\",\n\t},\n\t{\n\t\t`declare -A a=([x]=a); a[y]=d; a[x]=c; for e in ${a[@]}; do echo $e; done | sort`,\n\t\t\"c\\nd\\n\",\n\t},\n\t{\n\t\t// cheating a little; bash just did a=c\n\t\t`a=([\"x\"]=b [\"y\"]=c); echo ${a[\"y\"]}`,\n\t\t\"c\\n\",\n\t},\n\t{\n\t\t`declare -A a=(['x']=b); echo ${a['x']} ${a[$'x']} ${a[$\"x\"]}`,\n\t\t\"b b b\\n\",\n\t},\n\t{\n\t\t`a=(['x']=b); echo ${a['y']}`,\n\t\t\"\\n #IGNORE bash requires -A\",\n\t},\n\t{\n\t\t`declare -A a=(['a  1']=' x ' ['b  2']=' y '); for v in \"${a[@]}\"; do echo \"$v\"; done | sort`,\n\t\t\" x \\n y \\n\",\n\t},\n\t{\n\t\t`declare -A a=(['a  1']=' x ' ['b  2']=' y '); for v in \"${a[*]}\"; do echo \"$v\"; done`,\n\t\t\" x   y \\n\",\n\t},\n\t{\n\t\t`declare -A a=(['a  1']=' x ' ['b  2']=' y '); for v in \"${!a[@]}\"; do echo \"$v\"; done | sort`,\n\t\t\"a  1\\nb  2\\n\",\n\t},\n\t{\n\t\t`declare -A a=(['a  1']=' x ' ['b  2']=' y '); for v in \"${!a[*]}\"; do echo \"$v\"; done`,\n\t\t\"a  1 b  2\\n\",\n\t},\n\t{\n\t\t`declare -A a; a[a]=x; a[b]=y; for v in \"${!a[@]}\"; do echo \"$v\"; done | sort`,\n\t\t\"a\\nb\\n\",\n\t},\n\t{\n\t\t`declare -A a; a[a]=x; a[b]=y; declare -A a; for v in \"${!a[@]}\"; do echo \"$v\"; done | sort`,\n\t\t\"a\\nb\\n\",\n\t},\n\t// weird assignments\n\t{\"a=b; a=(c d); echo ${a[@]}\", \"c d\\n\"},\n\t{\"a=(b c); a=d; echo ${a[@]}\", \"d c\\n\"},\n\t{\"declare -A a=([x]=b [y]=c); a=d; for e in ${a[@]}; do echo $e; done | sort\", \"b\\nc\\nd\\n\"},\n\t{\"i=3; a=b; a[i]=x; echo ${a[@]}\", \"b x\\n\"},\n\t{\"i=3; declare a=(b); a[i]=x; echo ${!a[@]}\", \"0 3\\n\"},\n\t{\"i=3; declare -A a=(['x']=b); a[i]=x; for e in ${!a[@]}; do echo $e; done | sort\", \"i\\nx\\n\"},\n\n\t// declare\n\t{\"declare -B foo\", \"declare: invalid option \\\"-B\\\"\\nexit status 2 #JUSTERR\"},\n\t{\"a=b; declare a; echo $a; declare a=; echo $a\", \"b\\n\\n\"},\n\t{\"a=b; declare a; echo $a\", \"b\\n\"},\n\t{\n\t\t\"declare a=b c=(1 2); echo $a; echo ${c[@]}\",\n\t\t\"b\\n1 2\\n\",\n\t},\n\t{\"a=x; declare $a; echo $a $x\", \"x\\n\"},\n\t{\"a=x=y; declare $a; echo $a $x\", \"x=y y\\n\"},\n\t{\"a='x=(y)'; declare $a; echo $a $x\", \"x=(y) (y)\\n\"},\n\t{\"a='x=b y=c'; declare $a; echo $x $y\", \"b c\\n\"},\n\t{\"declare =bar\", \"declare: invalid name \\\"\\\"\\nexit status 1 #JUSTERR\"},\n\t{\"declare $unset=$unset\", \"declare: invalid name \\\"\\\"\\nexit status 1 #JUSTERR\"},\n\n\t// export\n\t{\"declare foo=bar; $ENV_PROG | grep '^foo='\", \"exit status 1\"},\n\t{\"declare -x foo=bar; $ENV_PROG | grep '^foo='\", \"foo=bar\\n\"},\n\t{\"export foo=bar; $ENV_PROG | grep '^foo='\", \"foo=bar\\n\"},\n\t{\"foo=bar; export foo; $ENV_PROG | grep '^foo='\", \"foo=bar\\n\"},\n\t{\"export foo=bar; foo=baz; $ENV_PROG | grep '^foo='\", \"foo=baz\\n\"},\n\t{\"export foo=bar; readonly foo=baz; $ENV_PROG | grep '^foo='\", \"foo=baz\\n\"},\n\t{\"export foo=(1 2); $ENV_PROG | grep '^foo='\", \"exit status 1\"},\n\t{\"declare -A foo=([a]=b); export foo; $ENV_PROG | grep '^foo='\", \"exit status 1\"},\n\t{\"export foo=(b c); foo=x; $ENV_PROG | grep '^foo='\", \"exit status 1\"},\n\t{\"foo() { bar=foo; export bar; }; foo; $ENV_PROG | grep ^bar=\", \"bar=foo\\n\"},\n\t{\"foo() { export bar; }; bar=foo; foo; $ENV_PROG | grep ^bar=\", \"bar=foo\\n\"},\n\t{\"foo() { export bar; }; foo; bar=foo; $ENV_PROG | grep ^bar=\", \"bar=foo\\n\"},\n\t{\"foo() { export bar=foo; }; foo; readonly bar; $ENV_PROG | grep ^bar=\", \"bar=foo\\n\"},\n\n\t// local\n\t{\n\t\t\"local a=b\",\n\t\t\"local: can only be used in a function\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"local a=b 2>/dev/null; echo $a\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"{ local a=b; }\",\n\t\t\"local: can only be used in a function\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"echo 'local a=b' >a; source ./a\",\n\t\t\"local: can only be used in a function\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"echo 'local a=b' >a; f() { source ./a; }; f; echo $a\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"f() { local a=b; }; f; echo $a\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"a=x; f() { local a=b; }; f; echo $a\",\n\t\t\"x\\n\",\n\t},\n\t{\n\t\t\"a=x; f() { echo $a; local a=b; echo $a; }; f\",\n\t\t\"x\\nb\\n\",\n\t},\n\t{\n\t\t\"f1() { local a=b; }; f2() { f1; echo $a; }; f2\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"f() { a=1; declare b=2; export c=3; readonly d=4; declare -g e=5; }; f; echo $a $b $c $d $e\",\n\t\t\"1 3 4 5\\n\",\n\t},\n\t{\n\t\t`f() { local x; [[ -v x ]] && echo set || echo unset; }; f`,\n\t\t\"unset\\n\",\n\t},\n\t{\n\t\t`f() { local x=; [[ -v x ]] && echo set || echo unset; }; f`,\n\t\t\"set\\n\",\n\t},\n\t{\n\t\t`export x=before; f() { local x; export x=after; $ENV_PROG | grep '^x='; }; f; echo $x`,\n\t\t\"x=after\\nbefore\\n\",\n\t},\n\t{\n\t\t\"getx() { echo $X; }; f() { local X=Y; getx; echo $X; }; f\",\n\t\t\"Y\\nY\\n\",\n\t},\n\t{\n\t\t\"setx() { X=Y; }; f() { local X; setx; echo $X; }; f\",\n\t\t\"Y\\n\",\n\t},\n\t{\n\t\t\"setx() { local X=Y; }; f() { local X; setx; echo $X; }; f\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"setx() { declare X=Y; }; f() { local X; setx; echo $X; }; f\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"setx() { X=Y :; }; f() { local X; setx; echo $X; }; f\",\n\t\t\"\\n\",\n\t},\n\n\t// unset global from inside function\n\t{\"f() { unset foo; echo $foo; }; foo=bar; f\", \"\\n\"},\n\t{\"f() { unset foo; }; foo=bar; f; echo $foo\", \"\\n\"},\n\n\t// name references\n\t{\"declare -n foo=bar; bar=etc; [[ -R foo ]]\", \"\"},\n\t{\"declare -n foo=bar; bar=etc; [ -R foo ]\", \"\"},\n\t{\"nameref foo=bar; bar=etc; [[ -R foo ]]\", \" #IGNORE\"},\n\t{\"declare foo=bar; bar=etc; [[ -R foo ]]\", \"exit status 1\"},\n\t{\n\t\t\"declare -n foo=bar; bar=etc; echo $foo; bar=zzz; echo $foo\",\n\t\t\"etc\\nzzz\\n\",\n\t},\n\t{\n\t\t\"declare -n foo=bar; bar=(x y); echo ${foo[1]}; bar=(a b); echo ${foo[1]}\",\n\t\t\"y\\nb\\n\",\n\t},\n\t{\n\t\t\"declare -n foo=bar; bar=etc; echo $foo; unset bar; echo $foo\",\n\t\t\"etc\\n\\n\",\n\t},\n\t{\n\t\t\"declare -n a1=a2 a2=a3 a3=a4; a4=x; echo $a1 $a3\",\n\t\t\"x x\\n\",\n\t},\n\t{\n\t\t\"declare -n foo=bar bar=foo; echo $foo\",\n\t\t\"\\n #IGNORE\",\n\t},\n\t{\n\t\t\"declare -n foo=bar; echo $foo\",\n\t\t\"\\n\",\n\t},\n\t{\n\t\t\"declare -n foo=bar; echo ${!foo}\",\n\t\t\"bar\\n\",\n\t},\n\t{\n\t\t\"declare -n foo=bar; bar=etc; echo $foo; echo ${!foo}\",\n\t\t\"etc\\nbar\\n\",\n\t},\n\t{\n\t\t\"declare -n foo=bar; bar=etc; foo=xxx; echo $foo $bar\",\n\t\t\"xxx xxx\\n\",\n\t},\n\t{\n\t\t\"declare -n foo=bar; foo=xxx; echo $foo $bar\",\n\t\t\"xxx xxx\\n\",\n\t},\n\t// TODO: figure this one out\n\t//{\n\t//        \"declare -n foo=bar bar=baz; foo=xxx; echo $foo $bar; echo $baz\",\n\t//        \"xxx xxx\\nxxx\\n\",\n\t//},\n\t{\n\t\t\"echo ${!@}-${!*}; set -- foo; echo ${!@}-${!*}-${!1}; foo=value; echo ${!@}-${!*}-${!1}\",\n\t\t\"-\\n--\\nvalue-value-value\\n\",\n\t},\n\n\t// read-only vars\n\t{\"declare -r foo=bar; echo $foo\", \"bar\\n\"},\n\t{\"readonly foo=bar; echo $foo\", \"bar\\n\"},\n\t{\"readonly foo=bar; export foo; echo $foo\", \"bar\\n\"},\n\t{\"readonly foo=bar; readonly bar=foo; export foo bar; echo $bar\", \"foo\\n\"},\n\t{\n\t\t\"a=b; a=c; echo $a; readonly a; a=d\",\n\t\t\"c\\na: readonly variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"declare -r foo=bar; foo=etc\",\n\t\t\"foo: readonly variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"declare -r foo=bar; export foo=\",\n\t\t\"foo: readonly variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"readonly foo=bar; foo=etc\",\n\t\t\"foo: readonly variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"foo() { bar=foo; readonly bar; }; foo; bar=bar\",\n\t\t\"bar: readonly variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"foo() { readonly bar; }; foo; bar=foo\",\n\t\t\"bar: readonly variable\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"foo() { readonly bar=foo; }; foo; export bar; $ENV_PROG | grep '^bar='\",\n\t\t\"bar=foo\\n\",\n\t},\n\n\t// multiple var modes at once\n\t{\n\t\t\"declare -r -x foo=bar; $ENV_PROG | grep '^foo='\",\n\t\t\"foo=bar\\n\",\n\t},\n\t{\n\t\t\"declare -r -x foo=bar; foo=x\",\n\t\t\"foo: readonly variable\\nexit status 1 #JUSTERR\",\n\t},\n\n\t// globbing\n\t{\"echo .\", \".\\n\"},\n\t{\"echo ..\", \"..\\n\"},\n\t{\"echo ./.\", \"./.\\n\"},\n\t{\n\t\t\">a.x >b.x >c.x; echo *.x; rm a.x b.x c.x\",\n\t\t\"a.x b.x c.x\\n\",\n\t},\n\t{\n\t\t`>a.x; echo '*.x' \"*.x\"; rm a.x`,\n\t\t\"*.x *.x\\n\",\n\t},\n\t{\n\t\t`>a.x >b.y; echo *'.'x; rm a.x`,\n\t\t\"a.x\\n\",\n\t},\n\t{\n\t\t`>a.x; echo *'.x' \"a.\"* '*'.x; rm a.x`,\n\t\t\"a.x a.x *.x\\n\",\n\t},\n\t{\n\t\t\"echo *.x; echo foo *.y bar\",\n\t\t\"*.x\\nfoo *.y bar\\n\",\n\t},\n\t{\n\t\t`>a.x >b.x >c.x; a=*.x; echo $a; echo \"$a\"`,\n\t\t\"a.x b.x c.x\\n*.x\\n\",\n\t},\n\t{\n\t\t`>a.x >b.x >c.x; a=(*.x); echo \"${a[@]}\"; echo ${a[1]}`,\n\t\t\"a.x b.x c.x\\nb.x\\n\",\n\t},\n\t{\n\t\t\"mkdir a; >a/b.x; echo */*.x | sed 's@\\\\\\\\@/@g'; cd a; echo *.x\",\n\t\t\"a/b.x\\nb.x\\n\",\n\t},\n\t{\n\t\t\"mkdir -p a/b/c; echo a/* | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a/b\\n\",\n\t},\n\t{\n\t\t\">.hidden >a; echo *; echo .h*; rm .hidden a\",\n\t\t\"a\\n.hidden\\n\",\n\t},\n\t{\n\t\t`mkdir d; >d/.hidden >d/a; set -- \"$(echo d/*)\" \"$(echo d/.h*)\"; echo ${#1} ${#2}; rm -r d`,\n\t\t\"3 9\\n\",\n\t},\n\t{\n\t\t\"mkdir -p a/b/c; echo a/** | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a/b\\n\",\n\t},\n\t{\n\t\t\"shopt -s globstar; mkdir -p a/b/c; echo a/** | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a/ a/b a/b/c\\n\",\n\t},\n\t{\n\t\t\"shopt -s globstar; mkdir -p a/b/c; echo **/c | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a/b/c\\n\",\n\t},\n\t{\n\t\t\"shopt -s globstar; mkdir -p a/b; touch c; echo ** | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a a/b c\\n\",\n\t},\n\t{\n\t\t\"shopt -s globstar; mkdir -p a/b; touch c; echo **/ | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a/ a/b/\\n\",\n\t},\n\t{\n\t\t\"shopt -s globstar; mkdir -p a/b/c a/d; echo ** | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a a/b a/b/c a/d\\n\",\n\t},\n\t{\n\t\t\"shopt -s globstar; mkdir -p a.x a/b.x a/b/c.x; echo **.x ./**.x | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a.x ./a.x\\n\",\n\t},\n\t{\n\t\t\"shopt -s globstar; mkdir -p a/b; touch a/b/c; echo **/* | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a a/b a/b/c\\n\",\n\t},\n\t{\n\t\t\"shopt -s globstar; mkdir -p b; touch x2 a b/c d x1; echo **/* | sed 's@\\\\\\\\@/@g'\",\n\t\t\"a b b/c d x1 x2\\n\",\n\t},\n\t{\n\t\t\"mkdir foo; touch foo/bar; echo */bar */bar/ | sed 's@\\\\\\\\@/@g'\",\n\t\t\"foo/bar */bar/\\n\",\n\t},\n\t{\n\t\t\"shopt -s nullglob; touch existing-1; echo missing-* existing-*\",\n\t\t\"existing-1\\n\",\n\t},\n\t// Extended globbing via the extglob option.\n\t// Note how extglob affects Bash's own line-by-line parsing, so we set the option before a newline.\n\t{\n\t\t\"shopt -s extglob\\necho invalid-?([)\",\n\t\t\"invalid-?([)\\n\",\n\t},\n\t{\n\t\t\"touch az a1z a12z a123z; echo a?([0-9])z\",\n\t\t\"extended globbing operator used without the \\\"extglob\\\" option set\\n #JUSTERR\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ntouch az a1z a12z a123z; echo a?([0-9])z\",\n\t\t\"a1z az\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ntouch az a1z a12z a123z; echo a*([0-9])z\",\n\t\t\"a123z a12z a1z az\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ntouch az a1z a12z a123z; echo a+([0-9])z\",\n\t\t\"a123z a12z a1z\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ntouch az a1z a12z a123z; echo a@([0-9])z\",\n\t\t\"a1z\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ntouch a{1..9}0z; echo a+(0|[1-2]|8)z\",\n\t\t\"a10z a20z a80z\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ntouch az a1z a12z a123z; echo a!([0-9])z\",\n\t\t\"a123z a12z az\\n\",\n\t},\n\t// !(pattern) extglob negation in case and [[ ]] matching\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"bar\\\" in !(foo)) echo match;; esac\",\n\t\t\"match\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"foo\\\" in !(foo)) echo match;; esac\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"\\\" in !(foo)) echo match;; esac\",\n\t\t\"match\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"baz\\\" in !(foo|bar)) echo match;; esac\",\n\t\t\"match\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"file.tar.gz\\\" in !(*.sig)) echo match;; esac\",\n\t\t\"match\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"file.sig\\\" in !(*.sig)) echo match;; esac\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"foo_xxx_baz\\\" in foo_!(bar)_baz) echo match;; esac\",\n\t\t\"match\\n\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"foo_bar_baz\\\" in foo_!(bar)_baz) echo match;; esac\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\n[[ \\\"bar\\\" == !(foo) ]] && echo match\",\n\t\t\"match\\n\",\n\t},\n\t// Unsupported: multiple groups, glob prefix, or glob suffix.\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"xabab\\\" in *a!(b)) echo match;; esac\",\n\t\t\" #IGNORE glob prefix not supported\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"baz\\\" in !(foo)!(bar)) echo match;; esac\",\n\t\t\" #IGNORE multiple extglob negation groups not supported\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\".bar\\\" in .*!(foo)) echo match;; esac\",\n\t\t\" #IGNORE glob prefix not supported\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\".foo\\\" in .*!(foo)) echo match;; esac\",\n\t\t\" #IGNORE glob prefix not supported\",\n\t},\n\t{\n\t\t\"shopt -s extglob\\ncase \\\"bar\\\" in .*!(foo)) echo match;; esac\",\n\t\t\" #IGNORE glob prefix not supported\",\n\t},\n\t{\n\t\t// Extended pattern matching is always available outside of pathname expansions (globbing).\n\t\t\"[[ a123z == a@([0-9])z ]]; echo $?; [[ a123z == a+([0-9])z ]]; echo $?\",\n\t\t\"1\\n0\\n\",\n\t},\n\t// Ensure that setting nullglob does not return invalid globs as null\n\t// strings.\n\t{\n\t\t\"shopt -s nullglob; [ -n butter ] && echo bubbles\",\n\t\t\"bubbles\\n\",\n\t},\n\t{\n\t\t\"cat <<EOF\\n{foo,bar}\\nEOF\",\n\t\t\"{foo,bar}\\n\",\n\t},\n\t{\n\t\t\"cat <<EOF\\n*.go\\nEOF\",\n\t\t\"*.go\\n\",\n\t},\n\t{\n\t\t\"mkdir -p a/b a/c; echo ./a/* | sed 's@\\\\\\\\@/@g'\",\n\t\t\"./a/b ./a/c\\n\",\n\t},\n\t{\n\t\t\"mkdir -p a/b a/c d; cd d; echo ../a/* | sed 's@\\\\\\\\@/@g'\",\n\t\t\"../a/b ../a/c\\n\",\n\t},\n\t{\n\t\t\"mkdir x-d1 x-d2; >x-f; echo x-*/ | sed 's@\\\\\\\\@/@g'\",\n\t\t\"x-d1/ x-d2/\\n\",\n\t},\n\t{\n\t\t\"mkdir x-d1 x-d2; >x-f; echo ././x-*/// | sed 's@\\\\\\\\@/@g'\",\n\t\t\"././x-d1/ ././x-d2/\\n\",\n\t},\n\t{\n\t\t\"mkdir -p x-d1/a x-d2/b; >x-f; echo x-*/* | sed 's@\\\\\\\\@/@g'\",\n\t\t\"x-d1/a x-d2/b\\n\",\n\t},\n\t{\n\t\t\"mkdir -p foo/bar; ln -s foo sym; echo sy*/; echo sym/b*\",\n\t\t\"sym/\\nsym/bar\\n\",\n\t},\n\t{\n\t\t\">foo; ln -s foo sym; echo sy*; echo sy*/\",\n\t\t\"sym\\nsy*/\\n\",\n\t},\n\t{\n\t\t\"mkdir x-d; >x-f; test -d $PWD/x-*/\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"mkdir dir; >dir/x-f; ln -s dir sym; cd sym; test -f $PWD/x-*\",\n\t\t\"\",\n\t},\n\n\t// brace expansion; there are also some tests in the expand package\n\t{\"echo a}b\", \"a}b\\n\"},\n\t{\"echo {a,b{c,d}\", \"{a,bc {a,bd\\n\"},\n\t{\"echo a{b}\", \"a{b}\\n\"},\n\t{\"echo a{à,世界}\", \"aà a世界\\n\"},\n\t{\"echo a{b,c}d{e,f}g\", \"abdeg abdfg acdeg acdfg\\n\"},\n\t{\"echo a{b{x,y},c}d\", \"abxd abyd acd\\n\"},\n\t{\"echo a{1..\", \"a{1..\\n\"},\n\t{\"echo a{1..2}b{4..5}c\", \"a1b4c a1b5c a2b4c a2b5c\\n\"},\n\t{\"echo a{c..f}\", \"ac ad ae af\\n\"},\n\t{\"echo a{4..1..1}\", \"a4 a3 a2 a1\\n\"},\n\t{\"b=c; echo ${b}a{4..1..1}\", \"ca4 ca3 ca2 ca1\\n\"},\n\t{\"b=c; echo a{1,2}$b\", \"a1c a2c\\n\"},\n\t{\"echo a{1,2}'bc'\", \"a1bc a2bc\\n\"},\n\n\t// brace expansion in declarations\n\t{\"declare {A,B}_VAR=1; echo $A_VAR $B_VAR\", \"1 1\\n\"},\n\t{\"declare {x,y}=val; echo $x $y\", \"val val\\n\"},\n\t{\"declare -x RUN_{VERY_,}EXPENSIVE_TESTS=yes; echo $RUN_EXPENSIVE_TESTS\", \"yes\\n\"},\n\t{\"declare {A,B}_VAR; A_VAR=1; B_VAR=2; echo $A_VAR $B_VAR\", \"1 2\\n\"},\n\t{\"declare {foo=x,bar=y}; echo $foo $bar\", \"x y\\n\"},\n\t{`declare foo{bar=baz`, \"declare: invalid name \\\"foo{bar\\\"\\nexit status 1 #JUSTERR\"},\n\t{\"{a,b}=value\", \"\\\"a=value\\\": executable file not found in $PATH\\nexit status 127 #JUSTERR\"},\n\n\t// tilde expansion\n\t{\n\t\t\"[[ '~/foo' == ~/foo ]] || [[ ~/foo == '~/foo' ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"case '~/foo' in ~/foo) echo match ;; esac\",\n\t\t\"\",\n\t},\n\t{\n\t\t\"a=~/foo; [[ $a == '~/foo' ]]\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t`a=$(echo \"~/foo\"); [[ $a == '~/foo' ]]`,\n\t\t\"\",\n\t},\n\t{\n\t\t`HOME=/foo; rel=/bar; echo ~/bar ~/'bar' ~/\"bar\" ~/$rel ~/\"$rel\"`,\n\t\t\"/foo/bar /foo/bar /foo/bar /foo//bar /foo//bar\\n\",\n\t},\n\t{\n\t\t`HOME=/foo; rel=/bar; echo ~'/bar' ~\"/bar\" ~$rel ~\"/$rel\"`,\n\t\t\"~/bar ~/bar ~/bar ~//bar\\n\",\n\t},\n\t{\n\t\t`HOME=/foo; echo ~ ~/ ~/'' ~'' ~\"\"`,\n\t\t\"/foo /foo/ /foo/ ~ ~\\n\",\n\t},\n\n\t// /dev/null\n\t{\"echo foo >/dev/null\", \"\"},\n\t{\"cat </dev/null\", \"\"},\n\n\t// time - real would be slow and flaky; see TestElapsedString\n\t{\"{ time; } |& wc | tr -s ' '\", \" 4 6 42\\n\"},\n\t{\"{ time echo -n; } |& wc | tr -s ' '\", \" 4 6 42\\n\"},\n\t{\"{ time -p; } |& wc | tr -s ' '\", \" 3 6 29\\n\"},\n\t{\"{ time -p echo -n; } |& wc | tr -s ' '\", \" 3 6 29\\n\"},\n\n\t// exec\n\t{\"exec\", \"\"},\n\t{\n\t\t\"exec builtin echo foo\",\n\t\t\"\\\"builtin\\\": executable file not found in $PATH\\nexit status 127 #JUSTERR\",\n\t},\n\t{\n\t\t\"exec $GOSH_PROG 'echo foo'; echo bar\",\n\t\t\"foo\\n\",\n\t},\n\n\t// read\n\t{\n\t\t\"read </dev/null\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"read 1</dev/null\",\n\t\t\"exit status 1\",\n\t},\n\t{\n\t\t\"read -X\",\n\t\t\"read: invalid option \\\"-X\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"read -rX\",\n\t\t\"read: invalid option \\\"-X\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"read 0ab\",\n\t\t\"read: invalid identifier \\\"0ab\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"read <<< foo; echo $REPLY\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"read <<<'  a  b  c  '; echo \\\"$REPLY\\\"\",\n\t\t\"  a  b  c  \\n\",\n\t},\n\t{\n\t\t\"read <<< 'y\\nn\\n'; echo $REPLY\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"read a_0 <<< foo; echo $a_0\",\n\t\t\"foo\\n\",\n\t},\n\t{\n\t\t\"read a b <<< 'foo  bar  baz  '; echo \\\"$a\\\"; echo \\\"$b\\\"\",\n\t\t\"foo\\nbar  baz\\n\",\n\t},\n\t{\n\t\t\"while read a; do echo $a; done <<< 'a\\nb\\nc'\",\n\t\t\"a\\nb\\nc\\n\",\n\t},\n\t{\n\t\t\"while read a b; do echo -e \\\"$a\\n$b\\\"; done <<< '1 2\\n3'\",\n\t\t\"1\\n2\\n3\\n\\n\",\n\t},\n\t{\n\t\t`read a <<< '\\\\'; echo \"$a\"`,\n\t\t\"\\\\\\n\",\n\t},\n\t{\n\t\t`read a <<< '\\a\\b\\c'; echo \"$a\"`,\n\t\t\"abc\\n\",\n\t},\n\t{\n\t\t\"read -r a b <<< '1\\\\\\t2'; echo $a; echo $b;\",\n\t\t\"1\\\\\\n2\\n\",\n\t},\n\t{\n\t\t\"echo line\\\\\\ncontinuation | while read a; do echo $a; done\",\n\t\t\"linecontinuation\\n\",\n\t},\n\t{\n\t\t\"while read a; do echo $a; GOSH_CMD=print_ok $GOSH_PROG; done <<< 'a\\nb\\nc'\",\n\t\t\"a\\nexec ok\\nb\\nexec ok\\nc\\nexec ok\\n\",\n\t},\n\t{\n\t\t\"while read a; do echo $a; GOSH_CMD=print_ok $GOSH_PROG; done <<EOF\\na\\nb\\nc\\nEOF\",\n\t\t\"a\\nexec ok\\nb\\nexec ok\\nc\\nexec ok\\n\",\n\t},\n\t{\n\t\t\"echo file1 >f; echo file2 >>f; while read a; do echo $a; done <f\",\n\t\t\"file1\\nfile2\\n\",\n\t},\n\t// TODO: our final exit status here isn't right.\n\t// {\n\t// \t\"while read a; do echo $a; GOSH_CMD=print_fail $GOSH_PROG; done <<< 'a\\nb\\nc'\",\n\t// \t\"a\\nexec fail\\nb\\nexec fail\\nc\\nexec fail\\nexit status 1\",\n\t// },\n\t{\n\t\t`read -r a <<< '\\\\'; echo \"$a\"`,\n\t\t\"\\\\\\\\\\n\",\n\t},\n\t{\n\t\t\"read -r a <<< '\\\\a\\\\b\\\\c'; echo $a\",\n\t\t\"\\\\a\\\\b\\\\c\\n\",\n\t},\n\t{\n\t\t\"IFS=: read a b c <<< '1:2:3'; echo $a; echo $b; echo $c\",\n\t\t\"1\\n2\\n3\\n\",\n\t},\n\t{\n\t\t\"IFS=: read a b c <<< '1\\\\:2:3'; echo \\\"$a\\\"; echo $b; echo $c\",\n\t\t\"1:2\\n3\\n\\n\",\n\t},\n\t{\n\t\t\"read -p\",\n\t\t\"read: -p: option requires an argument\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"read -X -p\",\n\t\t\"read: invalid option \\\"-X\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"read -p 'Display me as a prompt. Continue? (y/n) ' choice <<< 'y'; echo $choice\",\n\t\t\"Display me as a prompt. Continue? (y/n) y\\n #IGNORE bash requires a terminal\",\n\t},\n\t{\n\t\t\"read -r -p 'Prompt and raw flag together: ' a <<< '\\\\a\\\\b\\\\c'; echo $a\",\n\t\t\"Prompt and raw flag together: \\\\a\\\\b\\\\c\\n #IGNORE bash requires a terminal\",\n\t},\n\n\t// read -a\n\t{\n\t\t`echo \"1 2 3\" | { read -a arr; echo \"${arr[0]} ${arr[1]} ${arr[2]}\"; }`,\n\t\t\"1 2 3\\n\",\n\t},\n\t{\n\t\t`echo \"a b c\" | { read -a arr; echo \"${#arr[@]}\"; }`,\n\t\t\"3\\n\",\n\t},\n\t{\n\t\t`echo \"\" | { read -a arr; echo \"${#arr[@]}\"; }`,\n\t\t\"0\\n\",\n\t},\n\t{\n\t\t`echo 'a\\tb' | { read -ra arr; echo \"${#arr[@]} ${arr[0]}\"; }`,\n\t\t\"1 a\\\\tb\\n\",\n\t},\n\t{\n\t\t`a=a; echo | (read a; echo -n \"$a\")`,\n\t\t\"\",\n\t},\n\t{\n\t\t`a=b; read a < /dev/null; echo -n \"$a\"`,\n\t\t\"\",\n\t},\n\t{\n\t\t\"a=c; echo x | (read a; echo -n $a)\",\n\t\t\"x\",\n\t},\n\t{\n\t\t\"a=d; echo -n y | (read a; echo -n $a)\",\n\t\t\"y\",\n\t},\n\n\t// getopts\n\t{\n\t\t\"getopts\",\n\t\t\"getopts: usage: getopts optstring name [arg ...]\\nexit status 2\",\n\t},\n\t{\n\t\t\"getopts a a:b\",\n\t\t\"getopts: invalid identifier: \\\"a:b\\\"\\nexit status 2 #JUSTERR\",\n\t},\n\t{\n\t\t\"getopts abc opt -a; echo $opt; $optarg\",\n\t\t\"a\\n\",\n\t},\n\t{\n\t\t\"getopts abc opt -z\",\n\t\t\"getopts: illegal option -- \\\"z\\\"\\n #IGNORE\",\n\t},\n\t{\n\t\t\"getopts a: opt -a\",\n\t\t\"getopts: option requires an argument -- \\\"a\\\"\\n #IGNORE\",\n\t},\n\t{\n\t\t\"getopts :abc opt -z; echo $opt; echo $OPTARG\",\n\t\t\"?\\nz\\n\",\n\t},\n\t{\n\t\t\"getopts :a: opt -a; echo $opt; echo $OPTARG\",\n\t\t\":\\na\\n\",\n\t},\n\t{\n\t\t\"getopts abc opt foo -a; echo $opt; echo $OPTIND\",\n\t\t\"?\\n1\\n\",\n\t},\n\t{\n\t\t\"getopts abc opt -a foo; echo $opt; echo $OPTIND\",\n\t\t\"a\\n2\\n\",\n\t},\n\t{\n\t\t\"OPTIND=3; getopts abc opt -a -b -c; echo $opt;\",\n\t\t\"c\\n\",\n\t},\n\t{\n\t\t\"OPTIND=100; getopts abc opt -a -b -c; echo $opt;\",\n\t\t\"?\\n\",\n\t},\n\t{\n\t\t\"OPTIND=foo; getopts abc opt -a -b -c; echo $opt;\",\n\t\t\"a\\n\",\n\t},\n\t{\n\t\t\"while getopts ab:c opt -c -b arg -a foo; do echo $opt $OPTARG $OPTIND; done\",\n\t\t\"c 2\\nb arg 4\\na 5\\n\",\n\t},\n\t{\n\t\t\"while getopts abc opt -ba -c foo; do echo $opt $OPTARG $OPTIND; done\",\n\t\t\"b 1\\na 2\\nc 3\\n\",\n\t},\n\t{\n\t\t\"a() { while getopts abc: opt; do echo $opt $OPTARG; done }; a -a -b -c arg\",\n\t\t\"a\\nb\\nc arg\\n\",\n\t},\n\t// mapfile\n\t{\n\t\t\"mapfile <<EOF\\na\\nb\\nc\\nEOF\\n\" + `for x in \"${MAPFILE[@]}\"; do echo \"$x\"; done`,\n\t\t\"a\\n\\nb\\n\\nc\\n\\n\",\n\t},\n\t{\n\t\t\"mapfile -t <<EOF\\na\\nb\\nc\\nEOF\\n\" + `for x in \"${MAPFILE[@]}\"; do echo \"$x\"; done`,\n\t\t\"a\\nb\\nc\\n\",\n\t},\n\t{\n\t\t\"mapfile -t -d b <<EOF\\nabc\\nEOF\\n\" + `for x in \"${MAPFILE[@]}\"; do echo \"$x\"; done`,\n\t\t\"a\\nc\\n\\n\",\n\t},\n\t{\n\t\t\"mapfile -t butter <<EOF\\na\\nb\\nc\\nEOF\\n\" + `for x in \"${butter[@]}\"; do echo \"$x\"; done`,\n\t\t\"a\\nb\\nc\\n\",\n\t},\n}\n\nvar runTestsUnix = []runTest{\n\t{\"[[ -n $PPID && $PPID -ge 0 ]]\", \"\"}, // can be 0 if running as the init process\n\t{\n\t\t// no root user on windows\n\t\t\"[[ ~root == '~root' ]]\",\n\t\t\"exit status 1\",\n\t},\n\n\t// windows does not support paths with '*'\n\t{\n\t\t\"mkdir -p '*/a.z' 'b/a.z'; cd '*'; set -- *.z; echo $#\",\n\t\t\"1\\n\",\n\t},\n\t{\n\t\t\"mkdir -p 'a-*/d'; test -d $PWD/a-*/*\",\n\t\t\"\",\n\t},\n\n\t// no fifos on windows\n\t{\n\t\t\"[ -p a ] && echo x; mkfifo a; [ -p a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -p a ]] && echo x; mkfifo a; [[ -p a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\n\t{\"sh() { :; }; sh -c 'echo foo'\", \"\"},\n\t{\"sh() { :; }; command sh -c 'echo foo'\", \"foo\\n\"},\n\n\t// chmod is practically useless on Windows\n\t{\n\t\t\"[ -x a ] && echo x; >a; chmod 0755 a; [ -x a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\"[[ -x a ]] && echo x; >a; chmod 0755 a; [[ -x a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\">a; [ -k a ] && echo x; chmod +t a; [ -k a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\">a; [ -u a ] && echo x; chmod u+s a; [ -u a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\">a; [ -g a ] && echo x; chmod g+s a; [ -g a ] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\">a; [[ -k a ]] && echo x; chmod +t a; [[ -k a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\">a; [[ -u a ]] && echo x; chmod u+s a; [[ -u a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t\">a; [[ -g a ]] && echo x; chmod g+s a; [[ -g a ]] && echo y\",\n\t\t\"y\\n\",\n\t},\n\t{\n\t\t`mkdir a; chmod 0100 a; cd a`,\n\t\t\"\",\n\t},\n\t// Note that these will succeed if we're root.\n\t{\n\t\t`mkdir a; chmod 0000 a; cd a`,\n\t\t\"cd: permission denied: \\\"a\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t`mkdir a; chmod 0222 a; cd a`,\n\t\t\"cd: permission denied: \\\"a\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t`mkdir a; chmod 0444 a; cd a`,\n\t\t\"cd: permission denied: \\\"a\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t`mkdir a; chmod 0010 a; cd a`,\n\t\t\"cd: permission denied: \\\"a\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t`mkdir a; chmod 0001 a; cd a`,\n\t\t\"cd: permission denied: \\\"a\\\"\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t`unset UID`,\n\t\t\"UID: readonly variable\\n #IGNORE\",\n\t},\n\t{\n\t\t`test -n \"$EUID\" && echo OK`,\n\t\t\"OK\\n\",\n\t},\n\t{\n\t\t`set EUID=newvalue; test EUID != newvalue && echo OK || echo EUID=$EUID`,\n\t\t\"OK\\n\",\n\t},\n\t{\n\t\t`unset EUID`,\n\t\t\"EUID: readonly variable\\n #IGNORE\",\n\t},\n\t// GID is not set in bash\n\t{\n\t\t`unset GID`,\n\t\t\"GID: readonly variable\\n #IGNORE\",\n\t},\n\t{\n\t\t`[[ -z $GID ]] && echo \"GID not set\"`,\n\t\t\"exit status 1 #JUSTERR #IGNORE\",\n\t},\n\n\t// Unix-y PATH\n\t{\n\t\t\"PATH=; bash -c 'echo foo'\",\n\t\t\"\\\"bash\\\": executable file not found in $PATH\\nexit status 127 #JUSTERR\",\n\t},\n\t{\n\t\t\"cd /; sure/is/missing\",\n\t\t\"stat /sure/is/missing: no such file or directory\\nexit status 127 #JUSTERR\",\n\t},\n\t{\n\t\t\"echo '#!/bin/sh\\necho b' >a; chmod 0755 a; PATH=; a\",\n\t\t\"b\\n\",\n\t},\n\t{\n\t\t\"mkdir c; cd c; echo '#!/bin/sh\\necho b' >a; chmod 0755 a; PATH=; a\",\n\t\t\"b\\n\",\n\t},\n\t{\n\t\t\"mkdir c; echo '#!/bin/sh\\necho b' >c/a; chmod 0755 c/a; c/a\",\n\t\t\"b\\n\",\n\t},\n\t{\n\t\t\"GOSH_CMD=lookpath $GOSH_PROG\",\n\t\t\"sh found\\n\",\n\t},\n\n\t// error strings which are too different on Windows\n\t{\n\t\t\"echo foo >/shouldnotexist/file\",\n\t\t\"open /shouldnotexist/file: no such file or directory\\nexit status 1 #JUSTERR\",\n\t},\n\t{\n\t\t\"set -e; echo foo >/shouldnotexist/file; echo foo\",\n\t\t\"open /shouldnotexist/file: no such file or directory\\nexit status 1 #JUSTERR\",\n\t},\n\n\t// process substitution; named pipes (fifos) are a TODO for windows\n\t{\n\t\t\"sed 's/o/e/g' <(echo foo bar)\",\n\t\t\"fee bar\\n\",\n\t},\n\t{\n\t\t\"cat <(echo foo) <(echo bar) <(echo baz)\",\n\t\t\"foo\\nbar\\nbaz\\n\",\n\t},\n\t{\n\t\t\"cat <(cat <(echo nested))\",\n\t\t\"nested\\n\",\n\t},\n\t{\n\t\t// The tests here use \"wait\" because otherwise the parent may finish before\n\t\t// the subprocess has had time to process the input and print the result.\n\t\t\"echo foo bar > >(sed 's/o/e/g'); wait\",\n\t\t\"fee bar\\n\",\n\t},\n\t{\n\t\t\"echo foo bar | tee >(sed 's/o/e/g') >/dev/null; wait\",\n\t\t\"fee bar\\n\",\n\t},\n\t{\n\t\t\"echo nested > >(cat > >(cat); wait); wait\",\n\t\t\"nested\\n\",\n\t},\n\t{\n\t\t\"cat <(exit 0); wait $!; echo $?\",\n\t\t\"0\\n\",\n\t},\n\t{\n\t\t\"cat <(exit 5); wait $!; echo $?\",\n\t\t\"5\\n\",\n\t},\n\t{\n\t\t// The reader here does not consume the named pipe.\n\t\t\"test -e <(echo foo)\",\n\t\t\"\",\n\t},\n\t// echo trace\n\t{\n\t\t`set -x; animals=(\"dog\", \"cat\", \"otter\"); echo \"hello ${animals[*]}\"`,\n\t\t`+ animals=(\"dog\", \"cat\", \"otter\")\n+ echo 'hello dog, cat, otter'\nhello dog, cat, otter\n`,\n\t},\n\t{\n\t\t`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\"`,\n\t\t`+ s='always print a decimal point for %e, %E, %f, %F, %g and %G; do not remove trailing zeros for %g and %G'\n+ echo 'always print a decimal point for %e, %E, %f, %F, %g and %G; do not remove trailing zeros for %g and %G'\nalways print a decimal point for %e, %E, %f, %F, %g and %G; do not remove trailing zeros for %g and %G\n`,\n\t},\n\t{\n\t\t`set -x\nx=without; echo \"$x\"\nx=\"double quote\"; echo \"$x\"\nx='single quote'; echo \"$x\"`,\n\t\t`+ x=without\n+ echo without\nwithout\n+ x='double quote'\n+ echo 'double quote'\ndouble quote\n+ x='single quote'\n+ echo 'single quote'\nsingle quote\n`,\n\t},\n\t// for trace\n\t{\n\t\t`set -x\nexec >/dev/null\necho \"trace should go to stderr\"`,\n\t\t`+ exec\n+ echo 'trace should go to stderr'\n`,\n\t},\n\t{\n\t\t`set -x\nanimals=(dog, cat, otter)\nfor i in ${animals[@]}\ndo\n   echo \"hello ${i}\"\ndone\n`,\n\t\t`+ animals=(dog, cat, otter)\n+ for i in ${animals[@]}\n+ echo 'hello dog,'\nhello dog,\n+ for i in ${animals[@]}\n+ echo 'hello cat,'\nhello cat,\n+ for i in ${animals[@]}\n+ echo 'hello otter'\nhello otter\n`,\n\t},\n\t{\n\t\t`set -x\nloop() {\n    for i do\n        echo \"something with $i\"\n    done\n}\nloop 1 2 3`,\n\t\t`+ loop 1 2 3\n+ for i in \"$@\"\n+ echo 'something with 1'\nsomething with 1\n+ for i in \"$@\"\n+ echo 'something with 2'\nsomething with 2\n+ for i in \"$@\"\n+ echo 'something with 3'\nsomething with 3\n`,\n\t},\n\t{\n\t\t`set -x; animals=(dog, cat, otter); for i in ${animals[@]}; do echo \"hello ${i}\"; done`,\n\t\t`+ animals=(dog, cat, otter)\n+ for i in ${animals[@]}\n+ echo 'hello dog,'\nhello dog,\n+ for i in ${animals[@]}\n+ echo 'hello cat,'\nhello cat,\n+ for i in ${animals[@]}\n+ echo 'hello otter'\nhello otter\n`,\n\t},\n\t{\n\t\t`set -x; a=x\"y\"$z b=(foo bar $none '')`,\n\t\t\"+ a=xy\\n+ b=(foo bar $none '')\\n\",\n\t},\n\t{\n\t\t`set -x; for i in a b; do echo $i; done`,\n\t\t`+ for i in a b\n+ echo a\na\n+ for i in a b\n+ echo b\nb\n`,\n\t},\n\t{\n\t\t`set -x; for i in $none_a $none_b; do echo $i; done`,\n\t\t``,\n\t},\n\t// case trace\n\t{\n\t\t`set -x; pet=dog; case $pet in 'dog') echo \"barks\";; *) echo \"unknown\";; esac`,\n\t\t`+ pet=dog\n+ case $pet in\n+ echo barks\nbarks\n`,\n\t},\n\t{\n\t\t`set -x\npet=\"dog\"\ncase $pet in\n  dog)\n    echo \"barks\"\n    ;;\n  *)\n    echo \"unknown\"\n    ;;\nesac`,\n\t\t`+ pet=dog\n+ case $pet in\n+ echo barks\nbarks\n`,\n\t},\n\t// arithmetic\n\t{\n\t\t`set -x\na=$(( 4 + 5 )); echo $a\na=$((3+5)); echo $a`,\n\t\t`+ a=9\n+ echo 9\n9\n+ a=8\n+ echo 8\n8\n`,\n\t},\n\t{\n\t\t`set -x;\nlet a=5+4; echo $a\nlet \"a = 5 + 4\"; echo $a\nlet a++; echo $a`,\n\t\t`+ let a=5+4\n+ echo 9\n9\n+ let 'a = 5 + 4'\n+ echo 9\n9\n+ let a++\n+ echo 10\n10\n`,\n\t},\n\t// functions\n\t{\n\t\t`set -x; function with_function () { echo 'hello, world'; }; with_function`,\n\t\t`+ with_function\n+ echo 'hello, world'\nhello, world\n`,\n\t},\n\t{\n\t\t`set -x; without_function () { echo 'hello, world'; }; without_function`,\n\t\t`+ without_function\n+ echo 'hello, world'\nhello, world\n`,\n\t},\n\t{\n\t\t// globbing wildcard as function name\n\t\t`@() { echo \"$@\"; }; @ lala; function +() { echo \"$@\"; }; + foo`,\n\t\t\"lala\\nfoo\\n\",\n\t},\n\t{\n\t\t`      @() { echo \"$@\"; }; @ lala;`,\n\t\t\"lala\\n\",\n\t},\n\t{\n\t\t// globbing wildcard as function name but with space after the name\n\t\t`+ () { echo \"$@\"; }; + foo; @ () { echo \"$@\"; }; @ lala; ? () { echo \"$@\"; }; ? bar`,\n\t\t\"foo\\nlala\\nbar\\n\",\n\t},\n\t// mapfile, no process substitution yet on Windows\n\t{\n\t\t`mapfile -t -d \"\" < <(printf \"a\\0b\\n\"); for x in \"${MAPFILE[@]}\"; do echo \"$x\"; done`,\n\t\t\"a\\nb\\n\\n\",\n\t},\n\t// Windows does not support having a `\\n` in a filename\n\t{\n\t\t`> $'bar\\nbaz'; echo bar*baz`,\n\t\t\"bar\\nbaz\\n\",\n\t},\n}\n\nvar runTestsWindows = []runTest{\n\t{\"[[ -n $PPID || $PPID -gt 0 ]]\", \"\"}, // os.Getppid can be 0 on windows\n\t{\"cmd() { :; }; cmd /c 'echo foo'\", \"\"},\n\t{\"cmd() { :; }; command cmd /c 'echo foo'\", \"foo\\r\\n\"},\n\t{\n\t\t\"GOSH_CMD=lookpath $GOSH_PROG\",\n\t\t\"cmd found\\n\",\n\t},\n\t{\n\t\t\"localCase=camel; LocalCase=pascal; echo $localcase\",\n\t\t\"pascal\\n\",\n\t},\n\t{\n\t\t// Matching the env var name set as a global\n\t\t// in a case sensitive way.\n\t\t\"$ENV_PROG | grep -i '^mixedCase_interp'\",\n\t\t\"mixedCase_INTERP_GLOBAL=value\\n\",\n\t},\n\t{\n\t\t// Overwriting the env var set as a global\n\t\t// in a case insensitive way.\n\t\t\"MIXEDCASE_interp_global=replaced; echo $MIXEDCASE_interp_GLOBAL\",\n\t\t\"replaced\\n\",\n\t},\n\t{\n\t\t\"MIXEDCASE_interp_global=replaced; $ENV_PROG | grep -i '^mixedcase_interp'\",\n\t\t\"MIXEDCASE_interp_global=replaced\\n\",\n\t},\n}\n\n// These tests are specific to 64-bit architectures, and that's fine. We don't\n// need to add explicit versions for 32-bit.\nvar runTests64bit = []runTest{\n\t{\"printf %i,%u -3 -3\", \"-3,18446744073709551613\"},\n\t{\"printf %o -3\", \"1777777777777777777775\"},\n\t{\"printf %x -3\", \"fffffffffffffffd\"},\n}\n\nfunc init() {\n\tif runtime.GOOS == \"windows\" {\n\t\trunTests = append(runTests, runTestsWindows...)\n\t} else { // Unix-y\n\t\trunTests = append(runTests, runTestsUnix...)\n\t}\n\tif bits.UintSize == 64 {\n\t\trunTests = append(runTests, runTests64bit...)\n\t}\n}\n\n// ln -s: wine doesn't implement symlinks; see https://bugs.winehq.org/show_bug.cgi?id=44948\n// process substitutions are not supported on Windows\nvar skipOnWindows = regexp.MustCompile(`ln -s|<\\(`)\n\n// process substitutions seemflaky on mac; see https://github.com/mvdan/sh/issues/576\nvar skipOnMac = regexp.MustCompile(`>\\(|<\\(`)\n\nfunc skipIfUnsupported(tb testing.TB, src string) {\n\tswitch {\n\tcase runtime.GOOS == \"windows\" && skipOnWindows.MatchString(src):\n\t\ttb.Skipf(\"skipping non-portable test on windows\")\n\tcase runtime.GOOS == \"darwin\" && skipOnMac.MatchString(src):\n\t\ttb.Skipf(\"skipping non-portable test on mac\")\n\t}\n}\n\nfunc TestRunnerRun(t *testing.T) {\n\tt.Parallel()\n\n\tp := syntax.NewParser()\n\tfor _, c := range runTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tskipIfUnsupported(t, c.in)\n\t\t\tt.Logf(\"input: %q\", c.in)\n\n\t\t\t// Parse first, as we reuse a single parser.\n\t\t\tfile := parse(t, p, c.in)\n\n\t\t\tt.Parallel()\n\n\t\t\ttdir := t.TempDir()\n\t\t\tvar cb concBuffer\n\t\t\tr, err := interp.New(interp.Dir(tdir), interp.StdIO(nil, &cb, &cb),\n\t\t\t\t// TODO: why does this make some tests hang?\n\t\t\t\t// interp.Env(expand.ListEnviron(append(os.Environ(),\n\t\t\t\t// \t\"foo_NULL_BAR=foo\\x00bar\")...)),\n\t\t\t\tinterp.ExecHandlers(testExecHandler),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\t\t\tdefer cancel()\n\t\t\tif err := r.Run(ctx, file); err != nil {\n\t\t\t\tcb.WriteString(err.Error())\n\t\t\t}\n\n\t\t\t// Some builtins like \"pushd\" can show absolute paths as part of error messages.\n\t\t\t// Allow a very simple search-and-replace for the equivalent to \"$PWD/a\".\n\t\t\twant := strings.ReplaceAll(c.want, \"ABS_PATH_A\", fmt.Sprintf(\"%q\", filepath.Join(tdir, \"a\")))\n\n\t\t\tif i := strings.Index(want, \" #\"); i >= 0 {\n\t\t\t\twant = want[:i]\n\t\t\t}\n\t\t\tif got := cb.String(); got != want {\n\t\t\t\tif len(got) > 200 {\n\t\t\t\t\tgot = \"…\" + got[len(got)-200:]\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"wrong output in %q:\\nwant: %q\\ngot:  %q\",\n\t\t\t\t\tc.in, want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc readLines(hc interp.HandlerContext) ([][]byte, error) {\n\tbs, err := io.ReadAll(hc.Stdin)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif runtime.GOOS == \"windows\" {\n\t\tbs = bytes.ReplaceAll(bs, []byte(\"\\r\\n\"), []byte(\"\\n\"))\n\t}\n\tbs = bytes.TrimSuffix(bs, []byte(\"\\n\"))\n\treturn bytes.Split(bs, []byte(\"\\n\")), nil\n}\n\nfunc absPath(dir, path string) string {\n\tif path == \"\" {\n\t\treturn \"\"\n\t}\n\tif !filepath.IsAbs(path) {\n\t\tpath = filepath.Join(dir, path)\n\t}\n\treturn filepath.Clean(path) // TODO: this clean is likely unnecessary\n}\n\nvar testBuiltinsMap = map[string]func(interp.HandlerContext, []string) error{\n\t\"cat\": func(hc interp.HandlerContext, args []string) error {\n\t\tif len(args) == 0 {\n\t\t\tif hc.Stdin == nil || hc.Stdout == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t_, err := io.Copy(hc.Stdout, hc.Stdin)\n\t\t\treturn err\n\t\t}\n\t\tfor _, arg := range args {\n\t\t\tpath := absPath(hc.Dir, arg)\n\t\t\tf, err := os.Open(path)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t_, err = io.Copy(hc.Stdout, f)\n\t\t\tf.Close()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n\t\"wc\": func(hc interp.HandlerContext, args []string) error {\n\t\tbs, err := io.ReadAll(hc.Stdin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(args) == 0 {\n\t\t\tfmt.Fprintf(hc.Stdout, \"%7d\", bytes.Count(bs, []byte(\"\\n\")))\n\t\t\tfmt.Fprintf(hc.Stdout, \"%8d\", len(bytes.Fields(bs)))\n\t\t\tfmt.Fprintf(hc.Stdout, \"%8d\\n\", len(bs))\n\t\t} else if args[0] == \"-c\" {\n\t\t\tfmt.Fprintln(hc.Stdout, len(bs))\n\t\t} else if args[0] == \"-l\" {\n\t\t\tfmt.Fprintln(hc.Stdout, bytes.Count(bs, []byte(\"\\n\")))\n\t\t}\n\t\treturn nil\n\t},\n\t\"tr\": func(hc interp.HandlerContext, args []string) error {\n\t\tif len(args) != 2 || len(args[1]) != 1 {\n\t\t\treturn fmt.Errorf(\"usage: tr [-s -d] [character]\")\n\t\t}\n\t\tsqueeze := args[0] == \"-s\"\n\t\tchar := args[1][0]\n\t\tbs, err := io.ReadAll(hc.Stdin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor {\n\t\t\ti := bytes.IndexByte(bs, char)\n\t\t\tif i < 0 {\n\t\t\t\thc.Stdout.Write(bs) // remaining\n\t\t\t\tbreak\n\t\t\t}\n\t\t\thc.Stdout.Write(bs[:i]) // up to char\n\t\t\tbs = bs[i+1:]\n\n\t\t\tbs = bytes.TrimLeft(bs, string(char)) // remove repeats\n\t\t\tif squeeze {\n\t\t\t\thc.Stdout.Write([]byte{char})\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n\t\"sort\": func(hc interp.HandlerContext, args []string) error {\n\t\tlines, err := readLines(hc)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tslices.SortFunc(lines, bytes.Compare)\n\t\tfor _, line := range lines {\n\t\t\tfmt.Fprintf(hc.Stdout, \"%s\\n\", line)\n\t\t}\n\t\treturn nil\n\t},\n\t\"grep\": func(hc interp.HandlerContext, args []string) error {\n\t\tvar rx *regexp.Regexp\n\t\tquiet := false\n\t\tcaseInsensitive := false\n\t\tfor _, arg := range args {\n\t\t\tif arg == \"-q\" {\n\t\t\t\tquiet = true\n\t\t\t} else if arg == \"-i\" {\n\t\t\t\tcaseInsensitive = true\n\t\t\t} else if arg == \"-E\" {\n\t\t\t} else if rx == nil {\n\t\t\t\tif caseInsensitive {\n\t\t\t\t\targ = \"(?i)\" + arg\n\t\t\t\t}\n\t\t\t\trx = regexp.MustCompile(arg)\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"unexpected arg: %q\", arg)\n\t\t\t}\n\t\t}\n\t\tlines, err := readLines(hc)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tanyMatch := false\n\t\tfor _, line := range lines {\n\t\t\tif rx.Match(line) {\n\t\t\t\tif quiet {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tanyMatch = true\n\t\t\t\tfmt.Fprintf(hc.Stdout, \"%s\\n\", line)\n\t\t\t}\n\t\t}\n\t\tif !anyMatch {\n\t\t\treturn interp.ExitStatus(1)\n\t\t}\n\t\treturn nil\n\t},\n\t\"sed\": func(hc interp.HandlerContext, args []string) error {\n\t\tf := hc.Stdin\n\t\tswitch len(args) {\n\t\tcase 1:\n\t\tcase 2:\n\t\t\tvar err error\n\t\t\tf, err = os.Open(absPath(hc.Dir, args[1]))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"usage: sed pattern [file]\")\n\t\t}\n\t\texpr := args[0]\n\t\tif expr == \"\" || expr[0] != 's' {\n\t\t\treturn fmt.Errorf(\"unimplemented\")\n\t\t}\n\t\tsep := expr[1]\n\t\texpr = expr[2:]\n\t\tfrom := expr[:strings.IndexByte(expr, sep)]\n\t\texpr = expr[len(from)+1:]\n\t\tto := expr[:strings.IndexByte(expr, sep)]\n\t\tbs, err := io.ReadAll(f)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trx := regexp.MustCompile(from)\n\t\tbs = rx.ReplaceAllLiteral(bs, []byte(to))\n\t\t_, err = hc.Stdout.Write(bs)\n\t\treturn err\n\t},\n\t\"mkdir\": func(hc interp.HandlerContext, args []string) error {\n\t\tfor _, arg := range args {\n\t\t\tif arg == \"-p\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpath := absPath(hc.Dir, arg)\n\t\t\tif err := os.MkdirAll(path, 0o777); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n\t\"rm\": func(hc interp.HandlerContext, args []string) error {\n\t\tfor _, arg := range args {\n\t\t\tif arg == \"-r\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpath := absPath(hc.Dir, arg)\n\t\t\tif err := os.RemoveAll(path); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n\t\"ln\": func(hc interp.HandlerContext, args []string) error {\n\t\tsymbolic := args[0] == \"-s\"\n\t\tif symbolic {\n\t\t\targs = args[1:]\n\t\t}\n\t\toldname := absPath(hc.Dir, args[0])\n\t\tnewname := absPath(hc.Dir, args[1])\n\t\tif symbolic {\n\t\t\treturn os.Symlink(oldname, newname)\n\t\t}\n\t\treturn os.Link(oldname, newname)\n\t},\n\t\"touch\": func(hc interp.HandlerContext, args []string) error {\n\t\tfilenames := args // create all arguments as filenames\n\n\t\tnewTime := time.Now()\n\t\tif args[0] == \"-t\" {\n\t\t\tif len(args) < 3 {\n\t\t\t\treturn fmt.Errorf(\"usage: touch [-t [[CC]YY]MMDDhhmm[.SS]] file\")\n\t\t\t}\n\t\t\tfilenames = args[2:] // treat the rest of the args as filenames\n\n\t\t\targ := args[1]\n\t\t\tif len(arg) > 15 {\n\t\t\t\treturn fmt.Errorf(\"usage: touch [-t [[CC]YY]MMDDhhmm[.SS]] file\")\n\t\t\t}\n\t\t\ts, err := time.Parse(\"200601021504.05\", arg)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tnewTime = s\n\t\t}\n\n\t\tfor _, arg := range filenames {\n\t\t\tif strings.HasPrefix(arg, \"-\") {\n\t\t\t\treturn fmt.Errorf(\"usage: touch [-t [[CC]YY]MMDDhhmm[.SS]] file\")\n\t\t\t}\n\t\t\tpath := absPath(hc.Dir, arg)\n\t\t\t// create the file if it does not exist\n\t\t\tf, err := os.OpenFile(path, os.O_CREATE, 0o666)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tf.Close()\n\t\t\t// change the modification and access time\n\t\t\tif err := os.Chtimes(path, newTime, newTime); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n\t\"sleep\": func(hc interp.HandlerContext, args []string) error {\n\t\tfor _, arg := range args {\n\t\t\t// assume and default unit to be in seconds\n\t\t\td, err := time.ParseDuration(fmt.Sprintf(\"%ss\", arg))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttime.Sleep(d)\n\t\t}\n\t\treturn nil\n\t},\n}\n\nfunc testExecHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\tif fn := testBuiltinsMap[args[0]]; fn != nil {\n\t\t\treturn fn(interp.HandlerCtx(ctx), args[1:])\n\t\t}\n\t\treturn next(ctx, args)\n\t}\n}\n\nfunc TestRunnerRunConfirm(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"calling bash is slow\")\n\t}\n\tif !hasBash52 {\n\t\tt.Skip(\"bash 5.2 required to run\")\n\t}\n\tt.Parallel()\n\n\tif runtime.GOOS == \"windows\" {\n\t\t// For example, it seems to treat environment variables as\n\t\t// case-sensitive, which isn't how Windows works.\n\t\tt.Skip(\"bash on Windows emulates Unix-y behavior\")\n\t}\n\tfor _, c := range runTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tif strings.Contains(c.want, \" #IGNORE\") {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tskipIfUnsupported(t, c.in)\n\t\t\tt.Parallel()\n\t\t\ttdir := t.TempDir()\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\t\t\tdefer cancel()\n\t\t\tcmd := exec.CommandContext(ctx, \"bash\")\n\t\t\tcmd.Dir = tdir\n\t\t\tcmd.Stdin = strings.NewReader(c.in)\n\t\t\tout, err := cmd.CombinedOutput()\n\t\t\tif strings.Contains(c.want, \" #JUSTERR\") {\n\t\t\t\t// bash sometimes exits with status code 0 and\n\t\t\t\t// stderr \"bash: ...\" for an error\n\t\t\t\tfauxErr := bytes.HasPrefix(out, []byte(\"bash:\"))\n\t\t\t\tif err == nil && !fauxErr {\n\t\t\t\t\tt.Fatalf(\"wanted bash to error in %q\", c.in)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgot := string(out)\n\t\t\tif err != nil {\n\t\t\t\tgot += err.Error()\n\t\t\t}\n\t\t\tif got != c.want {\n\t\t\t\tt.Fatalf(\"wrong bash output in %q:\\nwant: %q\\ngot:  %q\",\n\t\t\t\t\tc.in, c.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRunnerOpts(t *testing.T) {\n\tt.Parallel()\n\n\twithPath := func(strs ...string) func(*interp.Runner) error {\n\t\tprefix := []string{\n\t\t\t\"PATH=\" + os.Getenv(\"PATH\"),\n\t\t\t\"ENV_PROG=\" + os.Getenv(\"ENV_PROG\"),\n\t\t}\n\t\treturn interp.Env(expand.ListEnviron(append(prefix, strs...)...))\n\t}\n\topts := func(list ...interp.RunnerOption) []interp.RunnerOption {\n\t\treturn list\n\t}\n\tcases := []struct {\n\t\topts     []interp.RunnerOption\n\t\tin, want string\n\t}{\n\t\t{\n\t\t\tnil,\n\t\t\t\"$ENV_PROG | grep -i '^interp_global='\",\n\t\t\t\"INTERP_GLOBAL=value\\n\",\n\t\t},\n\t\t{\n\t\t\topts(withPath()),\n\t\t\t\"$ENV_PROG | grep -i '^interp_global='\",\n\t\t\t\"exit status 1\",\n\t\t},\n\t\t{\n\t\t\topts(withPath(\"INTERP_GLOBAL=bar\")),\n\t\t\t\"$ENV_PROG | grep -i '^interp_global='\",\n\t\t\t\"INTERP_GLOBAL=bar\\n\",\n\t\t},\n\t\t{\n\t\t\topts(withPath(\"a=b\")),\n\t\t\t\"echo $a\",\n\t\t\t\"b\\n\",\n\t\t},\n\t\t{\n\t\t\topts(withPath(\"A=b\")),\n\t\t\t\"$ENV_PROG | grep '^A='; echo $A\",\n\t\t\t\"A=b\\nb\\n\",\n\t\t},\n\t\t{\n\t\t\topts(withPath(\"A=b\", \"A=c\")),\n\t\t\t\"$ENV_PROG | grep '^A='; echo $A\",\n\t\t\t\"A=c\\nc\\n\",\n\t\t},\n\t\t{\n\t\t\topts(withPath(\"HOME=\")),\n\t\t\t\"echo $HOME\",\n\t\t\t\"\\n\",\n\t\t},\n\t\t{\n\t\t\topts(withPath(\"PWD=foo\")),\n\t\t\t\"[[ $PWD == foo ]]\",\n\t\t\t\"exit status 1\",\n\t\t},\n\t\t{\n\t\t\topts(interp.Params(\"foo\")),\n\t\t\t\"echo $@\",\n\t\t\t\"foo\\n\",\n\t\t},\n\t\t{\n\t\t\topts(interp.Params(\"-u\", \"--\", \"foo\")),\n\t\t\t\"echo $@; echo $unset\",\n\t\t\t\"foo\\nunset: unbound variable\\nexit status 1\",\n\t\t},\n\t\t{\n\t\t\topts(interp.Params(\"-u\", \"--\", \"foo\")),\n\t\t\t\"echo $@; echo ${unset:-default}\",\n\t\t\t\"foo\\ndefault\\n\",\n\t\t},\n\t\t{\n\t\t\topts(interp.Params(\"foo\")),\n\t\t\t\"set >/dev/null; echo $@\",\n\t\t\t\"foo\\n\",\n\t\t},\n\t\t{\n\t\t\topts(interp.Params(\"foo\")),\n\t\t\t\"set -e; echo $@\",\n\t\t\t\"foo\\n\",\n\t\t},\n\t\t{\n\t\t\topts(interp.Params(\"foo\")),\n\t\t\t\"set --; echo $@\",\n\t\t\t\"\\n\",\n\t\t},\n\t\t{\n\t\t\topts(interp.Params(\"foo\")),\n\t\t\t\"set bar; echo $@\",\n\t\t\t\"bar\\n\",\n\t\t},\n\t\t{\n\t\t\topts(interp.Env(expand.FuncEnviron(func(name string) string {\n\t\t\t\tif name == \"foo\" {\n\t\t\t\t\treturn \"bar\"\n\t\t\t\t}\n\t\t\t\treturn \"\"\n\t\t\t}))),\n\t\t\t\"(echo $foo); echo x | echo $foo\",\n\t\t\t\"bar\\nbar\\n\",\n\t\t},\n\t}\n\tp := syntax.NewParser()\n\tfor _, c := range cases {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tskipIfUnsupported(t, c.in)\n\t\t\tfile := parse(t, p, c.in)\n\t\t\tvar cb concBuffer\n\t\t\tr, err := interp.New(append(c.opts,\n\t\t\t\tinterp.StdIO(nil, &cb, &cb),\n\t\t\t\tinterp.ExecHandlers(testExecHandler),\n\t\t\t)...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\t\t\tdefer cancel()\n\t\t\tif err := r.Run(ctx, file); err != nil {\n\t\t\t\tcb.WriteString(err.Error())\n\t\t\t}\n\t\t\tif got := cb.String(); got != c.want {\n\t\t\t\tt.Fatalf(\"wrong output in %q:\\nwant: %q\\ngot:  %q\",\n\t\t\t\t\tc.in, c.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRunnerContext(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []string{\n\t\t\"\",\n\t\t\"while true; do true; done\",\n\t\t\"until false; do true; done\",\n\t\t\"sleep 1000\",\n\t\t\"while true; do true; done & wait\",\n\t\t\"sleep 1000 & wait\",\n\t\t\"(while true; do true; done)\",\n\t\t\"$(while true; do true; done)\",\n\t\t\"while true; do true; done | while true; do true; done\",\n\t}\n\tp := syntax.NewParser()\n\tfor _, in := range cases {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tfile := parse(t, p, in)\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\tcancel()\n\t\t\tr, _ := interp.New()\n\t\t\terrChan := make(chan error)\n\t\t\tgo func() {\n\t\t\t\terrChan <- r.Run(ctx, file)\n\t\t\t}()\n\n\t\t\ttimeout := 500 * time.Millisecond\n\t\t\tselect {\n\t\t\tcase err := <-errChan:\n\t\t\t\tif err != nil && err != ctx.Err() {\n\t\t\t\t\tt.Fatal(\"Runner did not use ctx.Err()\")\n\t\t\t\t}\n\t\t\tcase <-time.After(timeout):\n\t\t\t\tt.Fatalf(\"program was not killed in %s\", timeout)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCancelBlockedStdinRead(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\t// TODO: Why is this? The [os.File.SetReadDeadline] docs seem to imply that it should work\n\t\t// across all major platforms, and the file polling  implementation seems to be\n\t\t// for all posix platforms including Windows.\n\t\t// Our previous logic and tests with muesli/cancelreader did not test an os.Pipe\n\t\t// on Windows either, so skipping here is not any worse.\n\t\tt.Skip(\"os.Pipe on windows appears to not support cancellable reads\")\n\t}\n\tt.Parallel()\n\n\tp := syntax.NewParser()\n\tfile := parse(t, p, \"read x\")\n\tctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n\t// Make the linter happy, even though we deliberately wait for the timeout.\n\tdefer cancel()\n\n\tstdinRead, stdinWrite, err := os.Pipe()\n\tif err != nil {\n\t\tt.Fatalf(\"Error calling os.Pipe: %v\", err)\n\t}\n\tdefer func() {\n\t\tstdinWrite.Close()\n\t\tstdinRead.Close()\n\t}()\n\tr, _ := interp.New(interp.StdIO(stdinRead, nil, nil))\n\tnow := time.Now()\n\terrChan := make(chan error)\n\tgo func() {\n\t\terrChan <- r.Run(ctx, file)\n\t}()\n\n\ttimeout := 500 * time.Millisecond\n\tselect {\n\tcase err := <-errChan:\n\t\tif err == nil || err.Error() != \"exit status 1\" || ctx.Err() != context.DeadlineExceeded {\n\t\t\tt.Fatalf(\"'read x' did not timeout correctly; err: %v, ctx.Err(): %v; dur: %v\",\n\t\t\t\terr, ctx.Err(), time.Since(now))\n\t\t}\n\tcase <-time.After(timeout):\n\t\tt.Fatalf(\"program was not killed in %s\", timeout)\n\t}\n}\n\nfunc TestRunnerAltNodes(t *testing.T) {\n\tt.Parallel()\n\n\tin := \"echo foo\"\n\tfile := parse(t, nil, in)\n\twant := \"foo\\n\"\n\tnodes := []syntax.Node{\n\t\tfile,\n\t\tfile.Stmts[0],\n\t\tfile.Stmts[0].Cmd,\n\t}\n\tfor _, node := range nodes {\n\t\tvar cb concBuffer\n\t\tr, _ := interp.New(interp.StdIO(nil, &cb, &cb))\n\t\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\t\tdefer cancel()\n\t\tif err := r.Run(ctx, node); err != nil {\n\t\t\tcb.WriteString(err.Error())\n\t\t}\n\t\tif got := cb.String(); got != want {\n\t\t\tt.Fatalf(\"wrong output in %q:\\nwant: %q\\ngot:  %q\",\n\t\t\t\tin, want, got)\n\t\t}\n\t}\n}\n\nfunc TestRunnerDir(t *testing.T) {\n\tt.Parallel()\n\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Run(\"Missing\", func(t *testing.T) {\n\t\t_, err := interp.New(interp.Dir(\"missing\"))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected New to error when Dir is missing\")\n\t\t}\n\t})\n\tt.Run(\"NotDir\", func(t *testing.T) {\n\t\t_, err := interp.New(interp.Dir(\"interp_test.go\"))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected New to error when Dir is not a dir\")\n\t\t}\n\t})\n\tt.Run(\"NotDirAbs\", func(t *testing.T) {\n\t\t_, err := interp.New(interp.Dir(filepath.Join(wd, \"interp_test.go\")))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected New to error when Dir is not a dir\")\n\t\t}\n\t})\n\tt.Run(\"Relative\", func(t *testing.T) {\n\t\t// On Windows, it's impossible to make a relative path from one\n\t\t// drive to another. Use the parent directory, as that's for\n\t\t// sure in the same drive as the current directory.\n\t\trel := \"..\" + string(filepath.Separator)\n\t\tr, err := interp.New(interp.Dir(rel))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !filepath.IsAbs(r.Dir) {\n\t\t\tt.Errorf(\"Runner.Dir is not absolute\")\n\t\t}\n\t})\n\t// Ensure that we treat symlinks and short paths properly, especially\n\t// with Dir and globbing.\n\tt.Run(\"SymlinkOrShortPath\", func(t *testing.T) {\n\t\ttdir := t.TempDir()\n\n\t\trealDir := filepath.Join(tdir, \"real-long-dir-name\")\n\t\trealFile := filepath.Join(realDir, \"realfile\")\n\n\t\tif err := os.Mkdir(realDir, 0o777); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif err := os.WriteFile(realFile, []byte(\"\"), 0o666); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tvar altDir string\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tshort, err := shortPathName(realDir)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\taltDir = short\n\t\t\t// We replace tdir later, and it might have been shortened.\n\t\t\ttdir = filepath.Dir(altDir)\n\t\t} else {\n\t\t\taltDir = filepath.Join(tdir, \"symlink\")\n\t\t\tif err := os.Symlink(realDir, altDir); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t}\n\n\t\tvar b bytes.Buffer\n\t\tr, err := interp.New(interp.Dir(altDir), interp.StdIO(nil, &b, &b))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tfile := parse(t, nil, \"echo $PWD $PWD/*\")\n\t\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\t\tdefer cancel()\n\t\tif err := r.Run(ctx, file); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tgot := b.String()\n\t\tgot = strings.ReplaceAll(got, tdir, \"\")\n\t\tgot = strings.TrimSpace(got)\n\t\twant := `/symlink /symlink/realfile`\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\twant = `\\\\REAL.{4} \\\\REAL.{4}\\\\realfile`\n\t\t}\n\t\tif !regexp.MustCompile(want).MatchString(got) {\n\t\t\tt.Fatalf(\"\\nwant regexp: %q\\ngot: %q\", want, got)\n\t\t}\n\t})\n}\n\nfunc TestRunnerIncremental(t *testing.T) {\n\tt.Parallel()\n\n\tfile := parse(t, nil, \"echo foo; false; echo bar; exit 0; echo baz\")\n\twant := \"foo\\nbar\\n\"\n\tvar b bytes.Buffer\n\tr, _ := interp.New(interp.StdIO(nil, &b, &b))\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tfor _, stmt := range file.Stmts {\n\t\terr := r.Run(ctx, stmt)\n\t\tif !errors.As(err, new(interp.ExitStatus)) && err != nil {\n\t\t\t// Keep track of unexpected errors.\n\t\t\tb.WriteString(err.Error())\n\t\t}\n\t\tif r.Exited() {\n\t\t\tbreak\n\t\t}\n\t}\n\tif got := b.String(); got != want {\n\t\tt.Fatalf(\"\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n}\n\nfunc TestRunnerResetFields(t *testing.T) {\n\tt.Parallel()\n\n\ttdir := t.TempDir()\n\tlogPath := filepath.Join(tdir, \"log\")\n\tlogFile, err := os.Create(logPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer logFile.Close()\n\tr, _ := interp.New(\n\t\tinterp.Params(\"-f\", \"--\", \"first\", tdir, logPath),\n\t\tinterp.Dir(tdir),\n\t\tinterp.ExecHandlers(testExecHandler),\n\t)\n\t// Check that using option funcs and Runner fields directly is still\n\t// kept by Reset.\n\tinterp.StdIO(nil, logFile, os.Stderr)(r)\n\tr.Env = expand.ListEnviron(append(os.Environ(), \"GLOBAL=foo\")...)\n\n\tfile := parse(t, nil, `\n# Params set 3 arguments\n[[ $# -eq 3 ]] || exit 10\n[[ $1 == \"first\" ]] || exit 11\n\n# Params set the -f option (noglob)\n[[ -o noglob ]] || exit 12\n\n# $PWD was set via Dir, and should be equal to $2\n[[ \"$PWD\" == \"$2\" ]] || exit 13\n\n# stdout should go into the log file, which is at $3\necho line1\necho line2\n[[ \"$(wc -l <$3)\" == \"2\" ]] || exit 14\n\n# $GLOBAL was set directly via the Env field\n[[ \"$GLOBAL\" == \"foo\" ]] || exit 15\n\n# Change all of the above within the script. Reset should undo this.\nset +f -- newargs\ncd\nexec >/dev/null 2>/dev/null\nGLOBAL=\nexport GLOBAL=\n`)\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tfor i := range 3 {\n\t\tif err := r.Run(ctx, file); err != nil {\n\t\t\tt.Fatalf(\"run number %d: %v\", i, err)\n\t\t}\n\t\tr.Reset()\n\t\t// empty the log file too\n\t\tlogFile.Truncate(0)\n\t\tlogFile.Seek(0, io.SeekStart)\n\t}\n}\n\nfunc TestRunnerManyResets(t *testing.T) {\n\tt.Parallel()\n\tr, _ := interp.New()\n\tfor range 5 {\n\t\tr.Reset()\n\t}\n}\n\nfunc TestRunnerFilename(t *testing.T) {\n\tt.Parallel()\n\n\twant := \"f.sh\\n\"\n\tfile, _ := syntax.NewParser().Parse(strings.NewReader(\"echo $0\"), \"f.sh\")\n\tvar b bytes.Buffer\n\tr, _ := interp.New(interp.StdIO(nil, &b, &b))\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tif err := r.Run(ctx, file); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif got := b.String(); got != want {\n\t\tt.Fatalf(\"\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n}\n\nfunc TestRunnerEnvNoModify(t *testing.T) {\n\tt.Parallel()\n\n\tenv := expand.ListEnviron(\"one=1\", \"two=2\")\n\tfile := parse(t, nil, `echo -n \"$one $two; \"; one=x; unset two`)\n\n\tvar b bytes.Buffer\n\tr, _ := interp.New(interp.Env(env), interp.StdIO(nil, &b, &b))\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tfor range 3 {\n\t\tr.Reset()\n\t\terr := r.Run(ctx, file)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\n\twant := \"1 2; 1 2; 1 2; \"\n\tif got := b.String(); got != want {\n\t\tt.Fatalf(\"\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n}\n\nfunc TestMalformedPathOnWindows(t *testing.T) {\n\tif runtime.GOOS != \"windows\" {\n\t\tt.Skip(\"Skipping windows test on non-windows GOOS\")\n\t}\n\ttdir := t.TempDir()\n\tt.Parallel()\n\n\tpath := filepath.Join(tdir, \"test.cmd\")\n\tscript := []byte(\"@echo foo\")\n\tif err := os.WriteFile(path, script, 0o777); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// set PATH to c:\\tmp\\dir instead of C:\\tmp\\dir\n\tvolume := filepath.VolumeName(tdir)\n\tpathList := strings.ToLower(volume) + tdir[len(volume):]\n\n\tfile := parse(t, nil, \"test.cmd\")\n\tvar cb concBuffer\n\tr, _ := interp.New(interp.Env(expand.ListEnviron(\"PATH=\"+pathList)), interp.StdIO(nil, &cb, &cb))\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tif err := r.Run(ctx, file); err != nil {\n\t\tt.Fatal(err)\n\t}\n\twant := \"foo\\r\\n\"\n\tif got := cb.String(); got != want {\n\t\tt.Fatalf(\"wrong output:\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n}\n\nfunc TestReadShouldNotPanicWithNilStdin(t *testing.T) {\n\tt.Parallel()\n\n\tr, err := interp.New()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tf := parse(t, nil, \"read foobar\")\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tif err := r.Run(ctx, f); err == nil {\n\t\tt.Fatal(\"it should have returned an error\")\n\t}\n}\n\nfunc TestRunnerVars(t *testing.T) {\n\tt.Parallel()\n\n\tr, err := interp.New()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tf := parse(t, nil, \"foo=updated; BAR=new\")\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tif err := r.Run(ctx, f); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif want, got := \"updated\", r.Vars[\"foo\"].String(); got != want {\n\t\tt.Fatalf(\"wrong output:\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n}\n\nfunc TestRunnerSubshell(t *testing.T) {\n\tt.Parallel()\n\n\tr1, err := interp.New()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tr2 := r1.Subshell()\n\tf1 := parse(t, nil, \"PARENT=foo\")\n\tf2 := parse(t, nil, \"CHILD=bar\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tif err := r1.Run(ctx, f1); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := r2.Run(ctx, f2); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif want, got := \"foo\", r1.Vars[\"PARENT\"].String(); got != want {\n\t\tt.Fatalf(\"wrong output:\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n\tif want, got := \"bar\", r2.Vars[\"CHILD\"].String(); got != want {\n\t\tt.Fatalf(\"wrong output:\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n\n\tr3 := r2.Subshell()\n\tf3 := parse(t, nil, \"CHILD=modified\")\n\tif err := r3.Run(ctx, f3); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif want, got := \"bar\", r2.Vars[\"CHILD\"].String(); got != want {\n\t\tt.Fatalf(\"wrong output:\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n\tif want, got := \"modified\", r3.Vars[\"CHILD\"].String(); got != want {\n\t\tt.Fatalf(\"wrong output:\\nwant: %q\\ngot:  %q\", want, got)\n\t}\n}\n\nfunc TestRunnerNonFileStdin(t *testing.T) {\n\tt.Parallel()\n\n\tvar cb concBuffer\n\tr, err := interp.New(interp.StdIO(strings.NewReader(\"a\\nb\\nc\\n\"), &cb, &cb))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfile := parse(t, nil, \"while read a; do echo $a; GOSH_CMD=print_ok $GOSH_PROG; done\")\n\tctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout)\n\tdefer cancel()\n\tif err := r.Run(ctx, file); err != nil {\n\t\tcb.WriteString(err.Error())\n\t}\n\t// TODO: just like with heredocs, the first print_ok call consumes all stdin.\n\tqt.Assert(t, qt.Equals(cb.String(), \"a\\nexec ok\\nb\\nexec ok\\nc\\nexec ok\\n\"))\n}\n"
  },
  {
    "path": "interp/os_notunix.go",
    "content": "// Copyright (c) 2017, Andrey Nering <andrey.nering@gmail.com>\n// See LICENSE for licensing information\n\n//go:build !unix\n\npackage interp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc mkfifo(path string, mode uint32) error {\n\treturn fmt.Errorf(\"unsupported\")\n}\n\n// access attempts to emulate [unix.Access] on Windows.\n// Windows seems to have a different system of permissions than Unix,\n// so for now just rely on what [io/fs.FileInfo] gives us.\nfunc (r *Runner) access(ctx context.Context, path string, mode uint32) error {\n\tinfo, err := r.lstat(ctx, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tm := info.Mode()\n\tswitch mode {\n\tcase access_R_OK:\n\t\tif m&0o400 == 0 {\n\t\t\treturn fmt.Errorf(\"file is not readable\")\n\t\t}\n\tcase access_W_OK:\n\t\tif m&0o200 == 0 {\n\t\t\treturn fmt.Errorf(\"file is not writable\")\n\t\t}\n\tcase access_X_OK:\n\t\tif m&0o100 == 0 {\n\t\t\treturn fmt.Errorf(\"file is not executable\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// unTestOwnOrGrp panics. Under Unix, it implements the -O and -G unary tests,\n// but under Windows, it's unclear how to implement those tests, since Windows\n// doesn't have the concept of a file owner, just ACLs, and it's unclear how\n// to map the one to the other.\nfunc (r *Runner) unTestOwnOrGrp(ctx context.Context, op syntax.UnTestOperator, x string) bool {\n\tpanic(fmt.Sprintf(\"unhandled unary test op: %v\", op))\n}\n\n// waitStatus is a no-op on plan9 and windows.\ntype waitStatus struct{}\n\nfunc (waitStatus) Signaled() bool { return false }\nfunc (waitStatus) Signal() int    { return 0 }\n"
  },
  {
    "path": "interp/os_unix.go",
    "content": "// Copyright (c) 2017, Andrey Nering <andrey.nering@gmail.com>\n// See LICENSE for licensing information\n\n//go:build unix\n\npackage interp\n\nimport (\n\t\"context\"\n\t\"os/user\"\n\t\"strconv\"\n\t\"syscall\"\n\n\t\"golang.org/x/sys/unix\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc mkfifo(path string, mode uint32) error {\n\treturn unix.Mkfifo(path, mode)\n}\n\n// access is similar to checking the permission bits from [io/fs.FileInfo],\n// but it also takes into account the current user's role.\nfunc (r *Runner) access(ctx context.Context, path string, mode uint32) error {\n\t// TODO(v4): \"access\" may need to become part of a handler, like \"open\" or \"stat\".\n\treturn unix.Access(path, mode)\n}\n\n// unTestOwnOrGrp implements the -O and -G unary tests. If the file does not\n// exist, or the current user cannot be retrieved, returns false.\nfunc (r *Runner) unTestOwnOrGrp(ctx context.Context, op syntax.UnTestOperator, x string) bool {\n\tinfo, err := r.stat(ctx, x)\n\tif err != nil {\n\t\treturn false\n\t}\n\tu, err := user.Current()\n\tif err != nil {\n\t\treturn false\n\t}\n\tif op == syntax.TsUsrOwn {\n\t\tuid, _ := strconv.Atoi(u.Uid)\n\t\treturn uint32(uid) == info.Sys().(*syscall.Stat_t).Uid\n\t}\n\tgid, _ := strconv.Atoi(u.Gid)\n\treturn uint32(gid) == info.Sys().(*syscall.Stat_t).Gid\n}\n\ntype waitStatus = syscall.WaitStatus\n"
  },
  {
    "path": "interp/runner.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"iter\"\n\t\"math\"\n\tmathrand \"math/rand/v2\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/internal\"\n\t\"mvdan.cc/sh/v3/pattern\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nconst (\n\t// shellReplyPS3Var, or PS3, is a special variable in Bash used by the select command,\n\t// while the shell is awaiting for input. the default value is [shellDefaultPS3]\n\tshellReplyPS3Var = \"PS3\"\n\t// shellDefaultPS3, or #?, is PS3's default value\n\tshellDefaultPS3 = \"#? \"\n\t// shellReplyVar, or REPLY, is a special variable in Bash that is used to store the result of\n\t// the select command or of the read command, when no variable name is specified\n\tshellReplyVar = \"REPLY\"\n\n\tfifoNamePrefix = \"sh-interp-\"\n)\n\nfunc (r *Runner) fillExpandConfig(ctx context.Context) {\n\tr.ectx = ctx\n\tr.ecfg = &expand.Config{\n\t\tEnv: expandEnv{r},\n\t\tCmdSubst: func(w io.Writer, cs *syntax.CmdSubst) error {\n\t\t\tswitch len(cs.Stmts) {\n\t\t\tcase 0: // nothing to do\n\t\t\t\treturn nil\n\t\t\tcase 1: // $(<file)\n\t\t\t\tword := catShortcutArg(cs.Stmts[0])\n\t\t\t\tif word == nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tpath := r.literal(word)\n\t\t\t\tf, err := r.open(ctx, path, os.O_RDONLY, 0, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t_, err = io.Copy(w, f)\n\t\t\t\tf.Close()\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tr2 := r.subshell(false)\n\t\t\tr2.stdout = w\n\t\t\tr2.stmts(ctx, cs.Stmts)\n\t\t\tr2.exit.exiting = false // subshells don't exit the parent shell\n\t\t\tr.lastExpandExit = r2.exit\n\t\t\tif r2.exit.fatalExit {\n\t\t\t\treturn r2.exit.err // surface fatal errors immediately\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t\tProcSubst: func(ps *syntax.ProcSubst) (string, error) {\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\treturn \"\", fmt.Errorf(\"TODO: support process substitution on Windows\")\n\t\t\t}\n\t\t\tif len(ps.Stmts) == 0 { // nothing to do\n\t\t\t\treturn os.DevNull, nil\n\t\t\t}\n\n\t\t\t// We can't atomically create a random unused temporary FIFO.\n\t\t\t// Similar to [os.CreateTemp],\n\t\t\t// keep trying new random paths until one does not exist.\n\t\t\t// We use a uint64 because a uint32 easily runs into retries.\n\t\t\tvar path string\n\t\t\ttry := 0\n\t\t\tfor {\n\t\t\t\tpath = filepath.Join(r.tempDir, fifoNamePrefix+strconv.FormatUint(mathrand.Uint64(), 16))\n\t\t\t\terr := mkfifo(path, 0o666)\n\t\t\t\tif err == nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif !os.IsExist(err) {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"cannot create fifo: %v\", err)\n\t\t\t\t}\n\t\t\t\tif try++; try > 100 {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"giving up at creating fifo: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tr2 := r.subshell(true)\n\t\t\tstdout := r.origStdout\n\t\t\t// TODO: note that `man bash` mentions that `wait` only waits for the last\n\t\t\t// process substitution as long as it is $!; the logic here would mean we wait for all of them.\n\t\t\tbg := bgProc{\n\t\t\t\tdone: make(chan struct{}),\n\t\t\t\texit: new(exitStatus),\n\t\t\t}\n\t\t\tr.bgProcs = append(r.bgProcs, bg)\n\t\t\tgo func() {\n\t\t\t\tdefer func() {\n\t\t\t\t\t*bg.exit = r2.exit\n\t\t\t\t\tclose(bg.done)\n\t\t\t\t}()\n\t\t\t\tswitch ps.Op {\n\t\t\t\tcase syntax.CmdIn:\n\t\t\t\t\tf, err := os.OpenFile(path, os.O_WRONLY, 0)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tr.errf(\"cannot open fifo for stdout: %v\\n\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tr2.stdout = f\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\tif err := f.Close(); err != nil {\n\t\t\t\t\t\t\tr.errf(\"closing stdout fifo: %v\\n\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tos.Remove(path)\n\t\t\t\t\t}()\n\t\t\t\tcase syntax.CmdOut:\n\t\t\t\t\tf, err := os.OpenFile(path, os.O_RDONLY, 0)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tr.errf(\"cannot open fifo for stdin: %v\\n\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tr2.stdin = f\n\t\t\t\t\tr2.stdout = stdout\n\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\tf.Close()\n\t\t\t\t\t\tos.Remove(path)\n\t\t\t\t\t}()\n\t\t\t\tdefault:\n\t\t\t\t\tpanic(fmt.Sprintf(\"unsupported process substitution operator: %q\", ps.Op))\n\t\t\t\t}\n\t\t\t\tr2.stmts(ctx, ps.Stmts)\n\t\t\t\tr2.exit.exiting = false // subshells don't exit the parent shell\n\t\t\t}()\n\t\t\treturn path, nil\n\t\t},\n\t}\n\tr.updateExpandOpts()\n}\n\n// catShortcutArg checks if a statement is of the form \"$(<file)\". The redirect\n// word is returned if there's a match, and nil otherwise.\nfunc catShortcutArg(stmt *syntax.Stmt) *syntax.Word {\n\tif stmt.Cmd != nil || stmt.Negated || stmt.Background || stmt.Coprocess || stmt.Disown {\n\t\treturn nil\n\t}\n\tif len(stmt.Redirs) != 1 {\n\t\treturn nil\n\t}\n\tredir := stmt.Redirs[0]\n\tif redir.Op != syntax.RdrIn {\n\t\treturn nil\n\t}\n\treturn redir.Word\n}\n\nfunc (r *Runner) updateExpandOpts() {\n\tif r.opts[optNoGlob] {\n\t\tr.ecfg.ReadDir2 = nil\n\t} else {\n\t\tr.ecfg.ReadDir2 = func(s string) ([]fs.DirEntry, error) {\n\t\t\treturn r.readDirHandler(r.handlerCtx(r.ectx, handlerKindReadDir, todoPos), s)\n\t\t}\n\t}\n\tr.ecfg.GlobStar = r.opts[optGlobStar]\n\tr.ecfg.DotGlob = r.opts[optDotGlob]\n\tr.ecfg.NoCaseGlob = r.opts[optNoCaseGlob]\n\tr.ecfg.NullGlob = r.opts[optNullGlob]\n\tr.ecfg.NoUnset = r.opts[optNoUnset]\n\tr.ecfg.ExtGlob = r.opts[optExtGlob]\n}\n\nfunc (r *Runner) expandErr(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\terrMsg := err.Error()\n\tfmt.Fprintln(r.stderr, errMsg)\n\tswitch {\n\tcase errors.As(err, &expand.UnsetParameterError{}):\n\tcase errMsg == \"invalid indirect expansion\":\n\t\t// TODO: These errors are treated as fatal by bash.\n\t\t// Make the error type reflect that.\n\tdefault:\n\t\treturn // other cases do not exit\n\t}\n\tr.exit.code = 1\n\tr.exit.exiting = true\n}\n\nfunc (r *Runner) arithm(expr syntax.ArithmExpr) int {\n\tn, err := expand.Arithm(r.ecfg, expr)\n\tr.expandErr(err)\n\treturn n\n}\n\nfunc (r *Runner) fields(words ...*syntax.Word) []string {\n\tstrs, err := expand.Fields(r.ecfg, words...)\n\tr.expandErr(err)\n\treturn strs\n}\n\nfunc (r *Runner) literal(word *syntax.Word) string {\n\tstr, err := expand.Literal(r.ecfg, word)\n\tr.expandErr(err)\n\treturn str\n}\n\nfunc (r *Runner) document(word *syntax.Word) string {\n\tstr, err := expand.Document(r.ecfg, word)\n\tr.expandErr(err)\n\treturn str\n}\n\nfunc (r *Runner) pattern(word *syntax.Word) string {\n\tstr, err := expand.Pattern(r.ecfg, word)\n\tr.expandErr(err)\n\treturn str\n}\n\n// expandEnviron exposes [Runner]'s variables to the expand package.\ntype expandEnv struct {\n\tr *Runner\n}\n\nvar _ expand.WriteEnviron = expandEnv{}\n\nfunc (e expandEnv) Get(name string) expand.Variable {\n\treturn e.r.lookupVar(name)\n}\n\nfunc (e expandEnv) Set(name string, vr expand.Variable) error {\n\te.r.setVar(name, vr)\n\treturn nil // TODO: return any errors\n}\n\nfunc (e expandEnv) Each(fn func(name string, vr expand.Variable) bool) {\n\te.r.writeEnv.Each(fn)\n}\n\nvar todoPos syntax.Pos // for handlerCtx callers where we don't yet have a position\n\nfunc (r *Runner) handlerCtx(ctx context.Context, kind handlerKind, pos syntax.Pos) context.Context {\n\thc := HandlerContext{\n\t\trunner: r,\n\t\tkind:   kind,\n\t\tEnv:    &overlayEnviron{parent: r.writeEnv},\n\t\tDir:    r.Dir,\n\t\tPos:    pos,\n\t\tStdout: r.stdout,\n\t\tStderr: r.stderr,\n\t}\n\tif r.stdin != nil { // do not leave hc.Stdin as a typed nil\n\t\thc.Stdin = r.stdin\n\t}\n\treturn context.WithValue(ctx, handlerCtxKey{}, hc)\n}\n\nfunc (r *Runner) out(s string) {\n\tio.WriteString(r.stdout, s)\n}\n\nfunc (r *Runner) outf(format string, a ...any) {\n\tfmt.Fprintf(r.stdout, format, a...)\n}\n\nfunc (r *Runner) errf(format string, a ...any) {\n\tfmt.Fprintf(r.stderr, format, a...)\n}\n\nfunc (r *Runner) stop(ctx context.Context) bool {\n\t// Some traps trigger on exit, so we do want those to run.\n\tif !r.handlingTrap && (r.exit.returning || r.exit.exiting) {\n\t\treturn true\n\t}\n\tif err := ctx.Err(); err != nil {\n\t\tr.exit.fatal(err)\n\t\treturn true\n\t}\n\tif r.opts[optNoExec] {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (r *Runner) stmt(ctx context.Context, st *syntax.Stmt) {\n\tif r.stop(ctx) {\n\t\treturn\n\t}\n\tr.exit = exitStatus{}\n\tif st.Background || st.Disown {\n\t\tr2 := r.subshell(true)\n\t\tst2 := *st\n\t\tst2.Background = false\n\t\tst2.Disown = false\n\t\tbg := bgProc{\n\t\t\tdone: make(chan struct{}),\n\t\t\texit: new(exitStatus),\n\t\t}\n\t\tr.bgProcs = append(r.bgProcs, bg)\n\t\tgo func() {\n\t\t\tr2.Run(ctx, &st2)\n\t\t\tr2.exit.exiting = false // subshells don't exit the parent shell\n\t\t\t*bg.exit = r2.exit\n\t\t\tclose(bg.done)\n\t\t}()\n\t} else {\n\t\tr.stmtSync(ctx, st)\n\t}\n\tr.lastExit = r.exit\n}\n\nfunc (r *Runner) stmtSync(ctx context.Context, st *syntax.Stmt) {\n\toldIn, oldOut, oldErr := r.stdin, r.stdout, r.stderr\n\tfor _, rd := range st.Redirs {\n\t\tcls, err := r.redir(ctx, rd)\n\t\tif err != nil {\n\t\t\tr.exit.code = 1\n\t\t\tbreak\n\t\t}\n\t\tif cls != nil {\n\t\t\tdefer cls.Close()\n\t\t}\n\t}\n\tif r.exit.ok() && st.Cmd != nil {\n\t\tr.cmd(ctx, st.Cmd)\n\t}\n\tif st.Negated {\n\t\tif r.exit.ok() {\n\t\t\tr.exit.code = 1\n\t\t} else {\n\t\t\tr.exit.clear()\n\t\t}\n\t} else if b, ok := st.Cmd.(*syntax.BinaryCmd); ok && (b.Op == syntax.AndStmt || b.Op == syntax.OrStmt) {\n\t} else if !r.exit.ok() && !r.noErrExit {\n\t\tr.trapCallback(ctx, r.callbackErr, \"error\")\n\t\t// If the \"errexit\" option is set and a command failed, exit the shell. Exceptions:\n\t\t//\n\t\t//   conditions (if <cond>, while <cond>, etc)\n\t\t//   part of && or || lists; excluded via \"else\" above\n\t\t//   preceded by !; excluded via \"else\" above\n\t\tif r.opts[optErrExit] {\n\t\t\tr.exit.exiting = true\n\t\t}\n\t}\n\tif !r.keepRedirs {\n\t\tr.stdin, r.stdout, r.stderr = oldIn, oldOut, oldErr\n\t}\n}\n\nfunc (r *Runner) cmd(ctx context.Context, cm syntax.Command) {\n\tif r.stop(ctx) {\n\t\treturn\n\t}\n\n\ttracingEnabled := r.opts[optXTrace]\n\ttrace := r.tracer()\n\n\tswitch cm := cm.(type) {\n\tcase *syntax.Block:\n\t\tr.stmts(ctx, cm.Stmts)\n\tcase *syntax.Subshell:\n\t\tr2 := r.subshell(false)\n\t\tr2.stmts(ctx, cm.Stmts)\n\t\tr2.exit.exiting = false // subshells don't exit the parent shell\n\t\tr.exit = r2.exit\n\tcase *syntax.CallExpr:\n\t\t// Use a new slice, to not modify the slice in the alias map.\n\t\targs := cm.Args\n\t\tfor i := 0; i < len(args); {\n\t\t\tif !r.opts[optExpandAliases] {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tals, ok := r.alias[args[i].Lit()]\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\targs = slices.Replace(args, i, i+1, als.args...)\n\t\t\tif !als.blank {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ti += len(als.args)\n\t\t}\n\t\tr.lastExpandExit = exitStatus{}\n\t\tfields := r.fields(args...)\n\t\tif len(fields) == 0 {\n\t\t\tfor _, as := range cm.Assigns {\n\t\t\t\tprev := r.lookupVar(as.Name.Value)\n\t\t\t\t// Here we have a naked \"foo=bar\", so if we inherited a local var from a parent\n\t\t\t\t// function we want to signal that we are modifying the parent var rather than\n\t\t\t\t// creating a new local var via \"local foo=bar\".\n\t\t\t\t// TODO: there is likely a better way to do this.\n\t\t\t\tprev.Local = false\n\n\t\t\t\tvr := r.assignVal(prev, as, \"\")\n\t\t\t\tr.setVarWithIndex(prev, as.Name.Value, as.Index, vr)\n\n\t\t\t\tif !tracingEnabled {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Strangely enough, it seems like Bash prints original\n\t\t\t\t// source for arrays, but the expanded value otherwise.\n\t\t\t\t// TODO: add test cases for x[i]=y and x+=y.\n\t\t\t\tif as.Array != nil {\n\t\t\t\t\ttrace.expr(as)\n\t\t\t\t} else if as.Value != nil {\n\t\t\t\t\tval, err := syntax.Quote(vr.String(), syntax.LangBash)\n\t\t\t\t\tif err != nil { // should never happen\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\t\t\t\t\ttrace.stringf(\"%s=%s\", as.Name.Value, val)\n\t\t\t\t}\n\t\t\t\ttrace.newLineFlush()\n\t\t\t}\n\t\t\t// If interpreting the last expansion like $(foo) failed,\n\t\t\t// and the expansion and assignments otherwise succeeded,\n\t\t\t// we need to surface that last exit code.\n\t\t\tif r.exit.ok() {\n\t\t\t\tr.exit = r.lastExpandExit\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\ttype restoreVar struct {\n\t\t\tname string\n\t\t\tvr   expand.Variable\n\t\t}\n\t\tvar restores []restoreVar\n\n\t\tfor _, as := range cm.Assigns {\n\t\t\tname := as.Name.Value\n\t\t\tprev := r.lookupVar(name)\n\n\t\t\tvr := r.assignVal(prev, as, \"\")\n\t\t\t// Inline command vars are always exported.\n\t\t\tvr.Exported = true\n\n\t\t\trestores = append(restores, restoreVar{name, prev})\n\n\t\t\tr.setVar(name, vr)\n\t\t}\n\n\t\ttrace.call(fields[0], fields[1:]...)\n\t\ttrace.newLineFlush()\n\n\t\tr.call(ctx, cm.Args[0].Pos(), fields)\n\t\tfor _, restore := range restores {\n\t\t\tr.setVar(restore.name, restore.vr)\n\t\t}\n\tcase *syntax.BinaryCmd:\n\t\tswitch cm.Op {\n\t\tcase syntax.AndStmt, syntax.OrStmt:\n\t\t\toldNoErrExit := r.noErrExit\n\t\t\tr.noErrExit = true\n\t\t\tr.stmt(ctx, cm.X)\n\t\t\tr.noErrExit = oldNoErrExit\n\t\t\tif r.exit.ok() == (cm.Op == syntax.AndStmt) {\n\t\t\t\tr.stmt(ctx, cm.Y)\n\t\t\t}\n\t\tcase syntax.Pipe, syntax.PipeAll:\n\t\t\tpr, pw, err := os.Pipe()\n\t\t\tif err != nil {\n\t\t\t\tr.exit.fatal(err) // not being able to create a pipe is rare but critical\n\t\t\t\treturn\n\t\t\t}\n\t\t\tr2 := r.subshell(true)\n\t\t\tr2.stdout = pw\n\t\t\tif cm.Op == syntax.PipeAll {\n\t\t\t\tr2.stderr = pw\n\t\t\t} else {\n\t\t\t\tr2.stderr = r.stderr\n\t\t\t}\n\t\t\tr.stdin = pr\n\t\t\tvar wg sync.WaitGroup\n\t\t\twg.Go(func() {\n\t\t\t\tr2.stmt(ctx, cm.X)\n\t\t\t\tr2.exit.exiting = false // subshells don't exit the parent shell\n\t\t\t\tpw.Close()\n\t\t\t})\n\t\t\tr.stmt(ctx, cm.Y)\n\t\t\tpr.Close()\n\t\t\twg.Wait()\n\t\t\tif r.opts[optPipeFail] && !r2.exit.ok() && r.exit.ok() {\n\t\t\t\tr.exit = r2.exit\n\t\t\t}\n\t\t\tif r2.exit.fatalExit {\n\t\t\t\tr.exit.fatal(r2.exit.err) // surface fatal errors immediately\n\t\t\t}\n\t\t}\n\tcase *syntax.IfClause:\n\t\toldNoErrExit := r.noErrExit\n\t\tr.noErrExit = true\n\t\tr.stmts(ctx, cm.Cond)\n\t\tr.noErrExit = oldNoErrExit\n\n\t\tif r.exit.ok() {\n\t\t\tr.stmts(ctx, cm.Then)\n\t\t\tbreak\n\t\t}\n\t\tr.exit.clear()\n\t\tif cm.Else != nil {\n\t\t\tr.cmd(ctx, cm.Else)\n\t\t}\n\tcase *syntax.WhileClause:\n\t\tfor !r.stop(ctx) {\n\t\t\toldNoErrExit := r.noErrExit\n\t\t\tr.noErrExit = true\n\t\t\tr.stmts(ctx, cm.Cond)\n\t\t\tr.noErrExit = oldNoErrExit\n\n\t\t\tstop := r.exit.ok() == cm.Until\n\t\t\tr.exit.clear()\n\t\t\tif stop || r.loopStmtsBroken(ctx, cm.Do) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tcase *syntax.ForClause:\n\t\tswitch y := cm.Loop.(type) {\n\t\tcase *syntax.WordIter:\n\t\t\tname := y.Name.Value\n\t\t\titems := r.Params // for i; do ...\n\n\t\t\tinToken := y.InPos.IsValid()\n\t\t\tif inToken {\n\t\t\t\titems = r.fields(y.Items...) // for i in ...; do ...\n\t\t\t}\n\n\t\t\tif cm.Select {\n\t\t\t\tps3 := shellDefaultPS3\n\t\t\t\tif e := r.envGet(shellReplyPS3Var); e != \"\" {\n\t\t\t\t\tps3 = e\n\t\t\t\t}\n\n\t\t\t\tprompt := func() []byte {\n\t\t\t\t\t// display menu\n\t\t\t\t\tfor i, word := range items {\n\t\t\t\t\t\tr.errf(\"%d) %v\\n\", i+1, word)\n\t\t\t\t\t}\n\t\t\t\t\tr.errf(\"%s\", ps3)\n\n\t\t\t\t\tline, err := r.readLine(ctx, true)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tr.exit.code = 1\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\treturn line\n\t\t\t\t}\n\n\t\t\tretry:\n\t\t\t\tchoice := prompt()\n\t\t\t\tif len(choice) == 0 {\n\t\t\t\t\tgoto retry // no reply; try again\n\t\t\t\t}\n\n\t\t\t\treply := string(choice)\n\t\t\t\tr.setVarString(shellReplyVar, reply)\n\n\t\t\t\tc, _ := strconv.Atoi(reply)\n\t\t\t\tif c > 0 && c <= len(items) {\n\t\t\t\t\tr.setVarString(name, items[c-1])\n\t\t\t\t}\n\n\t\t\t\t// execute commands until break or return is encountered\n\t\t\t\tif r.loopStmtsBroken(ctx, cm.Do) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor _, field := range items {\n\t\t\t\tr.setVarString(name, field)\n\t\t\t\ttrace.stringf(\"for %s in\", y.Name.Value)\n\t\t\t\tif inToken {\n\t\t\t\t\tfor _, item := range y.Items {\n\t\t\t\t\t\ttrace.string(\" \")\n\t\t\t\t\t\ttrace.expr(item)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttrace.string(` \"$@\"`)\n\t\t\t\t}\n\t\t\t\ttrace.newLineFlush()\n\t\t\t\tif r.loopStmtsBroken(ctx, cm.Do) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\tcase *syntax.CStyleLoop:\n\t\t\tif y.Init != nil {\n\t\t\t\tr.arithm(y.Init)\n\t\t\t}\n\t\t\tfor y.Cond == nil || r.arithm(y.Cond) != 0 {\n\t\t\t\tif !r.exit.ok() || r.loopStmtsBroken(ctx, cm.Do) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif y.Post != nil {\n\t\t\t\t\tr.arithm(y.Post)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcase *syntax.FuncDecl:\n\t\tr.setFunc(cm.Name.Value, cm.Body)\n\tcase *syntax.ArithmCmd:\n\t\tr.exit.oneIf(r.arithm(cm.X) == 0)\n\tcase *syntax.LetClause:\n\t\tvar val int\n\t\tfor _, expr := range cm.Exprs {\n\t\t\tval = r.arithm(expr)\n\n\t\t\tif !tracingEnabled {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch expr := expr.(type) {\n\t\t\tcase *syntax.Word:\n\t\t\t\tqs, err := syntax.Quote(r.literal(expr), syntax.LangBash)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttrace.stringf(\"let %v\", qs)\n\t\t\tcase *syntax.BinaryArithm, *syntax.UnaryArithm:\n\t\t\t\ttrace.expr(cm)\n\t\t\tcase *syntax.ParenArithm:\n\t\t\t\t// TODO\n\t\t\t}\n\t\t}\n\n\t\ttrace.newLineFlush()\n\t\tr.exit.oneIf(val == 0)\n\tcase *syntax.CaseClause:\n\t\ttrace.string(\"case \")\n\t\ttrace.expr(cm.Word)\n\t\ttrace.string(\" in\")\n\t\ttrace.newLineFlush()\n\t\tstr := r.literal(cm.Word)\n\t\tfor _, ci := range cm.Items {\n\t\t\tfor _, word := range ci.Patterns {\n\t\t\t\tpattern := r.pattern(word)\n\t\t\t\tif match(pattern, str) {\n\t\t\t\t\tr.stmts(ctx, ci.Stmts)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcase *syntax.TestClause:\n\t\tif r.bashTest(ctx, cm.X, false) == \"\" && r.exit.ok() {\n\t\t\t// to preserve exit status code 2 for regex errors, etc\n\t\t\tr.exit.code = 1\n\t\t}\n\tcase *syntax.DeclClause:\n\t\tlocal, global := false, false\n\t\tvar modes []string\n\t\tvalType := \"\"\n\t\tdeclQuery := \"\" // \"-f\" or \"-p\" for query mode\n\t\tswitch cm.Variant.Value {\n\t\tcase \"declare\":\n\t\t\t// When used in a function, \"declare\" acts as \"local\"\n\t\t\t// unless the \"-g\" option is used.\n\t\t\tlocal = r.inFunc\n\t\tcase \"local\":\n\t\t\tif !r.inFunc {\n\t\t\t\tr.errf(\"local: can only be used in a function\\n\")\n\t\t\t\tr.exit.code = 1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlocal = true\n\t\tcase \"export\":\n\t\t\tmodes = append(modes, \"-x\")\n\t\tcase \"readonly\":\n\t\t\tmodes = append(modes, \"-r\")\n\t\tcase \"nameref\":\n\t\t\tvalType = \"-n\"\n\t\t}\n\tassignLoop:\n\t\tfor as := range r.flattenAssigns(cm.Args) {\n\t\t\tfp := flagParser{remaining: []string{as.Name.Value}}\n\t\t\tfor fp.more() {\n\t\t\t\tswitch flag := fp.flag(); flag {\n\t\t\t\tcase \"-x\", \"-r\":\n\t\t\t\t\tmodes = append(modes, flag)\n\t\t\t\tcase \"-a\", \"-A\", \"-n\":\n\t\t\t\t\tvalType = flag\n\t\t\t\tcase \"-g\":\n\t\t\t\t\tglobal = true\n\t\t\t\tcase \"-f\", \"-p\":\n\t\t\t\t\tdeclQuery = flag\n\t\t\t\tdefault:\n\t\t\t\t\tr.errf(\"declare: invalid option %q\\n\", flag)\n\t\t\t\t\tr.exit.code = 2\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcontinue assignLoop\n\t\t\t}\n\t\t\tname := as.Name.Value\n\t\t\tif !syntax.ValidName(name) {\n\t\t\t\tr.errf(\"declare: invalid name %q\\n\", name)\n\t\t\t\tr.exit.code = 1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif declQuery == \"-f\" {\n\t\t\t\t// declare -f name: print function definition.\n\t\t\t\t// Bash silently returns exit 1 for missing functions.\n\t\t\t\tif body := r.Funcs[name]; body != nil {\n\t\t\t\t\tr.outf(\"%s()\\n\", name)\n\t\t\t\t\tprinter := syntax.NewPrinter()\n\t\t\t\t\tvar buf bytes.Buffer\n\t\t\t\t\tprinter.Print(&buf, body)\n\t\t\t\t\tr.outf(\"%s\\n\", buf.String())\n\t\t\t\t} else {\n\t\t\t\t\tr.exit.code = 1\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif declQuery == \"-p\" {\n\t\t\t\t// declare -p name: print variable with attributes.\n\t\t\t\tvr := r.lookupVar(name)\n\t\t\t\tif !vr.Declared() {\n\t\t\t\t\tr.errf(\"declare: %s: not found\\n\", name)\n\t\t\t\t\tr.exit.code = 1\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tflags := vr.Flags()\n\t\t\t\tif flags == \"\" {\n\t\t\t\t\tflags = \"-\"\n\t\t\t\t}\n\t\t\t\tswitch vr.Kind {\n\t\t\t\tcase expand.Indexed:\n\t\t\t\t\tr.outf(\"declare -%s %s=(\", flags, name)\n\t\t\t\t\tfor i, v := range vr.List {\n\t\t\t\t\t\tif i > 0 {\n\t\t\t\t\t\t\tr.out(\" \")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tr.outf(\"[%d]=%q\", i, v)\n\t\t\t\t\t}\n\t\t\t\t\tr.out(\")\\n\")\n\t\t\t\tcase expand.Associative:\n\t\t\t\t\tr.outf(\"declare -%s %s=(\", flags, name)\n\t\t\t\t\tfirst := true\n\t\t\t\t\tfor k, v := range vr.Map {\n\t\t\t\t\t\tif !first {\n\t\t\t\t\t\t\tr.out(\" \")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tr.outf(\"[%s]=%q\", k, v)\n\t\t\t\t\t\tfirst = false\n\t\t\t\t\t}\n\t\t\t\t\tr.out(\")\\n\")\n\t\t\t\tdefault:\n\t\t\t\t\tr.outf(\"declare -%s %s=%q\\n\", flags, name, vr.Str)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvr := r.lookupVar(as.Name.Value)\n\t\t\tif as.Naked {\n\t\t\t\tif valType == \"-A\" {\n\t\t\t\t\tvr.Kind = expand.Associative\n\t\t\t\t} else {\n\t\t\t\t\tvr.Kind = expand.KeepValue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tvr = r.assignVal(vr, as, valType)\n\t\t\t}\n\t\t\tif global {\n\t\t\t\tvr.Local = false\n\t\t\t} else if local {\n\t\t\t\tvr.Local = true\n\t\t\t}\n\t\t\tfor _, mode := range modes {\n\t\t\t\tswitch mode {\n\t\t\t\tcase \"-x\":\n\t\t\t\t\tvr.Exported = true\n\t\t\t\tcase \"-r\":\n\t\t\t\t\tvr.ReadOnly = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tr.setVar(name, vr)\n\t\t}\n\tcase *syntax.TimeClause:\n\t\tstart := time.Now()\n\t\tif cm.Stmt != nil {\n\t\t\tr.stmt(ctx, cm.Stmt)\n\t\t}\n\t\tformat := \"%s\\t%s\\n\"\n\t\tif cm.PosixFormat {\n\t\t\tformat = \"%s %s\\n\"\n\t\t} else {\n\t\t\tr.outf(\"\\n\")\n\t\t}\n\t\treal := time.Since(start)\n\t\tr.outf(format, \"real\", elapsedString(real, cm.PosixFormat))\n\t\t// TODO: can we do these?\n\t\tr.outf(format, \"user\", elapsedString(0, cm.PosixFormat))\n\t\tr.outf(format, \"sys\", elapsedString(0, cm.PosixFormat))\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled command node: %T\", cm))\n\t}\n}\n\nfunc (r *Runner) trapCallback(ctx context.Context, callback, name string) {\n\tif callback == \"\" {\n\t\treturn // nothing to do\n\t}\n\tif r.handlingTrap {\n\t\treturn // don't recurse, as that could lead to cycles\n\t}\n\tr.handlingTrap = true\n\n\tp := syntax.NewParser()\n\t// TODO: do this parsing when \"trap\" is called?\n\tfile, err := p.Parse(strings.NewReader(callback), name+\" trap\")\n\tif err != nil {\n\t\tr.errf(name+\"trap: %v\\n\", err)\n\t\t// ignore errors in the callback\n\t\treturn\n\t}\n\toldExit := r.exit\n\tr.stmts(ctx, file.Stmts)\n\tr.exit = oldExit // traps on EXIT or ERR should not modify the result\n\n\tr.handlingTrap = false\n}\n\nfunc (r *Runner) flattenAssigns(args []*syntax.Assign) iter.Seq[*syntax.Assign] {\n\treturn func(yield func(*syntax.Assign) bool) {\n\t\tfor _, as := range args {\n\t\t\t// Convert \"declare $x\" into \"declare value\".\n\t\t\t// Don't use syntax.Parser here, as we only want the basic\n\t\t\t// splitting by '='.\n\t\t\tif as.Name != nil {\n\t\t\t\tif !yield(as) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, field := range r.fields(as.Value) {\n\t\t\t\tas := &syntax.Assign{}\n\t\t\t\tname, val, ok := strings.Cut(field, \"=\")\n\t\t\t\tas.Name = &syntax.Lit{Value: name}\n\t\t\t\tif !ok {\n\t\t\t\t\tas.Naked = true\n\t\t\t\t} else {\n\t\t\t\t\tas.Value = &syntax.Word{Parts: []syntax.WordPart{\n\t\t\t\t\t\t&syntax.Lit{Value: val},\n\t\t\t\t\t}}\n\t\t\t\t}\n\t\t\t\tif !yield(as) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc match(pat, name string) bool {\n\tmatcher, err := internal.ExtendedPatternMatcher(pat, pattern.EntireString|pattern.ExtendedOperators)\n\t_ = err // TODO: report these errors\n\treturn matcher != nil && matcher(name)\n}\n\nfunc elapsedString(d time.Duration, posix bool) string {\n\tif posix {\n\t\treturn fmt.Sprintf(\"%.2f\", d.Seconds())\n\t}\n\tmin := int(d.Minutes())\n\tsec := math.Mod(d.Seconds(), 60.0)\n\treturn fmt.Sprintf(\"%dm%.3fs\", min, sec)\n}\n\nfunc (r *Runner) stmts(ctx context.Context, stmts []*syntax.Stmt) {\n\tfor _, stmt := range stmts {\n\t\tr.stmt(ctx, stmt)\n\t}\n}\n\nfunc (r *Runner) hdocReader(rd *syntax.Redirect) (*os.File, error) {\n\tpr, pw, err := os.Pipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// We write to the pipe in a new goroutine,\n\t// as pipe writes may block once the buffer gets full.\n\t// We still construct and buffer the entire heredoc first,\n\t// as doing it concurrently would lead to different semantics and be racy.\n\tif rd.Op != syntax.DashHdoc {\n\t\thdoc := r.document(rd.Hdoc)\n\t\tgo func() {\n\t\t\tpw.WriteString(hdoc)\n\t\t\tpw.Close()\n\t\t}()\n\t\treturn pr, nil\n\t}\n\tvar buf bytes.Buffer\n\tvar cur []syntax.WordPart\n\tflushLine := func() {\n\t\tif buf.Len() > 0 {\n\t\t\tbuf.WriteByte('\\n')\n\t\t}\n\t\tbuf.WriteString(r.document(&syntax.Word{Parts: cur}))\n\t\tcur = cur[:0]\n\t}\n\tfor _, wp := range rd.Hdoc.Parts {\n\t\tlit, ok := wp.(*syntax.Lit)\n\t\tif !ok {\n\t\t\tcur = append(cur, wp)\n\t\t\tcontinue\n\t\t}\n\t\tfirst := true\n\t\tfor part := range strings.SplitSeq(lit.Value, \"\\n\") {\n\t\t\tif !first {\n\t\t\t\tflushLine()\n\t\t\t\tcur = cur[:0]\n\t\t\t}\n\t\t\tfirst = false\n\t\t\tpart = strings.TrimLeft(part, \"\\t\")\n\t\t\tcur = append(cur, &syntax.Lit{Value: part})\n\t\t}\n\t}\n\tflushLine()\n\tgo func() {\n\t\tpw.Write(buf.Bytes())\n\t\tpw.Close()\n\t}()\n\treturn pr, nil\n}\n\nfunc (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, error) {\n\tif rd.Hdoc != nil {\n\t\tpr, err := r.hdocReader(rd)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.stdin = pr\n\t\treturn pr, nil\n\t}\n\n\torig := &r.stdout\n\tif rd.N != nil {\n\t\tswitch rd.N.Value {\n\t\tcase \"0\":\n\t\t\t// Note that the input redirects below always use stdin (0)\n\t\t\t// because we don't support anything else right now.\n\t\tcase \"1\":\n\t\t\t// The default for the output redirects below.\n\t\tcase \"2\":\n\t\t\torig = &r.stderr\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unsupported redirect fd: %v\", rd.N.Value))\n\t\t}\n\t}\n\targ := r.literal(rd.Word)\n\tswitch rd.Op {\n\tcase syntax.WordHdoc:\n\t\tpr, pw, err := os.Pipe()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.stdin = pr\n\t\t// We write to the pipe in a new goroutine,\n\t\t// as pipe writes may block once the buffer gets full.\n\t\tgo func() {\n\t\t\tpw.WriteString(arg)\n\t\t\tpw.WriteString(\"\\n\")\n\t\t\tpw.Close()\n\t\t}()\n\t\treturn pr, nil\n\tcase syntax.DplOut:\n\t\tswitch arg {\n\t\tcase \"1\":\n\t\t\t*orig = r.stdout\n\t\tcase \"2\":\n\t\t\t*orig = r.stderr\n\t\tcase \"-\":\n\t\t\t*orig = io.Discard // closing the output writer\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unhandled %v arg: %q\", rd.Op, arg))\n\t\t}\n\t\treturn nil, nil\n\tcase syntax.RdrIn, syntax.RdrOut, syntax.AppOut,\n\t\tsyntax.RdrAll, syntax.AppAll:\n\t\t// done further below\n\tcase syntax.DplIn:\n\t\tswitch arg {\n\t\tcase \"-\":\n\t\t\tr.stdin = nil // closing the input file\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unhandled %v arg: %q\", rd.Op, arg))\n\t\t}\n\t\treturn nil, nil\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled redirect op: %v\", rd.Op))\n\t}\n\tmode := os.O_RDONLY\n\tswitch rd.Op {\n\tcase syntax.AppOut, syntax.AppAll:\n\t\tmode = os.O_WRONLY | os.O_CREATE | os.O_APPEND\n\tcase syntax.RdrOut, syntax.RdrAll:\n\t\tmode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC\n\t}\n\tf, err := r.open(ctx, arg, mode, 0o644, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch rd.Op {\n\tcase syntax.RdrIn:\n\t\tstdin, err := stdinFile(f)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr.stdin = stdin\n\tcase syntax.RdrOut, syntax.AppOut:\n\t\t*orig = f\n\tcase syntax.RdrAll, syntax.AppAll:\n\t\tr.stdout = f\n\t\tr.stderr = f\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled redirect op: %v\", rd.Op))\n\t}\n\treturn f, nil\n}\n\nfunc (r *Runner) loopStmtsBroken(ctx context.Context, stmts []*syntax.Stmt) bool {\n\toldInLoop := r.inLoop\n\tr.inLoop = true\n\tdefer func() { r.inLoop = oldInLoop }()\n\tfor _, stmt := range stmts {\n\t\tr.stmt(ctx, stmt)\n\t\tif r.contnEnclosing > 0 {\n\t\t\tr.contnEnclosing--\n\t\t\treturn r.contnEnclosing > 0\n\t\t}\n\t\tif r.breakEnclosing > 0 {\n\t\t\tr.breakEnclosing--\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) {\n\tif r.stop(ctx) {\n\t\treturn\n\t}\n\tif r.callHandler != nil {\n\t\tvar err error\n\t\targs, err = r.callHandler(r.handlerCtx(ctx, handlerKindCall, pos), args)\n\t\tif err != nil {\n\t\t\t// handler's custom fatal error\n\t\t\tr.exit.fatal(err)\n\t\t\treturn\n\t\t}\n\t}\n\tname := args[0]\n\tif body := r.Funcs[name]; body != nil {\n\t\t// stack them to support nested func calls\n\t\toldParams := r.Params\n\t\tr.Params = args[1:]\n\t\toldInFunc := r.inFunc\n\t\tr.inFunc = true\n\n\t\t// Functions run in a nested scope.\n\t\t// Note that [Runner.exec] below does something similar.\n\t\torigEnv := r.writeEnv\n\t\tr.writeEnv = &overlayEnviron{parent: r.writeEnv, funcScope: true}\n\n\t\tr.stmt(ctx, body)\n\n\t\tr.writeEnv = origEnv\n\n\t\tr.Params = oldParams\n\t\tr.inFunc = oldInFunc\n\t\tr.exit.returning = false\n\t\treturn\n\t}\n\tif IsBuiltin(name) {\n\t\tr.exit = r.builtin(ctx, pos, name, args[1:])\n\t\treturn\n\t}\n\tr.exec(ctx, pos, args)\n}\n\nfunc (r *Runner) exec(ctx context.Context, pos syntax.Pos, args []string) {\n\tr.exit.fromHandlerError(r.execHandler(r.handlerCtx(ctx, handlerKindExec, pos), args))\n}\n\nfunc (r *Runner) open(ctx context.Context, path string, flags int, mode os.FileMode, print bool) (io.ReadWriteCloser, error) {\n\t// If we are opening a FIFO temporary file created by the interpreter itself,\n\t// don't pass this along to the open handler as it will not work at all\n\t// unless [os.OpenFile] is used directly with it.\n\t// Matching by directory and basename prefix isn't perfect, but works.\n\t//\n\t// If we want FIFOs to use a handler in the future, they probably\n\t// need their own separate handler API matching Unix-like semantics.\n\tdir, name := filepath.Split(path)\n\tdir = strings.TrimSuffix(dir, \"/\")\n\tif dir == r.tempDir && strings.HasPrefix(name, fifoNamePrefix) {\n\t\treturn os.OpenFile(path, flags, mode)\n\t}\n\n\tf, err := r.openHandler(r.handlerCtx(ctx, handlerKindOpen, todoPos), path, flags, mode)\n\t// TODO: support wrapped PathError returned from openHandler.\n\tswitch err.(type) {\n\tcase nil:\n\t\treturn f, nil\n\tcase *os.PathError:\n\t\tif print {\n\t\t\tr.errf(\"%v\\n\", err)\n\t\t}\n\tdefault: // handler's custom fatal error\n\t\tr.exit.fatal(err)\n\t}\n\treturn nil, err\n}\n\nfunc (r *Runner) stat(ctx context.Context, name string) (fs.FileInfo, error) {\n\tpath := absPath(r.Dir, name)\n\treturn r.statHandler(ctx, path, true)\n}\n\nfunc (r *Runner) lstat(ctx context.Context, name string) (fs.FileInfo, error) {\n\tpath := absPath(r.Dir, name)\n\treturn r.statHandler(ctx, path, false)\n}\n"
  },
  {
    "path": "interp/test.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\n\t\"golang.org/x/term\"\n\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// non-empty string is true, empty string is false\nfunc (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic bool) string {\n\tswitch x := expr.(type) {\n\tcase *syntax.Word:\n\t\tif classic {\n\t\t\t// In the classic \"test\" mode, we already expanded and\n\t\t\t// split the list of words, so don't redo that work.\n\t\t\treturn r.document(x)\n\t\t}\n\t\treturn r.literal(x)\n\tcase *syntax.ParenTest:\n\t\treturn r.bashTest(ctx, x.X, classic)\n\tcase *syntax.BinaryTest:\n\t\tswitch x.Op {\n\t\tcase syntax.TsMatchShort, syntax.TsMatch, syntax.TsNoMatch:\n\t\t\tstr := r.literal(x.X.(*syntax.Word))\n\t\t\tyw := x.Y.(*syntax.Word)\n\t\t\tif classic { // test, [\n\t\t\t\tlit := r.literal(yw)\n\t\t\t\tif (str == lit) == (x.Op != syntax.TsNoMatch) {\n\t\t\t\t\treturn \"1\"\n\t\t\t\t}\n\t\t\t} else { // [[\n\t\t\t\tpattern := r.pattern(yw)\n\t\t\t\tif match(pattern, str) == (x.Op != syntax.TsNoMatch) {\n\t\t\t\t\treturn \"1\"\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn \"\"\n\t\t}\n\t\tif r.binTest(ctx, x.Op, r.bashTest(ctx, x.X, classic), r.bashTest(ctx, x.Y, classic)) {\n\t\t\treturn \"1\"\n\t\t}\n\t\treturn \"\"\n\tcase *syntax.UnaryTest:\n\t\tif r.unTest(ctx, x.Op, r.bashTest(ctx, x.X, classic)) {\n\t\t\treturn \"1\"\n\t\t}\n\t\treturn \"\"\n\t}\n\treturn \"\"\n}\n\nfunc (r *Runner) binTest(ctx context.Context, op syntax.BinTestOperator, x, y string) bool {\n\tswitch op {\n\tcase syntax.TsReMatch:\n\t\tre, err := regexp.Compile(y)\n\t\tif err != nil {\n\t\t\tr.exit.code = 2\n\t\t\treturn false\n\t\t}\n\t\tm := re.FindStringSubmatch(x)\n\t\tif m == nil {\n\t\t\treturn false\n\t\t}\n\t\tvr := expand.Variable{\n\t\t\tSet:  true,\n\t\t\tKind: expand.Indexed,\n\t\t\tList: m,\n\t\t}\n\t\tr.setVar(\"BASH_REMATCH\", vr)\n\t\treturn true\n\tcase syntax.TsNewer:\n\t\tinfo1, err1 := r.stat(ctx, x)\n\t\tinfo2, err2 := r.stat(ctx, y)\n\t\tif err1 != nil || err2 != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn info1.ModTime().After(info2.ModTime())\n\tcase syntax.TsOlder:\n\t\tinfo1, err1 := r.stat(ctx, x)\n\t\tinfo2, err2 := r.stat(ctx, y)\n\t\tif err1 != nil || err2 != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn info1.ModTime().Before(info2.ModTime())\n\tcase syntax.TsDevIno:\n\t\tinfo1, err1 := r.stat(ctx, x)\n\t\tinfo2, err2 := r.stat(ctx, y)\n\t\tif err1 != nil || err2 != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn os.SameFile(info1, info2)\n\tcase syntax.TsEql:\n\t\treturn atoi(x) == atoi(y)\n\tcase syntax.TsNeq:\n\t\treturn atoi(x) != atoi(y)\n\tcase syntax.TsLeq:\n\t\treturn atoi(x) <= atoi(y)\n\tcase syntax.TsGeq:\n\t\treturn atoi(x) >= atoi(y)\n\tcase syntax.TsLss:\n\t\treturn atoi(x) < atoi(y)\n\tcase syntax.TsGtr:\n\t\treturn atoi(x) > atoi(y)\n\tcase syntax.AndTest:\n\t\treturn x != \"\" && y != \"\"\n\tcase syntax.OrTest:\n\t\treturn x != \"\" || y != \"\"\n\tcase syntax.TsBefore:\n\t\treturn x < y\n\tcase syntax.TsAfter:\n\t\treturn x > y\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unsupported binary test operator: %q\", op))\n\t}\n}\n\nfunc (r *Runner) statMode(ctx context.Context, name string, mode os.FileMode) bool {\n\tinfo, err := r.stat(ctx, name)\n\treturn err == nil && info.Mode()&mode != 0\n}\n\n// These are copied from x/sys/unix as we can't import it here.\nconst (\n\taccess_R_OK = 0x4\n\taccess_W_OK = 0x2\n\taccess_X_OK = 0x1\n)\n\nfunc (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) bool {\n\tswitch op {\n\tcase syntax.TsExists:\n\t\t_, err := r.stat(ctx, x)\n\t\treturn err == nil\n\tcase syntax.TsRegFile:\n\t\tinfo, err := r.stat(ctx, x)\n\t\treturn err == nil && info.Mode().IsRegular()\n\tcase syntax.TsDirect:\n\t\treturn r.statMode(ctx, x, os.ModeDir)\n\tcase syntax.TsCharSp:\n\t\treturn r.statMode(ctx, x, os.ModeCharDevice)\n\tcase syntax.TsBlckSp:\n\t\tinfo, err := r.stat(ctx, x)\n\t\treturn err == nil && info.Mode()&os.ModeDevice != 0 &&\n\t\t\tinfo.Mode()&os.ModeCharDevice == 0\n\tcase syntax.TsNmPipe:\n\t\treturn r.statMode(ctx, x, os.ModeNamedPipe)\n\tcase syntax.TsSocket:\n\t\treturn r.statMode(ctx, x, os.ModeSocket)\n\tcase syntax.TsSmbLink:\n\t\tinfo, err := r.lstat(ctx, x)\n\t\treturn err == nil && info.Mode()&os.ModeSymlink != 0\n\tcase syntax.TsSticky:\n\t\treturn r.statMode(ctx, x, os.ModeSticky)\n\tcase syntax.TsUIDSet:\n\t\treturn r.statMode(ctx, x, os.ModeSetuid)\n\tcase syntax.TsGIDSet:\n\t\treturn r.statMode(ctx, x, os.ModeSetgid)\n\t// case syntax.TsGrpOwn:\n\t// case syntax.TsUsrOwn:\n\t// case syntax.TsModif:\n\tcase syntax.TsRead:\n\t\treturn r.access(ctx, r.absPath(x), access_R_OK) == nil\n\tcase syntax.TsWrite:\n\t\treturn r.access(ctx, r.absPath(x), access_W_OK) == nil\n\tcase syntax.TsExec:\n\t\treturn r.access(ctx, r.absPath(x), access_X_OK) == nil\n\tcase syntax.TsNoEmpty:\n\t\tinfo, err := r.stat(ctx, x)\n\t\treturn err == nil && info.Size() > 0\n\tcase syntax.TsFdTerm:\n\t\tfd := atoi(x)\n\t\tvar f any\n\t\tswitch fd {\n\t\tcase 0:\n\t\t\tf = r.stdin\n\t\tcase 1:\n\t\t\tf = r.stdout\n\t\tcase 2:\n\t\t\tf = r.stderr\n\t\t}\n\t\tif f, ok := f.(interface{ Fd() uintptr }); ok {\n\t\t\t// Support [os.File.Fd] methods such as the one on [*os.File].\n\t\t\treturn term.IsTerminal(int(f.Fd()))\n\t\t}\n\t\t// TODO: allow term.IsTerminal here too if running in the\n\t\t// \"single process\" mode.\n\t\treturn false\n\tcase syntax.TsEmpStr:\n\t\treturn x == \"\"\n\tcase syntax.TsNempStr:\n\t\treturn x != \"\"\n\tcase syntax.TsOptSet:\n\t\tif opt := r.posixOptByName(x); opt != nil {\n\t\t\treturn *opt\n\t\t}\n\t\treturn false\n\tcase syntax.TsVarSet:\n\t\treturn r.lookupVar(x).IsSet()\n\tcase syntax.TsRefVar:\n\t\treturn r.lookupVar(x).Kind == expand.NameRef\n\tcase syntax.TsNot:\n\t\treturn x == \"\"\n\tcase syntax.TsUsrOwn, syntax.TsGrpOwn:\n\t\treturn r.unTestOwnOrGrp(ctx, op, x)\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled unary test op: %v\", op))\n\t}\n}\n"
  },
  {
    "path": "interp/test_classic.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n\t\"fmt\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nconst illegalTok = 0\n\ntype testParser struct {\n\teof bool\n\tval string\n\trem []string\n\n\terr func(err error)\n}\n\nfunc (p *testParser) errf(format string, a ...any) {\n\tp.err(fmt.Errorf(format, a...))\n}\n\nfunc (p *testParser) next() {\n\tif p.eof || len(p.rem) == 0 {\n\t\tp.eof = true\n\t\tp.val = \"\"\n\t\treturn\n\t}\n\tp.val = p.rem[0]\n\tp.rem = p.rem[1:]\n}\n\nfunc (p *testParser) followWord(fval string) *syntax.Word {\n\tif p.eof {\n\t\tp.errf(\"%s must be followed by a word\", fval)\n\t}\n\tw := &syntax.Word{Parts: []syntax.WordPart{\n\t\t&syntax.Lit{Value: p.val},\n\t}}\n\tp.next()\n\treturn w\n}\n\nfunc (p *testParser) classicTest(fval string, pastAndOr bool) syntax.TestExpr {\n\tvar left syntax.TestExpr\n\tif pastAndOr {\n\t\tleft = p.testExprBase(fval)\n\t} else {\n\t\tleft = p.classicTest(fval, true)\n\t}\n\tif left == nil || p.eof || p.val == \")\" {\n\t\treturn left\n\t}\n\topStr := p.val\n\top := testBinaryOp(p.val)\n\tif op == illegalTok {\n\t\tp.errf(\"not a valid test operator: %#q\", p.val)\n\t}\n\tb := &syntax.BinaryTest{\n\t\tOp: op,\n\t\tX:  left,\n\t}\n\tp.next()\n\tswitch b.Op {\n\tcase syntax.AndTest, syntax.OrTest:\n\t\tif b.Y = p.classicTest(opStr, false); b.Y == nil {\n\t\t\tp.errf(\"%s must be followed by an expression\", opStr)\n\t\t}\n\tdefault:\n\t\tb.Y = p.followWord(opStr)\n\t}\n\treturn b\n}\n\nfunc (p *testParser) testExprBase(fval string) syntax.TestExpr {\n\tif p.eof || p.val == \")\" {\n\t\treturn nil\n\t}\n\top := testUnaryOp(p.val)\n\tswitch op {\n\tcase syntax.TsNot:\n\t\tu := &syntax.UnaryTest{Op: op}\n\t\tp.next()\n\t\tu.X = p.classicTest(op.String(), false)\n\t\treturn u\n\tcase syntax.TsParen:\n\t\tpe := &syntax.ParenTest{}\n\t\tp.next()\n\t\tpe.X = p.classicTest(op.String(), false)\n\t\tif p.val != \")\" {\n\t\t\tp.errf(\"reached %s without matching '(' with ')'\", p.val)\n\t\t}\n\t\tp.next()\n\t\treturn pe\n\tcase illegalTok:\n\t\treturn p.followWord(fval)\n\tdefault:\n\t\tu := &syntax.UnaryTest{Op: op}\n\t\tp.next()\n\t\tif p.eof {\n\t\t\t// make [ -e ] fall back to [ -n -e ], i.e. use\n\t\t\t// the operator as an argument\n\t\t\treturn &syntax.Word{Parts: []syntax.WordPart{\n\t\t\t\t&syntax.Lit{Value: op.String()},\n\t\t\t}}\n\t\t}\n\t\tu.X = p.followWord(op.String())\n\t\treturn u\n\t}\n}\n\n// testUnaryOp is an exact copy of syntax's.\nfunc testUnaryOp(val string) syntax.UnTestOperator {\n\tswitch val {\n\tcase \"!\":\n\t\treturn syntax.TsNot\n\tcase \"(\":\n\t\treturn syntax.TsParen\n\tcase \"-e\", \"-a\":\n\t\treturn syntax.TsExists\n\tcase \"-f\":\n\t\treturn syntax.TsRegFile\n\tcase \"-d\":\n\t\treturn syntax.TsDirect\n\tcase \"-c\":\n\t\treturn syntax.TsCharSp\n\tcase \"-b\":\n\t\treturn syntax.TsBlckSp\n\tcase \"-p\":\n\t\treturn syntax.TsNmPipe\n\tcase \"-S\":\n\t\treturn syntax.TsSocket\n\tcase \"-L\", \"-h\":\n\t\treturn syntax.TsSmbLink\n\tcase \"-k\":\n\t\treturn syntax.TsSticky\n\tcase \"-g\":\n\t\treturn syntax.TsGIDSet\n\tcase \"-u\":\n\t\treturn syntax.TsUIDSet\n\tcase \"-G\":\n\t\treturn syntax.TsGrpOwn\n\tcase \"-O\":\n\t\treturn syntax.TsUsrOwn\n\tcase \"-N\":\n\t\treturn syntax.TsModif\n\tcase \"-r\":\n\t\treturn syntax.TsRead\n\tcase \"-w\":\n\t\treturn syntax.TsWrite\n\tcase \"-x\":\n\t\treturn syntax.TsExec\n\tcase \"-s\":\n\t\treturn syntax.TsNoEmpty\n\tcase \"-t\":\n\t\treturn syntax.TsFdTerm\n\tcase \"-z\":\n\t\treturn syntax.TsEmpStr\n\tcase \"-n\":\n\t\treturn syntax.TsNempStr\n\tcase \"-o\":\n\t\treturn syntax.TsOptSet\n\tcase \"-v\":\n\t\treturn syntax.TsVarSet\n\tcase \"-R\":\n\t\treturn syntax.TsRefVar\n\tdefault:\n\t\treturn illegalTok\n\t}\n}\n\n// testBinaryOp is like syntax's, but with -a and -o, and without =~.\nfunc testBinaryOp(val string) syntax.BinTestOperator {\n\tswitch val {\n\tcase \"-a\":\n\t\treturn syntax.AndTest\n\tcase \"-o\":\n\t\treturn syntax.OrTest\n\tcase \"==\", \"=\":\n\t\treturn syntax.TsMatch\n\tcase \"!=\":\n\t\treturn syntax.TsNoMatch\n\tcase \"-nt\":\n\t\treturn syntax.TsNewer\n\tcase \"-ot\":\n\t\treturn syntax.TsOlder\n\tcase \"-ef\":\n\t\treturn syntax.TsDevIno\n\tcase \"-eq\":\n\t\treturn syntax.TsEql\n\tcase \"-ne\":\n\t\treturn syntax.TsNeq\n\tcase \"-le\":\n\t\treturn syntax.TsLeq\n\tcase \"-ge\":\n\t\treturn syntax.TsGeq\n\tcase \"-lt\":\n\t\treturn syntax.TsLss\n\tcase \"-gt\":\n\t\treturn syntax.TsGtr\n\tdefault:\n\t\treturn illegalTok\n\t}\n}\n"
  },
  {
    "path": "interp/trace.go",
    "content": "package interp\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// tracer prints expressions like a shell would do if its\n// options '-o' is set to either 'xtrace' or its shorthand, '-x'.\ntype tracer struct {\n\tbuf       bytes.Buffer\n\tprinter   *syntax.Printer\n\toutput    io.Writer\n\tneedsPlus bool\n}\n\nfunc (r *Runner) tracer() *tracer {\n\tif !r.opts[optXTrace] {\n\t\treturn nil\n\t}\n\n\treturn &tracer{\n\t\tprinter:   syntax.NewPrinter(),\n\t\toutput:    r.stderr,\n\t\tneedsPlus: true,\n\t}\n}\n\n// string writes s to tracer.buf if tracer is non-nil,\n// prepending \"+\" if tracer.needsPlus is true.\nfunc (t *tracer) string(s string) {\n\tif t == nil {\n\t\treturn\n\t}\n\n\tif t.needsPlus {\n\t\tt.buf.WriteString(\"+ \")\n\t}\n\tt.needsPlus = false\n\tt.buf.WriteString(s)\n}\n\nfunc (t *tracer) stringf(f string, a ...any) {\n\tif t == nil {\n\t\treturn\n\t}\n\n\tt.string(fmt.Sprintf(f, a...))\n}\n\n// expr prints x to tracer.buf if tracer is non-nil,\n// prepending \"+\" if tracer.isFirstPrint is true.\nfunc (t *tracer) expr(x syntax.Node) {\n\tif t == nil {\n\t\treturn\n\t}\n\n\tif t.needsPlus {\n\t\tt.buf.WriteString(\"+ \")\n\t}\n\tt.needsPlus = false\n\tif err := t.printer.Print(&t.buf, x); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// flush writes the contents of tracer.buf to the tracer.stdout.\nfunc (t *tracer) flush() {\n\tif t == nil {\n\t\treturn\n\t}\n\n\tt.output.Write(t.buf.Bytes())\n\tt.buf.Reset()\n}\n\n// newLineFlush is like flush, but with extra new line before tracer.buf gets flushed.\nfunc (t *tracer) newLineFlush() {\n\tif t == nil {\n\t\treturn\n\t}\n\n\tt.buf.WriteString(\"\\n\")\n\tt.flush()\n\t// reset state\n\tt.needsPlus = true\n}\n\n// call prints a command and its arguments with varying formats depending on the cmd type,\n// for example, built-in command's arguments are printed enclosed in single quotes,\n// otherwise, call defaults to printing with double quotes.\nfunc (t *tracer) call(cmd string, args ...string) {\n\tif t == nil {\n\t\treturn\n\t}\n\n\ts := strings.Join(args, \" \")\n\tif strings.TrimSpace(s) == \"\" {\n\t\t// fields may be empty for function () {} declarations\n\t\tt.string(cmd)\n\t} else if IsBuiltin(cmd) {\n\t\tif cmd == \"set\" {\n\t\t\t// TODO: only first occurrence of set is not printed, succeeding calls are printed\n\t\t\treturn\n\t\t}\n\n\t\tqs, err := syntax.Quote(s, syntax.LangBash)\n\t\tif err != nil { // should never happen\n\t\t\tpanic(err)\n\t\t}\n\t\tt.stringf(\"%s %s\", cmd, qs)\n\t} else {\n\t\tt.stringf(\"%s %s\", cmd, s)\n\t}\n}\n"
  },
  {
    "path": "interp/unexported_test.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestElapsedString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tin    time.Duration\n\t\tposix bool\n\t\twant  string\n\t}{\n\t\t{time.Nanosecond, false, \"0m0.000s\"},\n\t\t{time.Millisecond, false, \"0m0.001s\"},\n\t\t{time.Millisecond, true, \"0.00\"},\n\t\t{2500 * time.Millisecond, false, \"0m2.500s\"},\n\t\t{2500 * time.Millisecond, true, \"2.50\"},\n\t\t{\n\t\t\t10*time.Minute + 10*time.Second,\n\t\t\tfalse,\n\t\t\t\"10m10.000s\",\n\t\t},\n\t\t{\n\t\t\t10*time.Minute + 10*time.Second,\n\t\t\ttrue,\n\t\t\t\"610.00\",\n\t\t},\n\t\t{31 * time.Second, false, \"0m31.000s\"},\n\t\t{102 * time.Second, false, \"1m42.000s\"},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.in.String(), func(t *testing.T) {\n\t\t\tgot := elapsedString(tc.in, tc.posix)\n\t\t\tif got != tc.want {\n\t\t\t\tt.Fatalf(\"wanted %q, got %q\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "interp/unix_test.go",
    "content": "// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n//go:build unix\n\npackage interp_test\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/creack/pty\"\n\t\"mvdan.cc/sh/v3/interp\"\n)\n\nfunc TestRunnerTerminalStdIO(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname  string\n\t\tfiles func(*testing.T) (secondary io.Writer, primary io.Reader)\n\t\twant  string\n\t}{\n\t\t{\"Nil\", func(t *testing.T) (io.Writer, io.Reader) {\n\t\t\treturn nil, strings.NewReader(\"\\n\")\n\t\t}, \"\\n\"},\n\t\t{\"Pipe\", func(t *testing.T) (io.Writer, io.Reader) {\n\t\t\tpr, pw := io.Pipe()\n\t\t\treturn pw, pr\n\t\t}, \"end\\n\"},\n\t\t{\"Pseudo\", func(t *testing.T) (io.Writer, io.Reader) {\n\t\t\tprimary, secondary, err := pty.Open()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treturn secondary, primary\n\t\t}, \"012end\\r\\n\"},\n\t}\n\tfile := parse(t, nil, `\n\t\tfor n in 0 1 2 3; do if [[ -t $n ]]; then echo -n $n; fi; done; echo end\n\t`)\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tsecondary, primary := test.files(t)\n\t\t\t// some secondary ends can be used as stdin too\n\t\t\tsecondaryReader, _ := secondary.(io.Reader)\n\n\t\t\tr, _ := interp.New(interp.StdIO(secondaryReader, secondary, secondary))\n\t\t\tgo func() {\n\t\t\t\t// To mimic [os/exec.Cmd.Start], use a goroutine.\n\t\t\t\tif err := r.Run(context.Background(), file); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tgot, err := bufio.NewReader(primary).ReadString('\\n')\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif got != test.want {\n\t\t\t\tt.Fatalf(\"\\nwant: %q\\ngot:  %q\", test.want, got)\n\t\t\t}\n\t\t\tif closer, ok := secondary.(io.Closer); ok {\n\t\t\t\tif err := closer.Close(); err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif closer, ok := primary.(io.Closer); ok {\n\t\t\t\tif err := closer.Close(); err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRunnerTerminalExec(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname  string\n\t\tstart func(*testing.T, *exec.Cmd) io.Reader\n\t\twant  string\n\t}{\n\t\t{\"Nil\", func(t *testing.T, cmd *exec.Cmd) io.Reader {\n\t\t\tif err := cmd.Start(); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treturn strings.NewReader(\"\\n\")\n\t\t}, \"\\n\"},\n\t\t{\"Pipe\", func(t *testing.T, cmd *exec.Cmd) io.Reader {\n\t\t\tout, err := cmd.StdoutPipe()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tcmd.Stderr = cmd.Stdout\n\t\t\tif err := cmd.Start(); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treturn out\n\t\t}, \"end\\n\"},\n\t\t{\"Pseudo\", func(t *testing.T, cmd *exec.Cmd) io.Reader {\n\t\t\t// Note that we avoid pty.Start,\n\t\t\t// as it closes the secondary terminal via a defer,\n\t\t\t// possibly before the command has finished.\n\t\t\t// That can lead to \"signal: hangup\" flakes.\n\t\t\tprimary, secondary, err := pty.Open()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tcmd.Stdin = secondary\n\t\t\tcmd.Stdout = secondary\n\t\t\tcmd.Stderr = secondary\n\t\t\tif err := cmd.Start(); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treturn primary\n\t\t}, \"012end\\r\\n\"},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tcmd := exec.Command(os.Getenv(\"GOSH_PROG\"),\n\t\t\t\t\"for n in 0 1 2 3; do if [[ -t $n ]]; then echo -n $n; fi; done; echo end\")\n\t\t\tprimary := test.start(t, cmd)\n\n\t\t\tgot, err := bufio.NewReader(primary).ReadString('\\n')\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif got != test.want {\n\t\t\t\tt.Fatalf(\"\\nwant: %q\\ngot:  %q\", test.want, got)\n\t\t\t}\n\t\t\tif closer, ok := primary.(io.Closer); ok {\n\t\t\t\tif err := closer.Close(); err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := cmd.Wait(); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc shortPathName(path string) (string, error) {\n\tpanic(\"only works on windows\")\n}\n"
  },
  {
    "path": "interp/vars.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n\tcryptorand \"crypto/rand\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"maps\"\n\tmathrand \"math/rand/v2\"\n\t\"os\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc newOverlayEnviron(parent expand.Environ, background bool) *overlayEnviron {\n\toenv := &overlayEnviron{}\n\tif !background {\n\t\toenv.parent = parent\n\t} else {\n\t\t// We could do better here if the parent is also an overlayEnviron;\n\t\t// measure with profiles or benchmarks before we choose to do so.\n\t\tfor name, vr := range parent.Each {\n\t\t\toenv.Set(name, vr)\n\t\t}\n\t}\n\treturn oenv\n}\n\n// overlayEnviron is our main implementation of [expand.WriteEnviron].\ntype overlayEnviron struct {\n\t// parent is non-nil if [values] is an overlay over a parent environment\n\t// which we can safely reuse without data races, such as non-background subshells\n\t// or function calls.\n\tparent expand.Environ\n\n\t// values maps normalized variable names, per [overlayEnviron.normalize].\n\tvalues map[string]namedVariable\n\n\t// We need to know if the current scope is a function's scope, because\n\t// functions can modify global variables. When true, [parent] must not be nil.\n\tfuncScope bool\n}\n\n// namedVariable records the original name of a variable for platforms\n// where variable names are matched in a case-insensitive way.\ntype namedVariable struct {\n\t// TODO(v4): consider adding this field to [expand.Variable],\n\t// as a general way for a variable to report its original name.\n\t// This can be useful for GOOS=windows with case insensitive env vars,\n\t// as otherwise it's not possible to Environ.Get a var\n\t// and know what was its original name without looping over Environ.Each.\n\tName string\n\texpand.Variable\n}\n\nfunc (o *overlayEnviron) normalize(name string) string {\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToUpper(name)\n\t}\n\treturn name\n}\n\nfunc (o *overlayEnviron) Get(name string) expand.Variable {\n\tnormalized := o.normalize(name)\n\tif vr, ok := o.values[normalized]; ok {\n\t\treturn vr.Variable\n\t}\n\tif o.parent != nil {\n\t\treturn o.parent.Get(name)\n\t}\n\treturn expand.Variable{}\n}\n\nfunc (o *overlayEnviron) Set(name string, vr expand.Variable) error {\n\tnormalized := o.normalize(name)\n\tprev, inOverlay := o.values[normalized]\n\t// Manipulation of a global var inside a function.\n\tif o.funcScope && !vr.Local && !prev.Local {\n\t\t// In a function, the parent environment is ours, so it's always read-write.\n\t\treturn o.parent.(expand.WriteEnviron).Set(name, vr)\n\t}\n\tif !inOverlay && o.parent != nil {\n\t\tprev.Variable = o.parent.Get(name)\n\t}\n\n\tif o.values == nil {\n\t\to.values = make(map[string]namedVariable)\n\t}\n\tif vr.Kind == expand.KeepValue {\n\t\tvr.Kind = prev.Kind\n\t\tvr.Str = prev.Str\n\t\tvr.List = prev.List\n\t\tvr.Map = prev.Map\n\t} else if prev.ReadOnly {\n\t\treturn fmt.Errorf(\"readonly variable\")\n\t}\n\tif !vr.IsSet() { // unsetting\n\t\tif prev.Local {\n\t\t\tvr.Local = true\n\t\t\to.values[normalized] = namedVariable{name, vr}\n\t\t\treturn nil\n\t\t}\n\t\tdelete(o.values, normalized)\n\t}\n\t// modifying the entire variable\n\tvr.Local = prev.Local || vr.Local\n\to.values[normalized] = namedVariable{name, vr}\n\treturn nil\n}\n\nfunc (o *overlayEnviron) Each(f func(name string, vr expand.Variable) bool) {\n\tif o.parent != nil {\n\t\to.parent.Each(f)\n\t}\n\tfor _, vr := range o.values {\n\t\tif !f(vr.Name, vr.Variable) {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc execEnv(env expand.Environ) []string {\n\tlist := make([]string, 0, 64)\n\tfor name, vr := range env.Each {\n\t\tif !vr.IsSet() {\n\t\t\t// If a variable is set globally but unset in the\n\t\t\t// runner, we need to ensure it's not part of the final\n\t\t\t// list. Seems like zeroing the element is enough.\n\t\t\t// This is a linear search, but this scenario should be\n\t\t\t// rare, and the number of variables shouldn't be large.\n\t\t\tfor i, kv := range list {\n\t\t\t\tif strings.HasPrefix(kv, name+\"=\") {\n\t\t\t\t\tlist[i] = \"\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif vr.Exported && vr.Kind == expand.String {\n\t\t\tlist = append(list, name+\"=\"+vr.String())\n\t\t}\n\t}\n\treturn list\n}\n\nfunc (r *Runner) lookupVar(name string) expand.Variable {\n\tif name == \"\" {\n\t\tpanic(\"variable name must not be empty\")\n\t}\n\tvar vr expand.Variable\n\tswitch name {\n\tcase \"#\":\n\t\tvr.Kind, vr.Str = expand.String, strconv.Itoa(len(r.Params))\n\tcase \"@\", \"*\":\n\t\tvr.Kind = expand.Indexed\n\t\tif r.Params == nil {\n\t\t\t// r.Params may be nil but positional parameters always exist\n\t\t\tvr.List = []string{}\n\t\t} else {\n\t\t\tvr.List = r.Params\n\t\t}\n\tcase \"!\":\n\t\tif n := len(r.bgProcs); n > 0 {\n\t\t\tvr.Kind, vr.Str = expand.String, \"g\"+strconv.Itoa(n)\n\t\t}\n\tcase \"?\":\n\t\tvr.Kind, vr.Str = expand.String, strconv.Itoa(int(r.lastExit.code))\n\tcase \"$\":\n\t\tvr.Kind, vr.Str = expand.String, strconv.Itoa(os.Getpid())\n\tcase \"PPID\":\n\t\tvr.Kind, vr.Str = expand.String, strconv.Itoa(os.Getppid())\n\tcase \"RANDOM\": // not for cryptographic use\n\t\tvr.Kind, vr.Str = expand.String, strconv.Itoa(mathrand.IntN(32767))\n\t\t// TODO: support setting RANDOM to seed it\n\tcase \"SRANDOM\": // pseudo-random generator from the system\n\t\tvar p [4]byte\n\t\tcryptorand.Read(p[:])\n\t\tn := binary.NativeEndian.Uint32(p[:])\n\t\tvr.Kind, vr.Str = expand.String, strconv.FormatUint(uint64(n), 10)\n\tcase \"DIRSTACK\":\n\t\tvr.Kind, vr.List = expand.Indexed, r.dirStack\n\tcase \"0\":\n\t\tvr.Kind = expand.String\n\t\tif r.filename != \"\" {\n\t\t\tvr.Str = r.filename\n\t\t} else {\n\t\t\tvr.Str = \"gosh\"\n\t\t}\n\tcase \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\":\n\t\tif i := int(name[0] - '1'); i < len(r.Params) {\n\t\t\tvr.Kind = expand.String\n\t\t\tvr.Str = r.Params[i]\n\t\t}\n\t}\n\tif vr.Kind != expand.Unknown {\n\t\tvr.Set = true\n\t\treturn vr\n\t}\n\tif vr := r.writeEnv.Get(name); vr.Declared() {\n\t\treturn vr\n\t}\n\treturn expand.Variable{}\n}\n\nfunc (r *Runner) envGet(name string) string {\n\treturn r.lookupVar(name).String()\n}\n\nfunc (r *Runner) delVar(name string) {\n\tif err := r.writeEnv.Set(name, expand.Variable{}); err != nil {\n\t\tr.errf(\"%s: %v\\n\", name, err)\n\t\tr.exit.code = 1\n\t\treturn\n\t}\n}\n\nfunc (r *Runner) setVarString(name, value string) {\n\tr.setVar(name, expand.Variable{Set: true, Kind: expand.String, Str: value})\n}\n\nfunc (r *Runner) setVar(name string, vr expand.Variable) {\n\tif r.opts[optAllExport] {\n\t\tvr.Exported = true\n\t}\n\tif err := r.writeEnv.Set(name, vr); err != nil {\n\t\tr.errf(\"%s: %v\\n\", name, err)\n\t\tr.exit.code = 1\n\t\treturn\n\t}\n}\n\nfunc (r *Runner) setVarWithIndex(prev expand.Variable, name string, index syntax.ArithmExpr, vr expand.Variable) {\n\tprev.Set = true\n\tif name2, var2 := prev.Resolve(r.writeEnv); name2 != \"\" {\n\t\tname = name2\n\t\tprev = var2\n\t}\n\n\tif vr.Kind == expand.String && index == nil {\n\t\t// When assigning a string to an array, fall back to the\n\t\t// zero value for the index.\n\t\tswitch prev.Kind {\n\t\tcase expand.Indexed:\n\t\t\tindex = &syntax.Word{Parts: []syntax.WordPart{\n\t\t\t\t&syntax.Lit{Value: \"0\"},\n\t\t\t}}\n\t\tcase expand.Associative:\n\t\t\tindex = &syntax.Word{Parts: []syntax.WordPart{\n\t\t\t\t&syntax.DblQuoted{},\n\t\t\t}}\n\t\t}\n\t}\n\tif index == nil {\n\t\tr.setVar(name, vr)\n\t\treturn\n\t}\n\n\t// from the syntax package, we know that value must be a string if index\n\t// is non-nil; nested arrays are forbidden.\n\tvalStr := vr.Str\n\n\tvar list []string\n\tswitch prev.Kind {\n\tcase expand.String:\n\t\tlist = append(list, prev.Str)\n\tcase expand.Indexed:\n\t\t// TODO: only clone when inside a subshell and getting a var from outside for the first time\n\t\tlist = slices.Clone(prev.List)\n\tcase expand.Associative:\n\t\t// if the existing variable is already an AssocArray, try our\n\t\t// best to convert the key to a string\n\t\tw, ok := index.(*syntax.Word)\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\t\tk := r.literal(w)\n\n\t\t// TODO: only clone when inside a subshell and getting a var from outside for the first time\n\t\tprev.Map = maps.Clone(prev.Map)\n\t\tif prev.Map == nil {\n\t\t\tprev.Map = make(map[string]string)\n\t\t}\n\t\tprev.Map[k] = valStr\n\t\tr.setVar(name, prev)\n\t\treturn\n\t}\n\tk := r.arithm(index)\n\tfor len(list) < k+1 {\n\t\tlist = append(list, \"\")\n\t}\n\tlist[k] = valStr\n\tprev.Kind = expand.Indexed\n\tprev.List = list\n\tr.setVar(name, prev)\n}\n\nfunc (r *Runner) setFunc(name string, body *syntax.Stmt) {\n\tif r.Funcs == nil {\n\t\tr.Funcs = make(map[string]*syntax.Stmt, 4)\n\t}\n\tr.Funcs[name] = body\n}\n\nfunc stringIndex(index syntax.ArithmExpr) bool {\n\tw, ok := index.(*syntax.Word)\n\tif !ok || len(w.Parts) != 1 {\n\t\treturn false\n\t}\n\tswitch w.Parts[0].(type) {\n\tcase *syntax.DblQuoted, *syntax.SglQuoted:\n\t\treturn true\n\t}\n\treturn false\n}\n\n// TODO: make assignVal and [setVar] consistent with the [expand.WriteEnviron] interface\n\nfunc (r *Runner) assignVal(prev expand.Variable, as *syntax.Assign, valType string) expand.Variable {\n\tprev.Set = true\n\tif as.Value != nil {\n\t\ts := r.literal(as.Value)\n\t\tif !as.Append {\n\t\t\tprev.Kind = expand.String\n\t\t\tif valType == \"-n\" {\n\t\t\t\tprev.Kind = expand.NameRef\n\t\t\t}\n\t\t\tprev.Str = s\n\t\t\treturn prev\n\t\t}\n\t\tswitch prev.Kind {\n\t\tcase expand.String, expand.Unknown:\n\t\t\tprev.Kind = expand.String\n\t\t\tprev.Str += s\n\t\tcase expand.Indexed:\n\t\t\tif len(prev.List) == 0 {\n\t\t\t\tprev.List = append(prev.List, \"\")\n\t\t\t}\n\t\t\tprev.List[0] += s\n\t\tcase expand.Associative:\n\t\t\t// TODO\n\t\t}\n\t\treturn prev\n\t}\n\tif as.Array == nil {\n\t\t// don't return the zero value, as that's an unset variable\n\t\tprev.Kind = expand.String\n\t\tif valType == \"-n\" {\n\t\t\tprev.Kind = expand.NameRef\n\t\t}\n\t\tprev.Str = \"\"\n\t\treturn prev\n\t}\n\t// Array assignment.\n\telems := as.Array.Elems\n\tif valType == \"\" {\n\t\tvalType = \"-a\" // indexed\n\t\tif len(elems) > 0 && stringIndex(elems[0].Index) {\n\t\t\tvalType = \"-A\" // associative\n\t\t}\n\t}\n\tif valType == \"-A\" {\n\t\tamap := make(map[string]string, len(elems))\n\t\tfor _, elem := range elems {\n\t\t\tk := r.literal(elem.Index.(*syntax.Word))\n\t\t\tamap[k] = r.literal(elem.Value)\n\t\t}\n\t\tif !as.Append {\n\t\t\tprev.Kind = expand.Associative\n\t\t\tprev.Map = amap\n\t\t\treturn prev\n\t\t}\n\t\t// TODO\n\t\treturn prev\n\t}\n\t// Evaluate values for each array element.\n\telemValues := make([]struct {\n\t\tindex  int\n\t\tvalues []string\n\t}, len(elems))\n\tvar index, maxIndex int\n\tfor i, elem := range elems {\n\t\tif elem.Index != nil {\n\t\t\t// Index resets our index with a literal value.\n\t\t\tindex = r.arithm(elem.Index)\n\t\t\telemValues[i].values = []string{r.literal(elem.Value)}\n\t\t} else {\n\t\t\t// Implicit index, advancing for every word.\n\t\t\telemValues[i].values = r.fields(elem.Value)\n\t\t}\n\t\telemValues[i].index = index\n\t\tindex += len(elemValues[i].values)\n\t\tmaxIndex = max(maxIndex, index)\n\t}\n\t// Flatten down the values.\n\tstrs := make([]string, maxIndex)\n\tfor _, ev := range elemValues {\n\t\tfor i, str := range ev.values {\n\t\t\tstrs[ev.index+i] = str\n\t\t}\n\t}\n\tif !as.Append {\n\t\tprev.Kind = expand.Indexed\n\t\tprev.List = strs\n\t\treturn prev\n\t}\n\tswitch prev.Kind {\n\tcase expand.Unknown:\n\t\tprev.Kind = expand.Indexed\n\t\tprev.List = strs\n\tcase expand.String:\n\t\tprev.Kind = expand.Indexed\n\t\tprev.List = append([]string{prev.Str}, strs...)\n\tcase expand.Indexed:\n\t\tprev.List = append(prev.List, strs...)\n\tcase expand.Associative:\n\t\t// TODO\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled conversion of kind %d\", prev.Kind))\n\t}\n\treturn prev\n}\n"
  },
  {
    "path": "interp/windows_test.go",
    "content": "// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n//go:build windows\n\npackage interp_test\n\nimport \"golang.org/x/sys/windows\"\n\n// shortPathName is used for testing against DOS short names.\n//\n// Only used for testing, so we assume that a short path always fits in\n// 2*len(path) in UTF-16.\nfunc shortPathName(path string) (string, error) {\n\tsrc, err := windows.UTF16FromString(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdst := make([]uint16, len(src)*2)\n\tif _, err := windows.GetShortPathName(&src[0], &dst[0], uint32(len(dst))); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn windows.UTF16ToString(dst), nil\n}\n"
  },
  {
    "path": "moreinterp/coreutils/coreutils.go",
    "content": "// Copyright (c) 2025, Andrey Nering <andrey@nering.com.br>\n// See LICENSE for licensing information\n\n// Package coreutils provides a middleware for the interpreter that handles\n// core utils commands like cat, chmod, cp, find, ls, mkdir, mv, rm, touch\n// and xargs.\n//\n// This is particularly useful to keep the max compability on Windows where\n// these core utils are not available, unless when installed manually by the\n// user.\npackage coreutils\n\nimport (\n\t\"context\"\n\n\t\"github.com/u-root/u-root/pkg/core\"\n\t\"github.com/u-root/u-root/pkg/core/base64\"\n\t\"github.com/u-root/u-root/pkg/core/cat\"\n\t\"github.com/u-root/u-root/pkg/core/chmod\"\n\t\"github.com/u-root/u-root/pkg/core/cp\"\n\t\"github.com/u-root/u-root/pkg/core/find\"\n\t\"github.com/u-root/u-root/pkg/core/gzip\"\n\t\"github.com/u-root/u-root/pkg/core/ls\"\n\t\"github.com/u-root/u-root/pkg/core/mkdir\"\n\t\"github.com/u-root/u-root/pkg/core/mktemp\"\n\t\"github.com/u-root/u-root/pkg/core/mv\"\n\t\"github.com/u-root/u-root/pkg/core/rm\"\n\t\"github.com/u-root/u-root/pkg/core/shasum\"\n\t\"github.com/u-root/u-root/pkg/core/tar\"\n\t\"github.com/u-root/u-root/pkg/core/touch\"\n\t\"github.com/u-root/u-root/pkg/core/xargs\"\n\t\"mvdan.cc/sh/v3/interp\"\n)\n\nvar commandBuilders = map[string]func() core.Command{\n\t\"cat\":    func() core.Command { return cat.New() },\n\t\"chmod\":  func() core.Command { return chmod.New() },\n\t\"cp\":     func() core.Command { return cp.New() },\n\t\"find\":   func() core.Command { return find.New() },\n\t\"ls\":     func() core.Command { return ls.New() },\n\t\"mkdir\":  func() core.Command { return mkdir.New() },\n\t\"mv\":     func() core.Command { return mv.New() },\n\t\"rm\":     func() core.Command { return rm.New() },\n\t\"touch\":  func() core.Command { return touch.New() },\n\t\"xargs\":  func() core.Command { return xargs.New() },\n\t\"base64\": func() core.Command { return base64.New() },\n\t\"gzcat\":  func() core.Command { return gzip.New(\"gzcat\") },\n\t\"gzip\":   func() core.Command { return gzip.New(\"gzip\") },\n\t\"gunzip\": func() core.Command { return gzip.New(\"gunzip\") },\n\t\"mktemp\": func() core.Command { return mktemp.New() },\n\t\"shasum\": func() core.Command { return shasum.New() },\n\t\"tar\":    func() core.Command { return tar.New() },\n}\n\n// ExecHandler returns an [interp.ExecHandlerFunc] middleware that handles core\n// utils commands.\n//\n// Keep in mind that this middleware has priority over the core utils available\n// by the system. You may want to use only on Windows to ensure that the system\n// core utils are used on other platforms, like macOS and Linux.\nfunc ExecHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {\n\treturn func(ctx context.Context, args []string) error {\n\t\tprogram, programArgs := args[0], args[1:]\n\n\t\tnewCoreUtil, ok := commandBuilders[program]\n\t\tif !ok {\n\t\t\treturn next(ctx, args)\n\t\t}\n\n\t\tc := interp.HandlerCtx(ctx)\n\n\t\tcmd := newCoreUtil()\n\t\tcmd.SetIO(c.Stdin, c.Stdout, c.Stderr)\n\t\tcmd.SetWorkingDir(c.Dir)\n\t\tcmd.SetLookupEnv(func(key string) (string, bool) {\n\t\t\tv := c.Env.Get(key)\n\t\t\treturn v.Str, v.Set\n\t\t})\n\t\tif err := cmd.RunContext(ctx, programArgs...); err != nil {\n\t\t\treturn &Error{err: err}\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "moreinterp/coreutils/coreutils_test.go",
    "content": "// Copyright (c) 2025, Andrey Nering <andrey@nering.com.br>\n// See LICENSE for licensing information\n\npackage coreutils\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"mvdan.cc/sh/v3/interp\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc TestExecHandler(t *testing.T) {\n\tfor coreUtil := range commandBuilders {\n\t\tt.Run(coreUtil, func(t *testing.T) {\n\t\t\tvar in bytes.Buffer\n\t\t\tvar out strings.Builder\n\n\t\t\tr, err := interp.New(\n\t\t\t\tinterp.StdIO(&in, &out, &out),\n\t\t\t\tinterp.ExecHandlers(ExecHandler),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to create interpreter: %v\", err)\n\t\t\t}\n\n\t\t\tcmd := fmt.Sprintf(\"%s --badoption\", coreUtil)\n\n\t\t\tprogram, err := syntax.NewParser().Parse(strings.NewReader(cmd), \"\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to parse command %q: %v\", cmd, err)\n\t\t\t}\n\t\t\terr = r.Run(context.Background(), program)\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"expected error for command %q, got none\", cmd)\n\t\t\t}\n\n\t\t\tif !strings.Contains(err.Error(), \"flag provided but not defined: -badoption\") {\n\t\t\t\tt.Errorf(\"expected error for command %q, got: %v\", cmd, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "moreinterp/coreutils/error.go",
    "content": "package coreutils\n\nimport \"fmt\"\n\n// Error wraps any error returned from the core utilities.\ntype Error struct {\n\terr error\n}\n\nvar (\n\t_ error                       = &Error{}\n\t_ interface{ Unwrap() error } = &Error{}\n)\n\nfunc (err *Error) Error() string {\n\treturn fmt.Sprintf(\"coreutils: %v\", err.err)\n}\n\nfunc (err *Error) Unwrap() error {\n\treturn err.err\n}\n"
  },
  {
    "path": "moreinterp/go.mod",
    "content": "module mvdan.cc/sh/moreinterp\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8\n\tmvdan.cc/sh/v3 v3.11.0\n)\n\nrequire (\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/klauspost/pgzip v1.2.6 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect\n\tgolang.org/x/sys v0.38.0 // indirect\n\tgolang.org/x/term v0.37.0 // indirect\n)\n"
  },
  {
    "path": "moreinterp/go.sum",
    "content": "github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=\ngithub.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=\ngithub.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 h1:cq+DjLAjz3ZPwh0+G571O/jMH0c0DzReDPLjQGL2/BA=\ngithub.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8/go.mod h1:JNauIV2zopCBv/6o+umxcT3bKe8YUqYJaTZQYSYpKss=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=\ngolang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=\nmvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=\nmvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=\n"
  },
  {
    "path": "pattern/example_test.go",
    "content": "// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage pattern_test\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"mvdan.cc/sh/v3/pattern\"\n)\n\nfunc ExampleRegexp() {\n\tpat := \"foo?bar*\"\n\tfmt.Println(pat)\n\n\texpr, err := pattern.Regexp(pat, 0)\n\tif err != nil {\n\t\treturn\n\t}\n\tfmt.Println(expr)\n\n\trx := regexp.MustCompile(expr)\n\tfmt.Println(rx.MatchString(\"foo bar baz\"))\n\tfmt.Println(rx.MatchString(\"foobarbaz\"))\n\t// Output:\n\t// foo?bar*\n\t// (?s)foo.bar.*\n\t// true\n\t// false\n}\n\nfunc ExampleQuoteMeta() {\n\tpat := \"foo?bar*\"\n\tconst mode = 0\n\tfmt.Println(pat)\n\n\tquoted := pattern.QuoteMeta(pat, mode)\n\tfmt.Println(quoted)\n\n\texpr, err := pattern.Regexp(quoted, mode)\n\tif err != nil {\n\t\treturn\n\t}\n\n\trx := regexp.MustCompile(expr)\n\tfmt.Println(rx.MatchString(\"foo bar baz\"))\n\tfmt.Println(rx.MatchString(\"foo?bar*\"))\n\t// Output:\n\t// foo?bar*\n\t// foo\\?bar\\*\n\t// false\n\t// true\n}\n"
  },
  {
    "path": "pattern/pattern.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package pattern allows working with shell pattern matching notation, also\n// known as wildcards or globbing.\n//\n// For reference, see\n// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13.\npackage pattern\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"regexp\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\n// Mode can be used to supply a number of options to the package's functions.\n// Not all functions change their behavior with all of the options below.\ntype Mode uint\n\ntype SyntaxError struct {\n\tmsg string\n\terr error\n}\n\nfunc (e SyntaxError) Error() string { return e.msg }\n\nfunc (e SyntaxError) Unwrap() error { return e.err }\n\n// NegExtGlobGroup represents the byte offset range of a single !(expr) group\n// within a pattern string. Start is the offset of '!', End is one past ')'.\ntype NegExtGlobGroup struct {\n\tStart, End int\n}\n\n// NegExtGlobError is returned by [Regexp] when an extglob negation operator\n// !(pattern-list) is encountered, as Go's [regexp] package does not support\n// negative lookahead. Callers can handle this by negating the result of\n// matching the inner pattern.\ntype NegExtGlobError struct {\n\tGroups []NegExtGlobGroup\n}\n\nfunc (e *NegExtGlobError) Error() string {\n\treturn \"extglob !(...) is not supported in this scenario\"\n}\n\n// TODO(v4): flip NoGlobStar to be opt-in via GlobStar, matching bash\n// TODO(v4): flip EntireString to be opt-out via PartialMatch, as EntireString causes subtle bugs when forgotten\n// TODO(v4): rename NoGlobCase to CaseInsensitive for readability\n\nconst (\n\tShortest          Mode = 1 << iota // prefer the shortest match.\n\tFilenames                          // \"*\" and \"?\" don't match slashes; only \"**\" does; only makes sense with EntireString too\n\tEntireString                       // match the entire string using ^$ delimiters\n\tNoGlobCase                         // do case-insensitive match (that is, use (?i) in the regexp); shopt \"nocaseglob\"\n\tNoGlobStar                         // do not support \"**\"; negated shopt \"globstar\"\n\tGlobLeadingDot                     // let wildcards match leading dots in filenames; shopt \"dotglob\"\n\tExtendedOperators                  // support extended pattern matching operators; shopt \"extglob\" for pathname expansion\n)\n\n// Regexp turns a shell pattern into a regular expression that can be used with\n// [regexp.Compile]. It will return an error if the input pattern was incorrect.\n// Otherwise, the returned expression can be passed to [regexp.MustCompile].\n//\n// For example, Regexp(`foo*bar?`, true) returns `foo.*bar.`.\n//\n// Note that this function (and [QuoteMeta]) should not be directly used with file\n// paths if Windows is supported, as the path separator on that platform is the\n// same character as the escaping character for shell patterns.\nfunc Regexp(pat string, mode Mode) (string, error) {\n\t// If there are no special pattern matching or regular expression characters,\n\t// and we don't need to insert extras for the modes affecting non-special characters,\n\t// we can directly return the input string as a short-cut.\n\tif mode&(EntireString|NoGlobCase) == 0 {\n\t\tneedsEscaping := false\n\tnoopLoop:\n\t\tfor _, r := range pat {\n\t\t\tswitch r {\n\t\t\t// including those that need escaping since they are\n\t\t\t// regular expression metacharacters\n\t\t\tcase '*', '?', '[', '\\\\', '.', '+', '(', ')', '|',\n\t\t\t\t']', '{', '}', '^', '$':\n\t\t\t\tneedsEscaping = true\n\t\t\t\tbreak noopLoop\n\t\t\t}\n\t\t}\n\t\tif !needsEscaping {\n\t\t\treturn pat, nil\n\t\t}\n\t}\n\tvar sb strings.Builder\n\t// Enable matching `\\n` with the `.` metacharacter as globs match `\\n`\n\tsb.WriteString(`(?s`)\n\tif mode&NoGlobCase != 0 {\n\t\tsb.WriteString(`i`)\n\t}\n\tif mode&Shortest != 0 {\n\t\tsb.WriteString(`U`)\n\t}\n\tsb.WriteString(`)`)\n\tif mode&EntireString != 0 {\n\t\tsb.WriteString(`^`)\n\t}\n\tsl := stringLexer{s: pat}\n\tvar negGroups []NegExtGlobGroup\n\tfor {\n\t\tif err := regexpNext(&sb, &sl, mode); err == io.EOF {\n\t\t\tbreak\n\t\t} else if err != nil {\n\t\t\tnegErr, ok := err.(*NegExtGlobError)\n\t\t\tif !ok {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tnegGroups = append(negGroups, negErr.Groups...)\n\t\t}\n\t}\n\tif len(negGroups) > 0 {\n\t\treturn \"\", &NegExtGlobError{Groups: negGroups}\n\t}\n\tif mode&EntireString != 0 {\n\t\tsb.WriteString(`$`)\n\t}\n\treturn sb.String(), nil\n}\n\n// stringLexer helps us tokenize a pattern string.\n// Note that we can use the null byte '\\x00' to signal \"no character\" as shell strings cannot contain null bytes.\n// TODO: should the tokenization be based on runes? e.g: [á-é]\ntype stringLexer struct {\n\ts string\n\ti int\n}\n\nfunc (sl *stringLexer) next() byte {\n\tif sl.i >= len(sl.s) {\n\t\treturn '\\x00'\n\t}\n\tc := sl.s[sl.i]\n\tsl.i++\n\treturn c\n}\n\nfunc (sl *stringLexer) last() byte {\n\tif sl.i < 2 {\n\t\treturn '\\x00'\n\t}\n\treturn sl.s[sl.i-2]\n}\n\nfunc (sl *stringLexer) peekNext() byte {\n\tif sl.i >= len(sl.s) {\n\t\treturn '\\x00'\n\t}\n\treturn sl.s[sl.i]\n}\n\nfunc (sl *stringLexer) peekRest() string {\n\treturn sl.s[sl.i:]\n}\n\nfunc regexpNext(sb *strings.Builder, sl *stringLexer, mode Mode) error {\n\tc := sl.next()\n\tif mode&ExtendedOperators != 0 {\n\t\t// Handle extended pattern matching operators separately,\n\t\t// given that they can be one of many two-character prefixes.\n\t\t// Note that we recurse into the same function in a loop,\n\t\t// as each of the patterns in the list separated by '|' is a regular pattern.\n\t\tswitch op := c; op {\n\t\tcase '!', '?', '*', '+', '@':\n\t\t\tif sl.peekNext() != '(' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tstart := sl.i - 1       // position of the operator\n\t\t\tsb.WriteByte(sl.next()) // (\n\t\tnestedLoop:\n\t\t\tfor {\n\t\t\t\tswitch sl.peekNext() {\n\t\t\t\tcase ')':\n\t\t\t\t\tbreak nestedLoop\n\t\t\t\tcase '|':\n\t\t\t\t\t// extended operators support a list of \"or\" separated expressions\n\t\t\t\t\tsb.WriteByte(sl.next())\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif err := regexpNext(sb, sl, mode); err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t} else if err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tsb.WriteByte(sl.next()) // )\n\t\t\tif op == '!' {\n\t\t\t\treturn &NegExtGlobError{Groups: []NegExtGlobGroup{{Start: start, End: sl.i}}}\n\t\t\t}\n\t\t\tif op != '@' {\n\t\t\t\t// @( is [syntax.GlobOne] for matching once; no suffix needed\n\t\t\t\tsb.WriteByte(op)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\tswitch c {\n\tcase '\\x00':\n\t\treturn io.EOF\n\tcase '*':\n\t\tif mode&Filenames == 0 {\n\t\t\t// * - matches anything when not in filename mode\n\t\t\tsb.WriteString(`.*`)\n\t\t\tbreak\n\t\t}\n\t\t// \"**\" only acts as globstar if it is alone as a path element.\n\t\tsingleBefore := sl.i == 1 || sl.last() == '/'\n\t\tif sl.peekNext() == '*' {\n\t\t\tsl.i++\n\t\t\tsingleAfter := sl.i == len(sl.s) || sl.peekNext() == '/'\n\t\t\tif mode&NoGlobStar == 0 && singleBefore && singleAfter {\n\t\t\t\t// ** - match any number of slashes or \"*\" path elements\n\t\t\t\tslashSuffix := sl.peekNext() == '/'\n\t\t\t\tif slashSuffix {\n\t\t\t\t\t// **/ - like \"**\" but requiring a trailing slash when matching\n\t\t\t\t\tsl.i++\n\t\t\t\t\t// wrap the expression to ensure that any match has a slash suffix\n\t\t\t\t\tsb.WriteString(`(`)\n\t\t\t\t}\n\t\t\t\tif mode&GlobLeadingDot == 0 {\n\t\t\t\t\tsb.WriteString(`(/|[^/.][^/]*)*`)\n\t\t\t\t} else {\n\t\t\t\t\t// with GlobLeadingDot (dotglob), match anything at all\n\t\t\t\t\tsb.WriteString(`.*`)\n\t\t\t\t}\n\t\t\t\tif slashSuffix {\n\t\t\t\t\tsb.WriteString(`/)?`)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// foo**, **bar, or NoGlobStar - behaves like \"*\" below\n\t\t}\n\t\t// * - matches anything except slashes and leading dots\n\t\tif singleBefore && mode&GlobLeadingDot == 0 {\n\t\t\tsb.WriteString(`([^/.][^/]*)?`)\n\t\t} else {\n\t\t\t// with GlobLeadingDot (dotglob), match anything except slashes\n\t\t\tsb.WriteString(`[^/]*`)\n\t\t}\n\tcase '?':\n\t\tif mode&Filenames != 0 {\n\t\t\tsb.WriteString(`[^/]`)\n\t\t} else {\n\t\t\tsb.WriteByte('.')\n\t\t}\n\tcase '\\\\':\n\t\tc = sl.next()\n\t\tif c == '\\x00' {\n\t\t\treturn &SyntaxError{msg: `\\ at end of pattern`}\n\t\t}\n\t\tsb.WriteString(regexp.QuoteMeta(string(c)))\n\tcase '[':\n\t\t// TODO: surely char classes can be mixed with others, e.g. [[:foo:]xyz]\n\t\tif name, err := charClass(sl.peekRest()); err != nil {\n\t\t\treturn &SyntaxError{msg: \"charClass invalid\", err: err}\n\t\t} else if name != \"\" {\n\t\t\tsb.WriteByte('[')\n\t\t\tsb.WriteString(name)\n\t\t\tsl.i += len(name)\n\t\t\tbreak\n\t\t}\n\t\tif mode&Filenames != 0 {\n\t\t\tfor i, c := range sl.peekRest() {\n\t\t\t\tif i > 0 && c == ']' {\n\t\t\t\t\tbreak\n\t\t\t\t} else if c == '/' {\n\t\t\t\t\tsb.WriteString(`\\[`)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsb.WriteByte(c)\n\t\tif c = sl.next(); c == '\\x00' {\n\t\t\treturn &SyntaxError{msg: \"[ was not matched with a closing ]\"}\n\t\t}\n\t\tswitch c {\n\t\tcase '!', '^':\n\t\t\tsb.WriteByte('^')\n\t\t\tif c = sl.next(); c == '\\x00' {\n\t\t\t\treturn &SyntaxError{msg: \"[ was not matched with a closing ]\"}\n\t\t\t}\n\t\t}\n\t\tif c == ']' {\n\t\t\tsb.WriteByte(']')\n\t\t\tif c = sl.next(); c == '\\x00' {\n\t\t\t\treturn &SyntaxError{msg: \"[ was not matched with a closing ]\"}\n\t\t\t}\n\t\t}\n\t\tfor {\n\t\t\tsb.WriteByte(c)\n\t\t\tswitch c {\n\t\t\tcase '\\x00':\n\t\t\t\treturn &SyntaxError{msg: \"[ was not matched with a closing ]\"}\n\t\t\tcase '\\\\':\n\t\t\t\tif c = sl.next(); c != '0' {\n\t\t\t\t\tsb.WriteByte(c)\n\t\t\t\t}\n\t\t\tcase '-':\n\t\t\t\tstart := sl.last()\n\t\t\t\tend := sl.peekNext()\n\t\t\t\t// TODO: what about overlapping ranges, like: [a--z]\n\t\t\t\tif end != ']' && start > end {\n\t\t\t\t\treturn &SyntaxError{msg: fmt.Sprintf(\"invalid range: %c-%c\", start, end)}\n\t\t\t\t}\n\t\t\tcase ']':\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tc = sl.next()\n\t\t}\n\tdefault:\n\t\tif c > utf8.RuneSelf {\n\t\t\tsb.WriteByte(c)\n\t\t} else {\n\t\t\tsb.WriteString(regexp.QuoteMeta(string(c)))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc charClass(s string) (string, error) {\n\tif strings.HasPrefix(s, \"[.\") || strings.HasPrefix(s, \"[=\") {\n\t\treturn \"\", fmt.Errorf(\"collating features not available\")\n\t}\n\tname, ok := strings.CutPrefix(s, \"[:\")\n\tif !ok {\n\t\treturn \"\", nil\n\t}\n\tname, _, ok = strings.Cut(name, \":]]\")\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"[[: was not matched with a closing :]]\")\n\t}\n\tswitch name {\n\tcase \"alnum\", \"alpha\", \"ascii\", \"blank\", \"cntrl\", \"digit\", \"graph\",\n\t\t\"lower\", \"print\", \"punct\", \"space\", \"upper\", \"word\", \"xdigit\":\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"invalid character class: %q\", name)\n\t}\n\treturn s[:len(name)+5], nil\n}\n\n// HasMeta returns whether a string contains any unescaped pattern\n// metacharacters: '*', '?', or '['. When the function returns false, the given\n// pattern can only match at most one string.\n//\n// For example, HasMeta(`foo\\*bar`) returns false, but HasMeta(`foo*bar`)\n// returns true.\n//\n// This can be useful to avoid extra work, like [Regexp]. Note that this\n// function cannot be used to avoid [QuoteMeta], as backslashes are quoted by\n// that function but ignored here.\n//\n// The [Mode] parameter is unused, and will be removed in v4.\nfunc HasMeta(pat string, mode Mode) bool {\n\tfor i := 0; i < len(pat); i++ {\n\t\tswitch pat[i] {\n\t\tcase '\\\\':\n\t\t\ti++\n\t\tcase '*', '?', '[':\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// QuoteMeta returns a string that quotes all pattern metacharacters in the\n// given text. The returned string is a pattern that matches the literal text.\n//\n// For example, QuoteMeta(`foo*bar?`) returns `foo\\*bar\\?`.\n//\n// The [Mode] parameter is unused, and will be removed in v4.\nfunc QuoteMeta(pat string, mode Mode) string {\n\tneedsEscaping := false\nloop:\n\tfor _, r := range pat {\n\t\tswitch r {\n\t\tcase '*', '?', '[', '\\\\':\n\t\t\tneedsEscaping = true\n\t\t\tbreak loop\n\t\t}\n\t}\n\tif !needsEscaping { // short-cut without a string copy\n\t\treturn pat\n\t}\n\tvar sb strings.Builder\n\tfor _, r := range pat {\n\t\tswitch r {\n\t\tcase '*', '?', '[', '\\\\':\n\t\t\tsb.WriteByte('\\\\')\n\t\t}\n\t\tsb.WriteRune(r)\n\t}\n\treturn sb.String()\n}\n"
  },
  {
    "path": "pattern/pattern_test.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage pattern\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"regexp/syntax\"\n\t\"testing\"\n\n\t\"github.com/go-quicktest/qt\"\n)\n\nvar regexpTests = []struct {\n\tpat     string\n\tmode    Mode\n\twant    string\n\twantErr string\n\n\tmustMatch    []string\n\tmustNotMatch []string\n}{\n\t{pat: ``, want: ``},\n\t{pat: `foo`, want: `foo`},\n\t{\n\t\tpat: `foo`, mode: NoGlobCase, want: `(?si)foo`,\n\t\tmustMatch:    []string{\"foo\", \"FOO\", \"Foo\"},\n\t\tmustNotMatch: []string{\"bar\"},\n\t},\n\t{pat: `foóà中`, mode: Filenames, want: `foóà中`},\n\t{pat: `.`, want: `(?s)\\.`},\n\t{pat: `foo*`, want: `(?s)foo.*`},\n\t{pat: `foo*`, mode: Shortest, want: `(?sU)foo.*`},\n\t{pat: `foo*`, mode: Shortest | Filenames, want: `(?sU)foo[^/]*`},\n\t{\n\t\tpat: `*foo*`, mode: EntireString, want: `(?s)^.*foo.*$`,\n\t\tmustMatch:    []string{\"foo\", \"prefix-foo\", \"foo-suffix\", \"foo.suffix\", \".foo.\", \"a\\nbfooc\\nd\"},\n\t\tmustNotMatch: []string{\"bar\"},\n\t},\n\t{\n\t\tpat: `foo*`, mode: Filenames | EntireString, want: `(?s)^foo[^/]*$`,\n\t\tmustMatch:    []string{\"foo\", \"foo-suffix\", \"foo.suffix\", \"foo\\nsuffix\"},\n\t\tmustNotMatch: []string{\"prefix-foo\", \"foo/suffix\"},\n\t},\n\t{\n\t\tpat: `foo/*`, mode: Filenames | EntireString, want: `(?s)^foo/([^/.][^/]*)?$`,\n\t\tmustMatch:    []string{\"foo/\", \"foo/suffix\"},\n\t\tmustNotMatch: []string{\"foo/.suffix\", \"foo/bar/baz\"},\n\t},\n\t{\n\t\tpat: `foo/*`, mode: Filenames | EntireString | GlobLeadingDot, want: `(?s)^foo/[^/]*$`,\n\t\tmustMatch:    []string{\"foo/\", \"foo/suffix\", \"foo/.suffix\"},\n\t\tmustNotMatch: []string{\"foo/bar/baz\"},\n\t},\n\t{pat: `*foo`, mode: Filenames, want: `(?s)([^/.][^/]*)?foo`},\n\t{\n\t\tpat: `*foo`, mode: Filenames | EntireString, want: `(?s)^([^/.][^/]*)?foo$`,\n\t\tmustMatch:    []string{\"foo\", \"prefix-foo\", \"prefix.foo\"},\n\t\tmustNotMatch: []string{\"foo-suffix\", \"/prefix/foo\", \".foo\", \".prefix-foo\"},\n\t},\n\t{pat: `**`, want: `(?s).*.*`},\n\t{\n\t\tpat: `**`, mode: Filenames | EntireString, want: `(?s)^(/|[^/.][^/]*)*$`,\n\t\tmustMatch:    []string{\"/foo\", \"/prefix/foo\", \"/a.b.c/foo\", \"/a/b/c/foo\", \"/foo/suffix.ext\", \"/a\\n/\\nb\"},\n\t\tmustNotMatch: []string{\"/.prefix/foo\", \"/prefix/.foo\"},\n\t},\n\t{\n\t\tpat: `**`, mode: Filenames | NoGlobStar | EntireString, want: `(?s)^([^/.][^/]*)?$`,\n\t\tmustMatch:    []string{\"foo.bar\"},\n\t\tmustNotMatch: []string{\"foo/bar\", \".foo\"},\n\t},\n\t{\n\t\tpat: `**`, mode: Filenames | EntireString | GlobLeadingDot, want: `(?s)^.*$`,\n\t\tmustMatch: []string{\"/foo\", \"/prefix/foo\", \"/a.b.c/foo\", \"/a/b/c/foo\", \"/foo/suffix.ext\", \"/a\\n/\\nb\", \"/.prefix/foo\", \"/prefix/.foo\"},\n\t},\n\t{pat: `/**/foo`, want: `(?s)/.*.*/foo`},\n\t{\n\t\tpat: `/**/foo`, mode: Filenames | EntireString, want: `(?s)^/((/|[^/.][^/]*)*/)?foo$`,\n\t\tmustMatch:    []string{\"/foo\", \"/prefix/foo\", \"/a.b.c/foo\", \"/a/b/c/foo\"},\n\t\tmustNotMatch: []string{\"/foo/suffix\", \"prefix/foo\", \"/.prefix/foo\", \"/prefix/.foo\"},\n\t},\n\t{\n\t\tpat: `/**/foo`, mode: Filenames | EntireString | GlobLeadingDot, want: `(?s)^/(.*/)?foo$`,\n\t\tmustMatch:    []string{\"/foo\", \"/prefix/foo\", \"/a.b.c/foo\", \"/a/b/c/foo\", \"/.prefix/foo\"},\n\t\tmustNotMatch: []string{\"/foo/suffix\", \"prefix/foo\", \"/prefix/.foo\"},\n\t},\n\t{pat: `/**/foo`, mode: Filenames | NoGlobStar, want: `(?s)/([^/.][^/]*)?/foo`},\n\t{pat: `/**/à`, mode: Filenames, want: `(?s)/((/|[^/.][^/]*)*/)?à`},\n\t{\n\t\tpat: `/**foo`, mode: Filenames, want: `(?s)/([^/.][^/]*)?foo`,\n\t\t// These all match because without EntireString, we match substrings.\n\t\tmustMatch: []string{\"/foo\", \"/prefix-foo\", \"/foo-suffix\", \"/sub/foo\"},\n\t},\n\t{\n\t\tpat: `/**foo`, mode: Filenames | EntireString, want: `(?s)^/([^/.][^/]*)?foo$`,\n\t\tmustMatch:    []string{\"/foo\", \"/prefix-foo\"},\n\t\tmustNotMatch: []string{\"/foo-suffix\", \"/sub/foo\", \"/.foo\", \"/.prefix-foo\"},\n\t},\n\t{\n\t\tpat: `/foo**`, mode: Filenames | EntireString, want: `(?s)^/foo[^/]*$`,\n\t\tmustMatch:    []string{\"/foo\", \"/foo-suffix\", \"/foo.suffix\"},\n\t\tmustNotMatch: []string{\"/prefix-foo\", \"/foo/sub\"},\n\t},\n\t{pat: `\\*`, want: `(?s)\\*`},\n\t{pat: `\\`, wantErr: `^\\\\ at end of pattern$`},\n\t{pat: `?`, want: `(?s).`},\n\t{\n\t\tpat: `?`, mode: EntireString, want: `(?s)^.$`,\n\t\tmustMatch:    []string{\"a\", \"\\n\", \" \"},\n\t\tmustNotMatch: []string{\"abc\", \"\"},\n\t},\n\t{pat: `?`, mode: Filenames, want: `(?s)[^/]`},\n\t{pat: `?à`, want: `(?s).à`},\n\t{pat: `\\a`, want: `(?s)a`},\n\t{pat: `(`, want: `(?s)\\(`},\n\t{pat: `a|b`, want: `(?s)a\\|b`},\n\t{pat: `x{3}`, want: `(?s)x\\{3\\}`},\n\t{pat: `{3,4}`, want: `(?s)\\{3,4\\}`},\n\t{pat: `[a]`, want: `(?s)[a]`},\n\t{pat: `[abc]`, want: `(?s)[abc]`},\n\t{pat: `[^bc]`, want: `(?s)[^bc]`},\n\t{pat: `[!bc]`, want: `(?s)[^bc]`},\n\t{pat: `[[]`, want: `(?s)[[]`},\n\t{pat: `[\\]]`, want: `(?s)[\\]]`},\n\t{pat: `[\\]]`, mode: Filenames, want: `(?s)[\\]]`},\n\t{pat: `[]]`, want: `(?s)[]]`},\n\t{pat: `[!]]`, want: `(?s)[^]]`},\n\t{pat: `[^]]`, want: `(?s)[^]]`},\n\t{pat: `[a/b]`, want: `(?s)[a/b]`},\n\t{\n\t\tpat: `[a/b]`, mode: EntireString | Filenames, want: `(?s)^\\[a/b\\]$`,\n\t\tmustMatch:    []string{\"[a/b]\"},\n\t\tmustNotMatch: []string{\"a\", \"/\", \"b\"},\n\t},\n\t{\n\t\tpat: `[]/a]`, mode: EntireString | Filenames, want: `(?s)^\\[\\]/a\\]$`,\n\t\tmustMatch:    []string{\"[]/a]\"},\n\t\tmustNotMatch: []string{\"]\", \"/\", \"a\", \"/a]\", \"/a\"},\n\t},\n\t{pat: `[`, wantErr: `^\\[ was not matched with a closing \\]$`},\n\t{pat: `[\\`, wantErr: `^\\[ was not matched with a closing \\]$`},\n\t{pat: `[^`, wantErr: `^\\[ was not matched with a closing \\]$`},\n\t{pat: `[!`, wantErr: `^\\[ was not matched with a closing \\]$`},\n\t{pat: `[!bc]`, want: `(?s)[^bc]`},\n\t{pat: `[]`, wantErr: `^\\[ was not matched with a closing \\]$`},\n\t{pat: `[^]`, wantErr: `^\\[ was not matched with a closing \\]$`},\n\t{pat: `[!]`, wantErr: `^\\[ was not matched with a closing \\]$`},\n\t{pat: `[ab`, wantErr: `^\\[ was not matched with a closing \\]$`},\n\t{pat: `[a-]`, want: `(?s)[a-]`},\n\t{pat: `[z-a]`, wantErr: `^invalid range: z-a$`},\n\t{pat: `[a-a]`, want: `(?s)[a-a]`},\n\t{pat: `[aa]`, want: `(?s)[aa]`},\n\t{pat: `[0-4A-Z]`, want: `(?s)[0-4A-Z]`},\n\t{pat: `[-a]`, want: `(?s)[-a]`},\n\t{pat: `[^-a]`, want: `(?s)[^-a]`},\n\t{pat: `[a-]`, want: `(?s)[a-]`},\n\t{pat: `[[:digit:]]`, want: `(?s)[[:digit:]]`},\n\t{pat: `[[:`, wantErr: `^charClass invalid$`},\n\t{pat: `[[:digit`, wantErr: `^charClass invalid$`},\n\t{pat: `[[:wrong:]]`, wantErr: `^charClass invalid$`},\n\t{pat: `[[=x=]]`, wantErr: `^charClass invalid$`},\n\t{pat: `[[.x.]]`, wantErr: `^charClass invalid$`},\n}\n\nfunc TestRegexp(t *testing.T) {\n\tt.Parallel()\n\tfor i, tc := range regexpTests {\n\t\tt.Run(fmt.Sprintf(\"%02d\", i), func(t *testing.T) {\n\t\t\tt.Logf(\"input: pattern=%q mode=%#b\\n\", tc.pat, tc.mode)\n\t\t\tgot, gotErr := Regexp(tc.pat, tc.mode)\n\t\t\tif tc.wantErr != \"\" {\n\t\t\t\tqt.Assert(t, qt.ErrorMatches(gotErr, tc.wantErr))\n\t\t\t} else {\n\t\t\t\tqt.Assert(t, qt.IsNil(gotErr))\n\t\t\t}\n\t\t\tif got != tc.want {\n\t\t\t\tt.Errorf(\"(%q, %#b) got %q, wanted %q\", tc.pat, tc.mode, got, tc.want)\n\t\t\t}\n\t\t\t_, rxErr := syntax.Parse(got, syntax.Perl)\n\t\t\tif gotErr == nil && rxErr != nil {\n\t\t\t\tt.Fatalf(\"regexp/syntax.Parse(%q) failed with %q\", got, rxErr)\n\t\t\t}\n\t\t\trx := regexp.MustCompile(got)\n\t\t\tfor _, s := range tc.mustMatch {\n\t\t\t\tqt.Check(t, qt.IsTrue(rx.MatchString(s)), qt.Commentf(\"must match: %q\", s))\n\t\t\t}\n\t\t\tfor _, s := range tc.mustNotMatch {\n\t\t\t\tqt.Check(t, qt.IsFalse(rx.MatchString(s)), qt.Commentf(\"must not match: %q\", s))\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar metaTests = []struct {\n\tpat       string\n\twantHas   bool\n\twantQuote string\n}{\n\t{``, false, ``},\n\t{`foo`, false, `foo`},\n\t{`.`, false, `.`},\n\t{`*`, true, `\\*`},\n\t{`foo?`, true, `foo\\?`},\n\t{`\\[`, false, `\\\\\\[`},\n\t{`{`, false, `{`},\n}\n\nfunc TestMeta(t *testing.T) {\n\tt.Parallel()\n\tfor _, tc := range metaTests {\n\t\tif got := HasMeta(tc.pat, 0); got != tc.wantHas {\n\t\t\tt.Errorf(\"HasMeta(%q, 0) got %t, wanted %t\",\n\t\t\t\ttc.pat, got, tc.wantHas)\n\t\t}\n\t\tif got := QuoteMeta(tc.pat, 0); got != tc.wantQuote {\n\t\t\tt.Errorf(\"QuoteMeta(%q, 0) got %q, wanted %q\",\n\t\t\t\ttc.pat, got, tc.wantQuote)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "shell/doc.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package shell contains high-level features that use the syntax, expand, and\n// interp packages under the hood.\n//\n// Please note that this package uses POSIX Shell syntax. As such, path names on\n// Windows need to use double backslashes or be within single quotes when given\n// to functions like Fields. For example:\n//\n//\tshell.Fields(\"echo /foo/bar\")     // on Unix-like\n//\tshell.Fields(\"echo C:\\\\foo\\\\bar\") // on Windows\n//\tshell.Fields(\"echo 'C:\\foo\\bar'\") // on Windows, with quotes\npackage shell\n"
  },
  {
    "path": "shell/example_test.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage shell_test\n\nimport (\n\t\"fmt\"\n\n\t\"mvdan.cc/sh/v3/shell\"\n)\n\nfunc ExampleExpand() {\n\tenv := func(name string) string {\n\t\tswitch name {\n\t\tcase \"HOME\":\n\t\t\treturn \"/home/user\"\n\t\t}\n\t\treturn \"\" // leave the rest unset\n\t}\n\tout, _ := shell.Expand(\"No place like $HOME\", env)\n\tfmt.Println(out)\n\n\tout, _ = shell.Expand(\"Some vars are ${missing:-awesome}\", env)\n\tfmt.Println(out)\n\n\tout, _ = shell.Expand(\"Math is fun! $((12 * 34))\", nil)\n\tfmt.Println(out)\n\t// Output:\n\t// No place like /home/user\n\t// Some vars are awesome\n\t// Math is fun! 408\n}\n\nfunc ExampleFields() {\n\tenv := func(name string) string {\n\t\tswitch name {\n\t\tcase \"foo\":\n\t\t\treturn \"bar baz\"\n\t\t}\n\t\treturn \"\" // leave the rest unset\n\t}\n\tout, _ := shell.Fields(`\"many quoted\" ' strings '`, env)\n\tfmt.Printf(\"%#v\\n\", out)\n\n\tout, _ = shell.Fields(\"unquoted $foo\", env)\n\tfmt.Printf(\"%#v\\n\", out)\n\n\tout, _ = shell.Fields(`quoted \"$foo\"`, env)\n\tfmt.Printf(\"%#v\\n\", out)\n\t// Output:\n\t// []string{\"many quoted\", \" strings \"}\n\t// []string{\"unquoted\", \"bar\", \"baz\"}\n\t// []string{\"quoted\", \"bar baz\"}\n}\n"
  },
  {
    "path": "shell/expand.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage shell\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// Expand performs shell expansion on s as if it were within double quotes,\n// using env to resolve variables. This includes parameter expansion, arithmetic\n// expansion, and quote removal.\n//\n// If env is nil, the current environment variables are used. Empty variables\n// are treated as unset; to support variables which are set but empty, use the\n// [expand] package directly.\n//\n// Other forms of expansion are not supported in this simple API, such as\n// command substitutions like $(echo foo). To support them, use the [expand] package.\n//\n// An error will be reported if the input string had invalid syntax.\nfunc Expand(s string, env func(string) string) (string, error) {\n\tp := syntax.NewParser()\n\tword, err := p.Document(strings.NewReader(s))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif env == nil {\n\t\tenv = os.Getenv\n\t}\n\tcfg := &expand.Config{Env: expand.FuncEnviron(env)}\n\treturn expand.Document(cfg, word)\n}\n\n// Fields performs shell expansion on s as if it were a command's arguments,\n// using env to resolve variables. It is similar to Expand, but includes brace\n// expansion, tilde expansion, and word splitting.\n//\n// If env is nil, the current environment variables are used. Empty variables\n// are treated as unset; to support variables which are set but empty, use the\n// [expand] package directly.\n//\n// Other forms of expansion are not supported in this simple API, such as\n// globbing and command substitutions like $(echo foo).\n// To support them, use the [expand] package.\n//\n// An error will be reported if the input string had invalid syntax.\nfunc Fields(s string, env func(string) string) ([]string, error) {\n\tp := syntax.NewParser()\n\tvar words []*syntax.Word\n\tfor w, err := range p.WordsSeq(strings.NewReader(s)) {\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\twords = append(words, w)\n\t}\n\tif env == nil {\n\t\tenv = os.Getenv\n\t}\n\tcfg := &expand.Config{Env: expand.FuncEnviron(env)}\n\treturn expand.Fields(cfg, words...)\n}\n"
  },
  {
    "path": "shell/expand_test.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage shell\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc strEnviron(pairs ...string) func(string) string {\n\treturn func(name string) string {\n\t\tprefix := name + \"=\"\n\t\tfor _, pair := range pairs {\n\t\t\tif val, ok := strings.CutPrefix(pair, prefix); ok {\n\t\t\t\treturn val\n\t\t\t}\n\t\t}\n\t\treturn \"\"\n\t}\n}\n\nvar expandTests = []struct {\n\tin   string\n\tenv  func(name string) string\n\twant string\n}{\n\t{\"foo\", nil, \"foo\"},\n\t{\"\\nfoo\\n\", nil, \"\\nfoo\\n\"},\n\t{\"a-$b-c\", nil, \"a--c\"},\n\t{\"${INTERP_GLOBAL:+hasOsEnv}\", nil, \"hasOsEnv\"},\n\t{\"a-$b-c\", strEnviron(), \"a--c\"},\n\t{\"a-$b-c\", strEnviron(\"b=b_val\"), \"a-b_val-c\"},\n\t{\"${x//o/a}\", strEnviron(\"x=foo\"), \"faa\"},\n\t{\"*.go\", nil, \"*.go\"},\n\t{\"~\", nil, \"~\"},\n}\n\nfunc TestExpand(t *testing.T) {\n\tos.Setenv(\"INTERP_GLOBAL\", \"value\")\n\tfor _, tc := range expandTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := Expand(tc.in, tc.env)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif got != tc.want {\n\t\t\t\tt.Fatalf(\"\\nwant: %q\\ngot:  %q\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnexpectedCmdSubst(t *testing.T) {\n\tt.Parallel()\n\twant := \"unexpected command substitution at 1:6\"\n\tfor _, fn := range []func() error{\n\t\tfunc() error {\n\t\t\t_, err := Expand(\"echo $(uname -a)\", nil)\n\t\t\treturn err\n\t\t},\n\t\tfunc() error {\n\t\t\t_, err := Fields(\"echo $(uname -a)\", nil)\n\t\t\treturn err\n\t\t},\n\t} {\n\t\tgot := fmt.Sprint(fn())\n\t\tif !strings.Contains(got, want) {\n\t\t\tt.Fatalf(\"wanted error %q, got: %s\", want, got)\n\t\t}\n\t}\n}\n\nvar fieldsTests = []struct {\n\tin   string\n\tenv  func(name string) string\n\twant []string\n}{\n\t{\"foo\", nil, []string{\"foo\"}},\n\t{\"\\nfoo\\n\", nil, []string{\"foo\"}},\n\t{\"foo bar\", nil, []string{\"foo\", \"bar\"}},\n\t{\"foo 'bar baz'\", nil, []string{\"foo\", \"bar baz\"}},\n\t{\"$x\", strEnviron(\"x=foo bar\"), []string{\"foo\", \"bar\"}},\n\t{`\"$x\"`, strEnviron(\"x=foo bar\"), []string{\"foo bar\"}},\n\t{\"~\", strEnviron(\"HOME=/my/home\"), []string{\"/my/home\"}},\n\t{\"~/foo/bar\", strEnviron(\"HOME=/my/home\"), []string{\"/my/home/foo/bar\"}},\n\t{\"~foo/file\", strEnviron(\"HOME foo=/bar\"), []string{\"/bar/file\"}},\n\t{\"*.go\", nil, []string{\"*.go\"}},\n\n\t{\"~\", func(name string) string {\n\t\tswitch runtime.GOOS {\n\t\tcase \"windows\":\n\t\t\tif name == \"USERPROFILE\" {\n\t\t\t\treturn \"/my/home\"\n\t\t\t}\n\t\tdefault:\n\t\t\tif name == \"HOME\" {\n\t\t\t\treturn \"/my/home\"\n\t\t\t}\n\t\t}\n\t\treturn \"\"\n\t}, []string{\"/my/home\"}},\n}\n\nfunc TestFields(t *testing.T) {\n\tos.Setenv(\"INTERP_GLOBAL\", \"value\")\n\tfor _, tc := range fieldsTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := Fields(tc.in, tc.env)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tc.want) {\n\t\t\t\tt.Fatalf(\"\\nwant: %q\\ngot:  %q\", tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "syntax/bench_test.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc BenchmarkParse(b *testing.B) {\n\tb.ReportAllocs()\n\tsrc := \"\" +\n\t\tstrings.Repeat(\"\\n\\n\\t\\t        \\n\", 10) +\n\t\t\"# \" + strings.Repeat(\"foo bar \", 10) + \"\\n\" +\n\t\tstrings.Repeat(\"longlit_\", 10) + \"\\n\" +\n\t\t\"'\" + strings.Repeat(\"foo bar \", 10) + \"'\\n\" +\n\t\t`\"` + strings.Repeat(\"foo bar \", 10) + `\"` + \"\\n\" +\n\t\tstrings.Repeat(\"aa bb cc dd; \", 6) +\n\t\t\"a() { (b); { c; }; }; $(d; `e`)\\n\" +\n\t\t\"foo=bar; a=b; c=d$foo${bar}e $simple ${complex:-default}\\n\" +\n\t\t\"if a; then while b; do for c in d e; do f; done; done; fi\\n\" +\n\t\t\"a | b && c || d | e && g || f\\n\" +\n\t\t\"foo >a <b <<<c 2>&1 <<EOF\\n\" +\n\t\tstrings.Repeat(\"somewhat long heredoc line\\n\", 10) +\n\t\t\"EOF\" +\n\t\t\"\"\n\tp := NewParser(KeepComments(true))\n\tin := strings.NewReader(src)\n\tfor b.Loop() {\n\t\tif _, err := p.Parse(in, \"\"); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tin.Reset(src)\n\t}\n}\n\nfunc BenchmarkPrint(b *testing.B) {\n\tb.ReportAllocs()\n\tprog := parsePath(b, canonicalPath)\n\tprinter := NewPrinter()\n\tfor b.Loop() {\n\t\tif err := printer.Print(io.Discard, prog); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "syntax/braces.go",
    "content": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar (\n\tlitLeftBrace  = &Lit{Value: \"{\"}\n\tlitComma      = &Lit{Value: \",\"}\n\tlitDots       = &Lit{Value: \"..\"}\n\tlitRightBrace = &Lit{Value: \"}\"}\n)\n\n// SplitBraces parses brace expansions within a word's literal parts.\n// If any valid brace expansions are found, they are replaced with BraceExp nodes,\n// and the function returns true.\n// Otherwise, the word is left untouched and the function returns false.\n//\n// For example, a literal word \"foo{bar,baz}\" will result in a word containing\n// the literal \"foo\", and a brace expansion with the elements \"bar\" and \"baz\".\n//\n// It does not return an error; malformed brace expansions are simply skipped.\n// For example, the literal word \"a{b\" is left unchanged.\nfunc SplitBraces(word *Word) bool {\n\tif !slices.ContainsFunc(word.Parts, func(part WordPart) bool {\n\t\tlit, ok := part.(*Lit)\n\t\treturn ok && strings.Contains(lit.Value, \"{\")\n\t}) {\n\t\t// In the common case where a word has no braces, skip any allocs.\n\t\treturn false\n\t}\n\ttop := &Word{}\n\tacc := top\n\tvar cur *BraceExp\n\topen := []*BraceExp{}\n\n\tpop := func() *BraceExp {\n\t\told := cur\n\t\topen = open[:len(open)-1]\n\t\tif len(open) == 0 {\n\t\t\tcur = nil\n\t\t\tacc = top\n\t\t} else {\n\t\t\tcur = open[len(open)-1]\n\t\t\tacc = cur.Elems[len(cur.Elems)-1]\n\t\t}\n\t\treturn old\n\t}\n\taddLit := func(lit *Lit) {\n\t\tacc.Parts = append(acc.Parts, lit)\n\t}\n\n\tfor _, wp := range word.Parts {\n\t\tlit, ok := wp.(*Lit)\n\t\tif !ok {\n\t\t\tacc.Parts = append(acc.Parts, wp)\n\t\t\tcontinue\n\t\t}\n\t\tlast := 0\n\t\tfor j := 0; j < len(lit.Value); j++ {\n\t\t\taddlitidx := func() {\n\t\t\t\tif last == j {\n\t\t\t\t\treturn // empty lit\n\t\t\t\t}\n\t\t\t\tl2 := *lit\n\t\t\t\tl2.Value = l2.Value[last:j]\n\t\t\t\taddLit(&l2)\n\t\t\t}\n\t\t\tswitch lit.Value[j] {\n\t\t\tcase '{':\n\t\t\t\taddlitidx()\n\t\t\t\tacc = &Word{}\n\t\t\t\tcur = &BraceExp{Elems: []*Word{acc}}\n\t\t\t\topen = append(open, cur)\n\t\t\tcase ',':\n\t\t\t\tif cur == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\taddlitidx()\n\t\t\t\tacc = &Word{}\n\t\t\t\tcur.Elems = append(cur.Elems, acc)\n\t\t\tcase '.':\n\t\t\t\tif cur == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif j+1 >= len(lit.Value) || lit.Value[j+1] != '.' {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\taddlitidx()\n\t\t\t\tcur.Sequence = true\n\t\t\t\tacc = &Word{}\n\t\t\t\tcur.Elems = append(cur.Elems, acc)\n\t\t\t\tj++\n\t\t\tcase '}':\n\t\t\t\tif cur == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\taddlitidx()\n\t\t\t\tbr := pop()\n\t\t\t\tif len(br.Elems) == 1 {\n\t\t\t\t\t// return {x} to a non-brace\n\t\t\t\t\taddLit(litLeftBrace)\n\t\t\t\t\tacc.Parts = append(acc.Parts, br.Elems[0].Parts...)\n\t\t\t\t\taddLit(litRightBrace)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif !br.Sequence {\n\t\t\t\t\tacc.Parts = append(acc.Parts, br)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tvar chars [2]bool\n\t\t\t\tbroken := false\n\t\t\t\tfor i, elem := range br.Elems[:2] {\n\t\t\t\t\tval := elem.Lit()\n\t\t\t\t\tif _, err := strconv.Atoi(val); err == nil {\n\t\t\t\t\t} else if len(val) == 1 && asciiLetter(val[0]) {\n\t\t\t\t\t\tchars[i] = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbroken = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif len(br.Elems) == 3 {\n\t\t\t\t\t// increment must be a number\n\t\t\t\t\tval := br.Elems[2].Lit()\n\t\t\t\t\tif _, err := strconv.Atoi(val); err != nil {\n\t\t\t\t\t\tbroken = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// are start and end both chars or\n\t\t\t\t// non-chars?\n\t\t\t\tif chars[0] != chars[1] {\n\t\t\t\t\tbroken = true\n\t\t\t\t}\n\t\t\t\tif !broken {\n\t\t\t\t\tacc.Parts = append(acc.Parts, br)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\t// return broken {x..y[..incr]} to a non-brace\n\t\t\t\taddLit(litLeftBrace)\n\t\t\t\tfor i, elem := range br.Elems {\n\t\t\t\t\tif i > 0 {\n\t\t\t\t\t\taddLit(litDots)\n\t\t\t\t\t}\n\t\t\t\t\tacc.Parts = append(acc.Parts, elem.Parts...)\n\t\t\t\t}\n\t\t\t\taddLit(litRightBrace)\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlast = j + 1\n\t\t}\n\t\tif last == 0 {\n\t\t\taddLit(lit)\n\t\t} else {\n\t\t\tleft := *lit\n\t\t\tleft.Value = left.Value[last:]\n\t\t\taddLit(&left)\n\t\t}\n\t}\n\t// open braces that were never closed fall back to non-braces\n\tfor acc != top {\n\t\tbr := pop()\n\t\taddLit(litLeftBrace)\n\t\tfor i, elem := range br.Elems {\n\t\t\tif i > 0 {\n\t\t\t\tif br.Sequence {\n\t\t\t\t\taddLit(litDots)\n\t\t\t\t} else {\n\t\t\t\t\taddLit(litComma)\n\t\t\t\t}\n\t\t\t}\n\t\t\tacc.Parts = append(acc.Parts, elem.Parts...)\n\t\t}\n\t}\n\t*word = *top\n\treturn true\n}\n"
  },
  {
    "path": "syntax/canonical.sh",
    "content": "#!/bin/bash\n\n# separate comment\n\n! foo bar >a &\n\nfoo() { bar; }\n\n{\n\tvar1=\"some long value\" # var1 comment\n\tvar2=short             # var2 comment\n}\n\nif foo; then bar; fi\n\nfor foo in a b c; do\n\tbar\ndone\n\ncase $foo in\na) A ;;\nb)\n\tB\n\t;;\nesac\n\nfoo | bar\nfoo &&\n\t$(bar) &&\n\t(more)\n\nfoo 2>&1\nfoo <<-EOF\n\tbar\nEOF\n\n$((3 + 4))\n"
  },
  {
    "path": "syntax/doc.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package syntax implements parsing and formatting of shell programs.\n// It supports POSIX Shell, Bash, and mksh.\npackage syntax\n"
  },
  {
    "path": "syntax/example_test.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\nfunc Example() {\n\tr := strings.NewReader(\"{ foo; bar; }\")\n\tf, err := syntax.NewParser().Parse(r, \"\")\n\tif err != nil {\n\t\treturn\n\t}\n\tsyntax.NewPrinter().Print(os.Stdout, f)\n\t// Output:\n\t// {\n\t//\tfoo\n\t//\tbar\n\t// }\n}\n\nfunc ExampleWord() {\n\tr := strings.NewReader(\"echo foo${bar}'baz'\")\n\tf, err := syntax.NewParser().Parse(r, \"\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tprinter := syntax.NewPrinter()\n\targs := f.Stmts[0].Cmd.(*syntax.CallExpr).Args\n\tfor i, word := range args {\n\t\tfmt.Printf(\"Word number %d:\\n\", i)\n\t\tfor _, part := range word.Parts {\n\t\t\tfmt.Printf(\"%-20T - \", part)\n\t\t\tprinter.Print(os.Stdout, part)\n\t\t\tfmt.Println()\n\t\t}\n\t\tfmt.Println()\n\t}\n\n\t// Output:\n\t// Word number 0:\n\t// *syntax.Lit          - echo\n\t//\n\t// Word number 1:\n\t// *syntax.Lit          - foo\n\t// *syntax.ParamExp     - ${bar}\n\t// *syntax.SglQuoted    - 'baz'\n}\n\nfunc ExampleCommand() {\n\tr := strings.NewReader(\"echo foo; if x; then y; fi; foo | bar\")\n\tf, err := syntax.NewParser().Parse(r, \"\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tprinter := syntax.NewPrinter()\n\tfor i, stmt := range f.Stmts {\n\t\tfmt.Printf(\"Cmd %d: %-20T - \", i, stmt.Cmd)\n\t\tprinter.Print(os.Stdout, stmt.Cmd)\n\t\tfmt.Println()\n\t}\n\n\t// Output:\n\t// Cmd 0: *syntax.CallExpr     - echo foo\n\t// Cmd 1: *syntax.IfClause     - if x; then y; fi\n\t// Cmd 2: *syntax.BinaryCmd    - foo | bar\n}\n\nfunc ExampleNewParser_options() {\n\tsrc := \"for ((i = 0; i < 5; i++)); do echo $i >f; done\"\n\n\t// LangBash is the default\n\tr := strings.NewReader(src)\n\tf, err := syntax.NewParser().Parse(r, \"\")\n\tfmt.Println(err)\n\n\t// Parser errors with LangPOSIX\n\tr = strings.NewReader(src)\n\t_, err = syntax.NewParser(syntax.Variant(syntax.LangPOSIX)).Parse(r, \"\")\n\tfmt.Println(err)\n\n\tsyntax.NewPrinter().Print(os.Stdout, f)\n\tsyntax.NewPrinter(syntax.SpaceRedirects(true)).Print(os.Stdout, f)\n\n\t// Output:\n\t// <nil>\n\t// 1:5: c-style fors are a bash/zsh feature; tried parsing as posix\n\t// for ((i = 0; i < 5; i++)); do echo $i >f; done\n\t// for ((i = 0; i < 5; i++)); do echo $i > f; done\n}\n\n// Keep in sync with FuzzQuote.\n\nfunc ExampleQuote() {\n\tfor _, s := range []string{\n\t\t\"foo\",\n\t\t\"bar $baz\",\n\t\t`\"won't\"`,\n\t\t\"~/home\",\n\t\t\"#1304\",\n\t\t\"name=value\",\n\t\t\"for\",\n\t\t\"glob-*\",\n\t\t\"invalid-\\xe2'\",\n\t\t\"nonprint-\\x0b\\x1b\",\n\t} {\n\t\t// We assume Bash syntax here.\n\t\t// For general shell syntax quoting, use syntax.LangPOSIX.\n\t\tquoted, err := syntax.Quote(s, syntax.LangBash)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"%q cannot be quoted: %v\\n\", s, err)\n\t\t} else {\n\t\t\tfmt.Printf(\"Quote(%17q): %s\\n\", s, quoted)\n\t\t}\n\t}\n\t// Output:\n\t// Quote(            \"foo\"): foo\n\t// Quote(       \"bar $baz\"): 'bar $baz'\n\t// Quote(      \"\\\"won't\\\"\"): \"\\\"won't\\\"\"\n\t// Quote(         \"~/home\"): '~/home'\n\t// Quote(          \"#1304\"): '#1304'\n\t// Quote(     \"name=value\"): 'name=value'\n\t// Quote(            \"for\"): 'for'\n\t// Quote(         \"glob-*\"): 'glob-*'\n\t// Quote(  \"invalid-\\xe2'\"): $'invalid-\\xe2\\''\n\t// Quote(\"nonprint-\\v\\x1b\"): $'nonprint-\\v\\x1b'\n}\n\nfunc ExampleWalk() {\n\tin := strings.NewReader(`echo $foo \"and $bar\"`)\n\tf, err := syntax.NewParser().Parse(in, \"\")\n\tif err != nil {\n\t\treturn\n\t}\n\tsyntax.Walk(f, func(node syntax.Node) bool {\n\t\tswitch node := node.(type) {\n\t\tcase *syntax.ParamExp:\n\t\t\tnode.Param.Value = strings.ToUpper(node.Param.Value)\n\t\t}\n\t\treturn true\n\t})\n\tsyntax.NewPrinter().Print(os.Stdout, f)\n\t// Output: echo $FOO \"and $BAR\"\n}\n\nfunc ExampleDebugPrint() {\n\tin := strings.NewReader(`echo 'foo'`)\n\tf, err := syntax.NewParser().Parse(in, \"\")\n\tif err != nil {\n\t\treturn\n\t}\n\tsyntax.DebugPrint(os.Stdout, f)\n\t// Output:\n\t// *syntax.File {\n\t// .  Name: \"\"\n\t// .  Stmts: []*syntax.Stmt (len = 1) {\n\t// .  .  0: *syntax.Stmt {\n\t// .  .  .  Comments: []syntax.Comment (len = 0) {}\n\t// .  .  .  Cmd: *syntax.CallExpr {\n\t// .  .  .  .  Assigns: []*syntax.Assign (len = 0) {}\n\t// .  .  .  .  Args: []*syntax.Word (len = 2) {\n\t// .  .  .  .  .  0: *syntax.Word {\n\t// .  .  .  .  .  .  Parts: []syntax.WordPart (len = 1) {\n\t// .  .  .  .  .  .  .  0: *syntax.Lit {\n\t// .  .  .  .  .  .  .  .  ValuePos: 1:1\n\t// .  .  .  .  .  .  .  .  ValueEnd: 1:5\n\t// .  .  .  .  .  .  .  .  Value: \"echo\"\n\t// .  .  .  .  .  .  .  }\n\t// .  .  .  .  .  .  }\n\t// .  .  .  .  .  }\n\t// .  .  .  .  .  1: *syntax.Word {\n\t// .  .  .  .  .  .  Parts: []syntax.WordPart (len = 1) {\n\t// .  .  .  .  .  .  .  0: *syntax.SglQuoted {\n\t// .  .  .  .  .  .  .  .  Left: 1:6\n\t// .  .  .  .  .  .  .  .  Right: 1:10\n\t// .  .  .  .  .  .  .  .  Dollar: false\n\t// .  .  .  .  .  .  .  .  Value: \"foo\"\n\t// .  .  .  .  .  .  .  }\n\t// .  .  .  .  .  .  }\n\t// .  .  .  .  .  }\n\t// .  .  .  .  }\n\t// .  .  .  }\n\t// .  .  .  Position: 1:1\n\t// .  .  .  Semicolon: 0:0\n\t// .  .  .  Negated: false\n\t// .  .  .  Background: false\n\t// .  .  .  Coprocess: false\n\t// .  .  .  Disown: false\n\t// .  .  .  Redirs: []*syntax.Redirect (len = 0) {}\n\t// .  .  }\n\t// .  }\n\t// .  Last: []syntax.Comment (len = 0) {}\n\t// }\n}\n"
  },
  {
    "path": "syntax/filetests_test.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc lit(s string) *Lit { return &Lit{Value: s} }\nfunc lits(strs ...string) []*Lit {\n\tl := make([]*Lit, 0, len(strs))\n\tfor _, s := range strs {\n\t\tl = append(l, lit(s))\n\t}\n\treturn l\n}\nfunc word(ps ...WordPart) *Word { return &Word{Parts: ps} }\nfunc litWord(s string) *Word    { return word(lit(s)) }\nfunc litWords(strs ...string) []*Word {\n\tl := make([]*Word, 0, len(strs))\n\tfor _, s := range strs {\n\t\tl = append(l, litWord(s))\n\t}\n\treturn l\n}\n\nfunc litAssigns(pairs ...string) []*Assign {\n\tl := make([]*Assign, len(pairs))\n\tfor i, pair := range pairs {\n\t\tname, val, ok := strings.Cut(pair, \"=\")\n\t\tif !ok {\n\t\t\tl[i] = &Assign{Naked: true, Name: lit(name)}\n\t\t} else if val == \"\" {\n\t\t\tl[i] = &Assign{Name: lit(name)}\n\t\t} else {\n\t\t\tl[i] = &Assign{Name: lit(name), Value: litWord(val)}\n\t\t}\n\t}\n\treturn l\n}\n\nfunc call(words ...*Word) *CallExpr    { return &CallExpr{Args: words} }\nfunc litCall(strs ...string) *CallExpr { return call(litWords(strs...)...) }\n\nfunc stmt(cmd Command) *Stmt { return &Stmt{Cmd: cmd} }\nfunc stmts(cmds ...Command) []*Stmt {\n\tl := make([]*Stmt, len(cmds))\n\tfor i, cmd := range cmds {\n\t\tl[i] = stmt(cmd)\n\t}\n\treturn l\n}\n\nfunc litStmt(strs ...string) *Stmt { return stmt(litCall(strs...)) }\nfunc litStmts(strs ...string) []*Stmt {\n\tl := make([]*Stmt, len(strs))\n\tfor i, s := range strs {\n\t\tl[i] = litStmt(s)\n\t}\n\treturn l\n}\n\nfunc sglQuoted(s string) *SglQuoted        { return &SglQuoted{Value: s} }\nfunc sglDQuoted(s string) *SglQuoted       { return &SglQuoted{Dollar: true, Value: s} }\nfunc dblQuoted(ps ...WordPart) *DblQuoted  { return &DblQuoted{Parts: ps} }\nfunc dblDQuoted(ps ...WordPart) *DblQuoted { return &DblQuoted{Dollar: true, Parts: ps} }\nfunc block(sts ...*Stmt) *Block            { return &Block{Stmts: sts} }\nfunc subshell(sts ...*Stmt) *Subshell      { return &Subshell{Stmts: sts} }\nfunc arithmExp(e ArithmExpr) *ArithmExp    { return &ArithmExp{X: e} }\nfunc arithmExpBr(e ArithmExpr) *ArithmExp  { return &ArithmExp{Bracket: true, X: e} }\nfunc arithmCmd(e ArithmExpr) *ArithmCmd    { return &ArithmCmd{X: e} }\nfunc parenArit(e ArithmExpr) *ParenArithm  { return &ParenArithm{X: e} }\nfunc parenTest(e TestExpr) *ParenTest      { return &ParenTest{X: e} }\n\nfunc cmdSubst(sts ...*Stmt) *CmdSubst { return &CmdSubst{Stmts: sts} }\nfunc litParamExp(s string) *ParamExp {\n\treturn &ParamExp{Short: true, Param: lit(s)}\n}\n\nfunc letClause(exps ...ArithmExpr) *LetClause {\n\treturn &LetClause{Exprs: exps}\n}\n\nfunc arrValues(words ...*Word) *ArrayExpr {\n\tae := &ArrayExpr{}\n\tfor _, w := range words {\n\t\tae.Elems = append(ae.Elems, &ArrayElem{Value: w})\n\t}\n\treturn ae\n}\n\nfunc fullProg(v any) *File {\n\tf := &File{}\n\tswitch v := v.(type) {\n\tcase *File:\n\t\treturn v\n\tcase []*Stmt:\n\t\tf.Stmts = v\n\t\treturn f\n\tcase *Stmt:\n\t\tf.Stmts = append(f.Stmts, v)\n\t\treturn f\n\tcase []Command:\n\t\tfor _, cmd := range v {\n\t\t\tf.Stmts = append(f.Stmts, stmt(cmd))\n\t\t}\n\t\treturn f\n\tcase *Word:\n\t\treturn fullProg(call(v))\n\tcase WordPart:\n\t\treturn fullProg(word(v))\n\tcase Command:\n\t\treturn fullProg(stmt(v))\n\tcase nil:\n\tdefault:\n\t\tpanic(reflect.TypeOf(v))\n\t}\n\treturn nil\n}\n\ntype fileTestCase struct {\n\tinputs []string // input sources; the first is the canonical formatting\n\n\t// Each language in [langResolvedVariants] has an entry:\n\t// - nil:    nothing to test\n\t// - *File:  parse as the given syntax tree\n\t// - string: parse error with the given string, substituting LANG\n\tbyLangIndex [langResolvedVariantsCount]any\n\n\t// The real shells where testing the input succeeds or fails in the opposite way.\n\tflipConfirmSet LangVariant\n}\n\nfunc flipConfirm2(langSet LangVariant) func(*fileTestCase) {\n\treturn func(c *fileTestCase) { c.flipConfirmSet = langSet }\n}\n\nfunc (c *fileTestCase) setForLangs(val any, langSets ...LangVariant) {\n\t// The parameter is a slice to allow omitting the argument.\n\tswitch len(langSets) {\n\tcase 0:\n\t\tfor i := range c.byLangIndex {\n\t\t\tc.byLangIndex[i] = val\n\t\t}\n\t\treturn\n\tcase 1:\n\t\tfor lang := range langSets[0].bits() {\n\t\t\tc.byLangIndex[lang.index()] = val\n\t\t}\n\tdefault:\n\t\tpanic(\"use a LangVariant bitset\")\n\t}\n}\n\nfunc fileTest(in []string, opts ...func(*fileTestCase)) fileTestCase {\n\tc := fileTestCase{inputs: in}\n\tfor _, o := range opts {\n\t\to(&c)\n\t}\n\treturn c\n}\n\nfunc langSkip(langSets ...LangVariant) func(*fileTestCase) {\n\treturn func(c *fileTestCase) { c.setForLangs(nil, langSets...) }\n}\n\nfunc langFile(wantNode any, langSets ...LangVariant) func(*fileTestCase) {\n\treturn func(c *fileTestCase) {\n\t\tc.setForLangs(fullProg(wantNode), langSets...)\n\t}\n}\n\nfunc langErr2(wantErr string, langSets ...LangVariant) func(*fileTestCase) {\n\treturn func(c *fileTestCase) { c.setForLangs(wantErr, langSets...) }\n}\n\nvar fileTests = []fileTestCase{\n\tfileTest(\n\t\t[]string{\"\", \" \", \"\\t\", \"\\n \\n\", \"\\r \\r\\n\"},\n\t\tlangFile(&File{}),\n\t),\n\tfileTest(\n\t\t[]string{\"\", \"# foo\", \"# foo ( bar\", \"# foo'bar\"},\n\t\tlangFile(&File{}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo\", \"foo \", \" foo\", \"foo # bar\"},\n\t\tlangFile(litWord(\"foo\")),\n\t),\n\tfileTest(\n\t\t[]string{`\\`},\n\t\tlangFile(litWord(`\\`)),\n\t),\n\tfileTest(\n\t\t[]string{`foo\\`, \"f\\\\\\noo\\\\\"},\n\t\tlangFile(litWord(`foo\\`)),\n\t),\n\tfileTest(\n\t\t[]string{`foo\\a`, \"f\\\\\\noo\\\\a\"},\n\t\tlangFile(litWord(`foo\\a`)),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo\\nbar\",\n\t\t\t\"foo; bar;\",\n\t\t\t\"foo;bar;\",\n\t\t\t\"\\nfoo\\nbar\\n\",\n\t\t\t\"foo\\r\\nbar\\r\\n\",\n\t\t},\n\t\tlangFile(litStmts(\"foo\", \"bar\")),\n\t),\n\tfileTest(\n\t\t[]string{\"foo a b\", \" foo  a  b \", \"foo \\\\\\n a b\", \"foo \\\\\\r\\n a b\"},\n\t\tlangFile(litCall(\"foo\", \"a\", \"b\")),\n\t),\n\tfileTest(\n\t\t[]string{\"foobar\", \"foo\\\\\\nbar\", \"foo\\\\\\nba\\\\\\nr\"},\n\t\tlangFile(litWord(\"foobar\")),\n\t),\n\tfileTest(\n\t\t[]string{\"foo\", \"foo \\\\\\n\", \"foo \\\\\\r\\n\"},\n\t\tlangFile(litWord(\"foo\")),\n\t),\n\tfileTest(\n\t\t[]string{\"foo'bar'\"},\n\t\tlangFile(word(lit(\"foo\"), sglQuoted(\"bar\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"(foo)\", \"(foo;)\", \"(\\nfoo\\n)\"},\n\t\tlangFile(subshell(litStmt(\"foo\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"(\\n\\tfoo\\n\\tbar\\n)\", \"(foo; bar)\"},\n\t\tlangFile(subshell(litStmt(\"foo\"), litStmt(\"bar\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"{ foo; }\", \"{\\nfoo\\n}\"},\n\t\tlangFile(block(litStmt(\"foo\"))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"{ if a; then b; fi; }\",\n\t\t\t\"{ if a; then b; fi }\",\n\t\t},\n\t\tlangFile(block(stmt(&IfClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tThen: litStmts(\"b\"),\n\t\t}))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"if a; then b; fi\",\n\t\t\t\"if a\\nthen\\nb\\nfi\",\n\t\t\t\"if a;\\nthen\\nb\\nfi\",\n\t\t\t\"if a \\nthen\\nb\\nfi\",\n\t\t\t\"if\\x00 a; th\\x00en b; \\x00fi\",\n\t\t},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tThen: litStmts(\"b\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"if a; then b; else c; fi\",\n\t\t\t\"if a\\nthen b\\nelse\\nc\\nfi\",\n\t\t},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tThen: litStmts(\"b\"),\n\t\t\tElse: &IfClause{\n\t\t\t\tThen: litStmts(\"c\"),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"if a; then a; elif b; then b; else c; fi\",\n\t\t},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tThen: litStmts(\"a\"),\n\t\t\tElse: &IfClause{\n\t\t\t\tCond: litStmts(\"b\"),\n\t\t\t\tThen: litStmts(\"b\"),\n\t\t\t\tElse: &IfClause{\n\t\t\t\t\tThen: litStmts(\"c\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"if a; then a; elif b; then b; elif c; then c; else d; fi\",\n\t\t\t\"if a\\nthen a\\nelif b\\nthen b\\nelif c\\nthen c\\nelse\\nd\\nfi\",\n\t\t},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tThen: litStmts(\"a\"),\n\t\t\tElse: &IfClause{\n\t\t\t\tCond: litStmts(\"b\"),\n\t\t\t\tThen: litStmts(\"b\"),\n\t\t\t\tElse: &IfClause{\n\t\t\t\t\tCond: litStmts(\"c\"),\n\t\t\t\t\tThen: litStmts(\"c\"),\n\t\t\t\t\tElse: &IfClause{\n\t\t\t\t\t\tThen: litStmts(\"d\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"if\\n\\ta1\\n\\ta2 foo\\n\\ta3 bar\\nthen b; fi\",\n\t\t\t\"if a1; a2 foo; a3 bar; then b; fi\",\n\t\t},\n\t\tlangFile(&IfClause{\n\t\t\tCond: []*Stmt{\n\t\t\t\tlitStmt(\"a1\"),\n\t\t\t\tlitStmt(\"a2\", \"foo\"),\n\t\t\t\tlitStmt(\"a3\", \"bar\"),\n\t\t\t},\n\n\t\t\tThen: litStmts(\"b\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`((a == 2))`},\n\t\tlangFile(arithmCmd(&BinaryArithm{\n\t\t\tOp: Eql,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"2\"),\n\t\t}), LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(subshell(stmt(subshell(litStmt(\"a\", \"==\", \"2\")))), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"if (($# > 2)); then b; fi\"},\n\t\tlangFile(&IfClause{\n\t\t\tCond: stmts(arithmCmd(&BinaryArithm{\n\t\t\t\tOp: Gtr,\n\t\t\t\tX:  word(litParamExp(\"#\")),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t})),\n\t\t\tThen: litStmts(\"b\"),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"(($(date -u) > DATE))\",\n\t\t\t\"((`date -u` > DATE))\",\n\t\t},\n\t\tlangFile(arithmCmd(&BinaryArithm{\n\t\t\tOp: Gtr,\n\t\t\tX:  word(cmdSubst(litStmt(\"date\", \"-u\"))),\n\t\t\tY:  litWord(\"DATE\"),\n\t\t}), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\": $((0x$foo == 10))\"},\n\t\tlangFile(call(\n\t\t\tlitWord(\":\"),\n\t\t\tword(arithmExp(&BinaryArithm{\n\t\t\t\tOp: Eql,\n\t\t\t\tX:  word(lit(\"0x\"), litParamExp(\"foo\")),\n\t\t\t\tY:  litWord(\"10\"),\n\t\t\t})),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"((# 1 + 2))\", \"(( # 1 + 2 ))\"},\n\t\tlangFile(&ArithmCmd{\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: Add,\n\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t},\n\t\t\tUnsigned: true,\n\t\t}, LangMirBSDKorn),\n\t\tlangErr2(\"1:1: unsigned expressions are a mksh feature; tried parsing as LANG\", LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"$((# 1 + 2))\", \"$(( # 1 + 2 ))\"},\n\t\tlangFile(&ArithmExp{\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: Add,\n\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t},\n\t\t\tUnsigned: true,\n\t\t}, LangMirBSDKorn),\n\t\tlangErr2(\"1:1: unsigned expressions are a mksh feature; tried parsing as LANG\", LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"((3#20))\"},\n\t\tlangFile(arithmCmd(litWord(\"3#20\")), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"((1.2 > 0.3))\"},\n\t\tlangFile(arithmCmd(&BinaryArithm{\n\t\t\tOp: Gtr,\n\t\t\tX:  litWord(\"1.2\"),\n\t\t\tY:  litWord(\"0.3\"),\n\t\t}), LangZsh),\n\t\tlangErr2(\"1:4: floating point arithmetic is a zsh feature; tried parsing as LANG\", LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"while a; do b; done\",\n\t\t\t\"wh\\\\\\nile a; do b; done\",\n\t\t\t\"wh\\\\\\r\\nile a; do b; done\",\n\t\t\t\"while a\\ndo\\nb\\ndone\",\n\t\t\t\"while a;\\ndo\\nb\\ndone\",\n\t\t},\n\t\tlangFile(&WhileClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tDo:   litStmts(\"b\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"while { a; }; do b; done\", \"while { a; } do b; done\"},\n\t\tlangFile(&WhileClause{\n\t\t\tCond: stmts(block(litStmt(\"a\"))),\n\t\t\tDo:   litStmts(\"b\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"while (a); do b; done\", \"while (a) do b; done\"},\n\t\tlangFile(&WhileClause{\n\t\t\tCond: stmts(subshell(litStmt(\"a\"))),\n\t\t\tDo:   litStmts(\"b\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"while ((1 > 2)); do b; done\"},\n\t\tlangFile(&WhileClause{\n\t\t\tCond: stmts(arithmCmd(&BinaryArithm{\n\t\t\t\tOp: Gtr,\n\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t})),\n\t\t\tDo: litStmts(\"b\"),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"until a; do b; done\", \"until a\\ndo\\nb\\ndone\"},\n\t\tlangFile(&WhileClause{\n\t\t\tUntil: true,\n\t\t\tCond:  litStmts(\"a\"),\n\t\t\tDo:    litStmts(\"b\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"for i; do foo; done\",\n\t\t\t\"for i do foo; done\",\n\t\t\t\"for i\\ndo foo\\ndone\",\n\t\t\t\"for i;\\ndo foo\\ndone\",\n\t\t\t\"for i in; do foo; done\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tLoop: &WordIter{Name: lit(\"i\")},\n\t\t\tDo:   litStmts(\"foo\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"for i in 1 2 3; do echo $i; done\",\n\t\t\t\"for i in 1 2 3\\ndo echo $i\\ndone\",\n\t\t\t\"for i in 1 2 3;\\ndo echo $i\\ndone\",\n\t\t\t\"for i in 1 2 3 #foo\\ndo echo $i\\ndone\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tLoop: &WordIter{\n\t\t\t\tName:  lit(\"i\"),\n\t\t\t\tItems: litWords(\"1\", \"2\", \"3\"),\n\t\t\t},\n\t\t\tDo: stmts(call(\n\t\t\t\tlitWord(\"echo\"),\n\t\t\t\tword(litParamExp(\"i\")),\n\t\t\t)),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"for i in \\\\\\n\\t1 2 3; do #foo\\n\\techo $i\\ndone\",\n\t\t\t\"for i #foo\\n\\tin 1 2 3; do\\n\\techo $i\\ndone\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tLoop: &WordIter{\n\t\t\t\tName:  lit(\"i\"),\n\t\t\t\tItems: litWords(\"1\", \"2\", \"3\"),\n\t\t\t},\n\t\t\tDo: stmts(call(\n\t\t\t\tlitWord(\"echo\"),\n\t\t\t\tword(litParamExp(\"i\")),\n\t\t\t)),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"for i; do foo; done\",\n\t\t\t\"for i; { foo; }\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tLoop: &WordIter{Name: lit(\"i\")},\n\t\t\tDo:   litStmts(\"foo\"),\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"for i in 1 2 3; do echo $i; done\",\n\t\t\t\"for i in 1 2 3; { echo $i; }\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tLoop: &WordIter{\n\t\t\t\tName:  lit(\"i\"),\n\t\t\t\tItems: litWords(\"1\", \"2\", \"3\"),\n\t\t\t},\n\t\t\tDo: stmts(call(\n\t\t\t\tlitWord(\"echo\"),\n\t\t\t\tword(litParamExp(\"i\")),\n\t\t\t)),\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"for ((i = 0; i < 10; i++)); do echo $i; done\",\n\t\t\t\"for ((i=0;i<10;i++)) do echo $i; done\",\n\t\t\t\"for (( i = 0 ; i < 10 ; i++ ))\\ndo echo $i\\ndone\",\n\t\t\t\"for (( i = 0 ; i < 10 ; i++ ));\\ndo echo $i\\ndone\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tLoop: &CStyleLoop{\n\t\t\t\tInit: &BinaryArithm{\n\t\t\t\t\tOp: Assgn,\n\t\t\t\t\tX:  litWord(\"i\"),\n\t\t\t\t\tY:  litWord(\"0\"),\n\t\t\t\t},\n\t\t\t\tCond: &BinaryArithm{\n\t\t\t\t\tOp: Lss,\n\t\t\t\t\tX:  litWord(\"i\"),\n\t\t\t\t\tY:  litWord(\"10\"),\n\t\t\t\t},\n\t\t\t\tPost: &UnaryArithm{\n\t\t\t\t\tOp:   Inc,\n\t\t\t\t\tPost: true,\n\t\t\t\t\tX:    litWord(\"i\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tDo: stmts(call(\n\t\t\t\tlitWord(\"echo\"),\n\t\t\t\tword(litParamExp(\"i\")),\n\t\t\t)),\n\t\t}, LangBash|LangZsh),\n\t\tlangErr2(\"1:5: c-style fors are a bash/zsh feature; tried parsing as LANG\", LangPOSIX|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"for (( ; ; )); do foo; done\",\n\t\t\t\"for ((;;)); do foo; done\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tLoop: &CStyleLoop{},\n\t\t\tDo:   litStmts(\"foo\"),\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"for ((i = 0; ; )); do foo; done\",\n\t\t\t\"for ((i = 0;;)); do foo; done\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tLoop: &CStyleLoop{\n\t\t\t\tInit: &BinaryArithm{\n\t\t\t\t\tOp: Assgn,\n\t\t\t\t\tX:  litWord(\"i\"),\n\t\t\t\t\tY:  litWord(\"0\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tDo: litStmts(\"foo\"),\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"select i; do foo; done\",\n\t\t\t\"select i in; do foo; done\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tSelect: true,\n\t\t\tLoop:   &WordIter{Name: lit(\"i\")},\n\t\t\tDo:     litStmts(\"foo\"),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"select i in 1 2 3; do echo $i; done\",\n\t\t\t\"select i in 1 2 3\\ndo echo $i\\ndone\",\n\t\t\t\"select i in 1 2 3 #foo\\ndo echo $i\\ndone\",\n\t\t},\n\t\tlangFile(&ForClause{\n\t\t\tSelect: true,\n\t\t\tLoop: &WordIter{\n\t\t\t\tName:  lit(\"i\"),\n\t\t\t\tItems: litWords(\"1\", \"2\", \"3\"),\n\t\t\t},\n\t\t\tDo: stmts(call(\n\t\t\t\tlitWord(\"echo\"),\n\t\t\t\tword(litParamExp(\"i\")),\n\t\t\t)),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"select foo bar\"},\n\t\tlangFile(litStmt(\"select\", \"foo\", \"bar\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`' ' \"foo bar\"`},\n\t\tlangFile(call(\n\t\t\tword(sglQuoted(\" \")),\n\t\t\tword(dblQuoted(lit(\"foo bar\"))),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`\"foo \\\" bar\"`},\n\t\tlangFile(word(dblQuoted(lit(`foo \\\" bar`)))),\n\t),\n\tfileTest(\n\t\t[]string{\"\\\">foo\\\" \\\"\\nbar\\\"\"},\n\t\tlangFile(call(\n\t\t\tword(dblQuoted(lit(\">foo\"))),\n\t\t\tword(dblQuoted(lit(\"\\nbar\"))),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`foo \\\" bar`},\n\t\tlangFile(litCall(`foo`, `\\\"`, `bar`)),\n\t),\n\tfileTest(\n\t\t[]string{`'\"'`},\n\t\tlangFile(sglQuoted(`\"`)),\n\t),\n\tfileTest(\n\t\t[]string{\"'`'\"},\n\t\tlangFile(sglQuoted(\"`\")),\n\t),\n\tfileTest(\n\t\t[]string{`\"'\"`},\n\t\tlangFile(dblQuoted(lit(\"'\"))),\n\t),\n\tfileTest(\n\t\t[]string{`\"\"`},\n\t\tlangFile(dblQuoted()),\n\t),\n\tfileTest(\n\t\t[]string{\"=a s{s s=s\"},\n\t\tlangFile(litCall(\"=a\", \"s{s\", \"s=s\")),\n\t\tlangSkip(LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"foo && bar\", \"foo&&bar\", \"foo &&\\nbar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: AndStmt,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY:  litStmt(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo &&\\n\\tbar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: AndStmt,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY:  litStmt(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo || bar\", \"foo||bar\", \"foo ||\\nbar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: OrStmt,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY:  litStmt(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"if a; then b; fi || while a; do b; done\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: OrStmt,\n\t\t\tX: stmt(&IfClause{\n\t\t\t\tCond: litStmts(\"a\"),\n\t\t\t\tThen: litStmts(\"b\"),\n\t\t\t}),\n\t\t\tY: stmt(&WhileClause{\n\t\t\t\tCond: litStmts(\"a\"),\n\t\t\t\tDo:   litStmts(\"b\"),\n\t\t\t}),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo && bar1 || bar2\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: OrStmt,\n\t\t\tX: stmt(&BinaryCmd{\n\t\t\t\tOp: AndStmt,\n\t\t\t\tX:  litStmt(\"foo\"),\n\t\t\t\tY:  litStmt(\"bar1\"),\n\t\t\t}),\n\t\t\tY: litStmt(\"bar2\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a || b || c || d\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: OrStmt,\n\t\t\tX: stmt(&BinaryCmd{\n\t\t\t\tOp: OrStmt,\n\t\t\t\tX: stmt(&BinaryCmd{\n\t\t\t\t\tOp: OrStmt,\n\t\t\t\t\tX:  litStmt(\"a\"),\n\t\t\t\t\tY:  litStmt(\"b\"),\n\t\t\t\t}),\n\t\t\t\tY: litStmt(\"c\"),\n\t\t\t}),\n\t\t\tY: litStmt(\"d\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo | bar\", \"foo|bar\", \"foo |\\n#etc\\nbar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: Pipe,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY:  litStmt(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo | bar | extra\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: Pipe,\n\t\t\tX: stmt(&BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  litStmt(\"foo\"),\n\t\t\t\tY:  litStmt(\"bar\"),\n\t\t\t}),\n\t\t\tY: litStmt(\"extra\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo | a=b bar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: Pipe,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY: stmt(&CallExpr{\n\t\t\t\tAssigns: litAssigns(\"a=b\"),\n\t\t\t\tArgs:    litWords(\"bar\"),\n\t\t\t}),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo |&\"},\n\t\tlangFile(&Stmt{Cmd: litCall(\"foo\"), Coprocess: true}, LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"foo \\\\\\n\\t|&\"},\n\t\tlangFile(&Stmt{Cmd: litCall(\"foo\"), Coprocess: true}, LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"foo |& bar\", \"foo|&bar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: PipeAll,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY:  litStmt(\"bar\"),\n\t\t}, LangBash|LangZsh),\n\t\tlangFile([]*Stmt{\n\t\t\t{Cmd: litCall(\"foo\"), Coprocess: true},\n\t\t\tlitStmt(\"bar\"),\n\t\t}, LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"foo &|\", \"foo &!\"},\n\t\tlangFile(&Stmt{Cmd: litCall(\"foo\"), Disown: true}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"foo &|\\nbar\"},\n\t\tlangFile([]*Stmt{\n\t\t\t{Cmd: litCall(\"foo\"), Disown: true},\n\t\t\tlitStmt(\"bar\"),\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo() {\\n\\ta\\n\\tb\\n}\",\n\t\t\t\"foo() { a; b; }\",\n\t\t\t\"foo ( ) {\\na\\nb\\n}\",\n\t\t\t\"foo()\\n{\\na\\nb\\n}\",\n\t\t},\n\t\tlangFile(&FuncDecl{\n\t\t\tParens: true,\n\t\t\tName:   lit(\"foo\"),\n\t\t\tBody:   stmt(block(litStmt(\"a\"), litStmt(\"b\"))),\n\t\t}),\n\t\tlangSkip(LangZsh), // fails on foo ( )\n\t),\n\tfileTest(\n\t\t[]string{\"foo() { a; }\\nbar\", \"foo() {\\na\\n}; bar\"},\n\t\tlangFile([]Command{\n\t\t\t&FuncDecl{\n\t\t\t\tParens: true,\n\t\t\t\tName:   lit(\"foo\"),\n\t\t\t\tBody:   stmt(block(litStmt(\"a\"))),\n\t\t\t},\n\t\t\tlitCall(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foO_123() { a; }\"},\n\t\tlangFile(&FuncDecl{\n\t\t\tParens: true,\n\t\t\tName:   lit(\"foO_123\"),\n\t\t\tBody:   stmt(block(litStmt(\"a\"))),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"-foo_.,+-bar() { a; }\"},\n\t\tlangFile(&FuncDecl{\n\t\t\tParens: true,\n\t\t\tName:   lit(\"-foo_.,+-bar\"),\n\t\t\tBody:   stmt(block(litStmt(\"a\"))),\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"function foo() {\\n\\ta\\n\\tb\\n}\",\n\t\t\t\"function foo() { a; b; }\",\n\t\t},\n\t\tlangFile(&FuncDecl{\n\t\t\tRsrvWord: true,\n\t\t\tParens:   true,\n\t\t\tName:     lit(\"foo\"),\n\t\t\tBody:     stmt(block(litStmt(\"a\"), litStmt(\"b\"))),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:13: the `function` builtin is a bash feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"function foo {\\n\\ta\\n\\tb\\n}\",\n\t\t\t\"function foo { a; b; }\",\n\t\t},\n\t\tlangFile(&FuncDecl{\n\t\t\tRsrvWord: true,\n\t\t\tName:     lit(\"foo\"),\n\t\t\tBody:     stmt(block(litStmt(\"a\"), litStmt(\"b\"))),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"function foo() (a)\"},\n\t\tlangFile(&FuncDecl{\n\t\t\tRsrvWord: true,\n\t\t\tParens:   true,\n\t\t\tName:     lit(\"foo\"),\n\t\t\tBody:     stmt(subshell(litStmt(\"a\"))),\n\t\t}, LangBash),\n\t\tlangErr2(\"1:13: the `function` builtin is a bash feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"function f1 f2 f3() {\\n\\ta\\n}\"},\n\t\tlangFile(&FuncDecl{\n\t\t\tRsrvWord: true,\n\t\t\tParens:   true,\n\t\t\tNames:    lits(\"f1\", \"f2\", \"f3\"),\n\t\t\tBody:     stmt(block(litStmt(\"a\"))),\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"function f1 f2 f3() {\\n\\ta\\n}\"},\n\t\tlangFile(&FuncDecl{\n\t\t\tRsrvWord: true,\n\t\t\tParens:   true,\n\t\t\tNames:    lits(\"f1\", \"f2\", \"f3\"),\n\t\t\tBody:     stmt(block(litStmt(\"a\"))),\n\t\t}, LangZsh),\n\t\tlangErr2(\"1:1: multi-name functions are a zsh feature; tried parsing as LANG\", LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"function {\\n\\ta\\n}\"},\n\t\tlangFile(&FuncDecl{\n\t\t\tRsrvWord: true,\n\t\t\tBody:     stmt(block(litStmt(\"a\"))),\n\t\t}, LangZsh),\n\t\tlangErr2(\"1:1: anonymous functions are a zsh feature; tried parsing as LANG\", LangBash|LangMirBSDKorn),\n\t),\n\t// Note that zsh also supports `f1 f2 f3 () { body; }`,\n\t// but it seems rare and hard to implement well,\n\t// so leave it out for now.\n\tfileTest(\n\t\t[]string{\"() {\\n\\ta\\n}\"},\n\t\tlangFile(&FuncDecl{\n\t\t\tParens: true,\n\t\t\tBody:   stmt(block(litStmt(\"a\"))),\n\t\t}, LangZsh),\n\t\tlangErr2(\"1:1: anonymous functions are a zsh feature; tried parsing as LANG\", LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"a=b foo=$bar foo=start$bar\"},\n\t\tlangFile(&CallExpr{\n\t\t\tAssigns: []*Assign{\n\t\t\t\t{Name: lit(\"a\"), Value: litWord(\"b\")},\n\t\t\t\t{Name: lit(\"foo\"), Value: word(litParamExp(\"bar\"))},\n\t\t\t\t{Name: lit(\"foo\"), Value: word(\n\t\t\t\t\tlit(\"start\"),\n\t\t\t\t\tlitParamExp(\"bar\"),\n\t\t\t\t)},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a=\\\"\\nbar\\\"\"},\n\t\tlangFile(&CallExpr{\n\t\t\tAssigns: []*Assign{{\n\t\t\t\tName:  lit(\"a\"),\n\t\t\t\tValue: word(dblQuoted(lit(\"\\nbar\"))),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"A_3a= foo\"},\n\t\tlangFile(&CallExpr{\n\t\t\tAssigns: litAssigns(\"A_3a=\"),\n\t\t\tArgs:    litWords(\"foo\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a=b=c\"},\n\t\tlangFile(&CallExpr{\n\t\t\tAssigns: litAssigns(\"a=b=c\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"à=b foo\"},\n\t\tlangFile(litStmt(\"à=b\", \"foo\")),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo >a >>b <c\",\n\t\t\t\"foo > a >> b < c\",\n\t\t\t\">a >>b <c foo\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"a\")},\n\t\t\t\t{Op: AppOut, Word: litWord(\"b\")},\n\t\t\t\t{Op: RdrIn, Word: litWord(\"c\")},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo bar >a\",\n\t\t\t\"foo >a bar\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\", \"bar\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"a\")},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`>a >\\b`},\n\t\tlangFile(&Stmt{\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"a\")},\n\t\t\t\t{Op: RdrOut, Word: litWord(`\\b`)},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\">a\\n>b\", \">a; >b\"},\n\t\tlangFile([]*Stmt{\n\t\t\t{Redirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"a\")},\n\t\t\t}},\n\t\t\t{Redirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"b\")},\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo1\\nfoo2 >r2\", \"foo1; >r2 foo2\"},\n\t\tlangFile([]*Stmt{\n\t\t\tlitStmt(\"foo1\"),\n\t\t\t{\n\t\t\t\tCmd: litCall(\"foo2\"),\n\t\t\t\tRedirs: []*Redirect{\n\t\t\t\t\t{Op: RdrOut, Word: litWord(\"r2\")},\n\t\t\t\t},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo >bar$(etc)\", \"foo >b\\\\\\nar`etc`\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: word(\n\t\t\t\t\tlit(\"bar\"),\n\t\t\t\t\tcmdSubst(litStmt(\"etc\")),\n\t\t\t\t)},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"a=b c=d foo >x <y\",\n\t\t\t\"a=b c=d >x <y foo\",\n\t\t\t\">x a=b c=d <y foo\",\n\t\t\t\">x <y a=b c=d foo\",\n\t\t\t\"a=b >x c=d foo <y\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: &CallExpr{\n\t\t\t\tAssigns: litAssigns(\"a=b\", \"c=d\"),\n\t\t\t\tArgs:    litWords(\"foo\"),\n\t\t\t},\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"x\")},\n\t\t\t\t{Op: RdrIn, Word: litWord(\"y\")},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo <<EOF\\nbar\\nEOF\",\n\t\t\t\"foo <<EOF \\nbar\\nEOF\",\n\t\t\t\"foo <<EOF\\t\\nbar\\nEOF\",\n\t\t\t\"foo <<EOF\\r\\nbar\\r\\nEOF\\r\\n\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<EOF\\n\\nbar\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"\\nbar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<EOF\\nbar\\n\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<EOF\\n1\\n2\\n3\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"1\\n2\\n3\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a <<EOF\\nfoo$bar\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"a\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: word(\n\t\t\t\t\tlit(\"foo\"),\n\t\t\t\t\tlitParamExp(\"bar\"),\n\t\t\t\t\tlit(\"\\n\"),\n\t\t\t\t),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a <<EOF\\n\\\"$bar\\\"\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"a\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: word(\n\t\t\t\t\tlit(`\"`),\n\t\t\t\t\tlitParamExp(\"bar\"),\n\t\t\t\t\tlit(\"\\\"\\n\"),\n\t\t\t\t),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a <<EOF\\n$''$bar\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"a\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: word(\n\t\t\t\t\tlit(\"$\"),\n\t\t\t\t\tlit(\"''\"),\n\t\t\t\t\tlitParamExp(\"bar\"),\n\t\t\t\t\tlit(\"\\n\"),\n\t\t\t\t),\n\t\t\t}},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"a <<EOF\\n$(b)\\nc\\nEOF\",\n\t\t\t\"a <<EOF\\n`b`\\nc\\nEOF\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"a\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: word(\n\t\t\t\t\tcmdSubst(litStmt(\"b\")),\n\t\t\t\t\tlit(\"\\nc\\n\"),\n\t\t\t\t),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"a <<EOF\\nfoo$(bar)baz\\nEOF\",\n\t\t\t\"a <<EOF\\nfoo`bar`baz\\nEOF\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"a\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: word(\n\t\t\t\t\tlit(\"foo\"),\n\t\t\t\t\tcmdSubst(litStmt(\"bar\")),\n\t\t\t\t\tlit(\"baz\\n\"),\n\t\t\t\t),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a <<EOF\\n\\\\${\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"a\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"\\\\${\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"{\\n\\tfoo <<EOF\\nbar\\nEOF\\n}\",\n\t\t\t\"{ foo <<EOF\\nbar\\nEOF\\n}\",\n\t\t},\n\t\tlangFile(block(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$(\\n\\tfoo <<EOF\\nbar\\nEOF\\n)\",\n\t\t\t\"$(foo <<EOF\\nbar\\nEOF\\n)\",\n\t\t\t\"`\\nfoo <<EOF\\nbar\\nEOF\\n`\",\n\t\t\t\"`foo <<EOF\\nbar\\nEOF`\",\n\t\t},\n\t\tlangFile(cmdSubst(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo <<EOF\\nbar\\nEOF$(oops)\\nEOF\",\n\t\t\t\"foo <<EOF\\nbar\\nEOF`oops`\\nEOF\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: word(\n\t\t\t\t\tlit(\"bar\\nEOF\"),\n\t\t\t\t\tcmdSubst(litStmt(\"oops\")),\n\t\t\t\t\tlit(\"\\n\"),\n\t\t\t\t),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo <<EOF\\nbar\\nNOTEOF$(oops)\\nEOF\",\n\t\t\t\"foo <<EOF\\nbar\\nNOTEOF`oops`\\nEOF\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: word(\n\t\t\t\t\tlit(\"bar\\nNOTEOF\"),\n\t\t\t\t\tcmdSubst(litStmt(\"oops\")),\n\t\t\t\t\tlit(\"\\n\"),\n\t\t\t\t),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$(\\n\\tfoo <<'EOF'\\nbar\\nEOF\\n)\",\n\t\t\t\"$(foo <<'EOF'\\nbar\\nEOF\\n)\",\n\t\t\t\"`\\nfoo <<'EOF'\\nbar\\nEOF\\n`\",\n\t\t\t\"`foo <<'EOF'\\nbar\\nEOF`\",\n\t\t},\n\t\tlangFile(cmdSubst(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: word(sglQuoted(\"EOF\")),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<'EOF'\\nbar\\nEOF`oops`\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: word(sglQuoted(\"EOF\")),\n\t\t\t\tHdoc: litWord(\"bar\\nEOF`oops`\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<'EOF'\\nbar\\nNOTEOF`oops`\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: word(sglQuoted(\"EOF\")),\n\t\t\t\tHdoc: litWord(\"bar\\nNOTEOF`oops`\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"$(<foo)\", \"`<foo`\"},\n\t\tlangFile(cmdSubst(&Stmt{\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   RdrIn,\n\t\t\t\tWord: litWord(\"foo\"),\n\t\t\t}},\n\t\t})),\n\t\tlangSkip(LangZsh), // actually tries to read foo when confirming\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<EOF >f\\nbar\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{\n\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t\t},\n\t\t\t\t{Op: RdrOut, Word: litWord(\"f\")},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<EOF && {\\nbar\\nEOF\\n\\tetc\\n}\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: AndStmt,\n\t\t\tX: &Stmt{\n\t\t\t\tCmd: litCall(\"foo\"),\n\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t\t}},\n\t\t\t},\n\t\t\tY: stmt(block(litStmt(\"etc\"))),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$(\\n\\tfoo\\n) <<EOF\\nbar\\nEOF\",\n\t\t\t\"<<EOF $(\\n\\tfoo\\n)\\nbar\\nEOF\",\n\t\t},\n\t\t// note that dash won't accept the second one\n\t\tlangFile(&Stmt{\n\t\t\tCmd: call(word(cmdSubst(litStmt(\"foo\")))),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$(\\n\\tfoo\\n) <<EOF\\nbar\\nEOF\",\n\t\t\t\"`\\n\\tfoo\\n` <<EOF\\nbar\\nEOF\",\n\t\t\t\"<<EOF `\\n\\tfoo\\n`\\nbar\\nEOF\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: call(word(cmdSubst(litStmt(\"foo\")))),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$((foo)) <<EOF\\nbar\\nEOF\",\n\t\t\t\"<<EOF $((\\n\\tfoo\\n))\\nbar\\nEOF\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: call(word(arithmExp(litWord(\"foo\")))),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"if true; then\\n\\tfoo <<-EOF\\n\\t\\tbar\\n\\tEOF\\nfi\"},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"true\"),\n\t\t\tThen: []*Stmt{{\n\t\t\t\tCmd: litCall(\"foo\"),\n\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\tOp:   DashHdoc,\n\t\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\t\tHdoc: litWord(\"\\t\\tbar\\n\\t\"),\n\t\t\t\t}},\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"if true; then\\n\\tfoo <<-EOF\\n\\tEOF\\nfi\"},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"true\"),\n\t\t\tThen: []*Stmt{{\n\t\t\t\tCmd: litCall(\"foo\"),\n\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\tOp:   DashHdoc,\n\t\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\t\tHdoc: litWord(\"\\t\"),\n\t\t\t\t}},\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<EOF\\nEOF_body\\nEOF\\nfoo2\"},\n\t\tlangFile([]*Stmt{\n\t\t\t{\n\t\t\t\tCmd: litCall(\"foo\"),\n\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\t\tHdoc: litWord(\"EOF_body\\n\"),\n\t\t\t\t}},\n\t\t\t},\n\t\t\tlitStmt(\"foo2\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<FOOBAR\\nbar\\nFOOBAR\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"FOOBAR\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<\\\"EOF\\\"\\nbar\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: word(dblQuoted(lit(\"EOF\"))),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<'EOF'\\nEOF_body\\nEOF\\nfoo2\"},\n\t\tlangFile([]*Stmt{\n\t\t\t{\n\t\t\t\tCmd: litCall(\"foo\"),\n\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\tWord: word(sglQuoted(\"EOF\")),\n\t\t\t\t\tHdoc: litWord(\"EOF_body\\n\"),\n\t\t\t\t}},\n\t\t\t},\n\t\t\tlitStmt(\"foo2\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<'EOF'\\n${\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: word(sglQuoted(\"EOF\")),\n\t\t\t\tHdoc: litWord(\"${\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<'EOF'\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: word(sglQuoted(\"EOF\")),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<\\\"EOF\\\"2\\nbar\\nEOF2\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: word(dblQuoted(lit(\"EOF\")), lit(\"2\")),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<\\\\EOF\\nbar\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"\\\\EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<EOF\\nbar\\\\\\nbaz\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: word(lit(\"bar\"), lit(\"baz\\n\")),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo <<'EOF'\\nbar\\\\\\nEOF\",\n\t\t\t\"foo <<'EOF'\\nbar\\\\\\r\\nEOF\",\n\t\t\t\"foo <<'EOF'\\nbar\\\\\\r\\nEOF\\r\\n\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: word(sglQuoted(\"EOF\")),\n\t\t\t\tHdoc: litWord(\"bar\\\\\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo <<-EOF\\n\\tbar\\nEOF\",\n\t\t\t\"foo <<-EOF\\r\\n\\tbar\\r\\nEOF\\r\\n\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   DashHdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"\\tbar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<EOF\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<-EOF\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   DashHdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<-EOF\\n\\tbar\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   DashHdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"\\tbar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo <<-'EOF'\\n\\tbar\\nEOF\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   DashHdoc,\n\t\t\t\tWord: word(sglQuoted(\"EOF\")),\n\t\t\t\tHdoc: litWord(\"\\tbar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"f1 <<EOF1\\nh1\\nEOF1\\nf2 <<EOF2\\nh2\\nEOF2\",\n\t\t\t\"f1 <<EOF1; f2 <<EOF2\\nh1\\nEOF1\\nh2\\nEOF2\",\n\t\t},\n\t\tlangFile([]*Stmt{\n\t\t\t{\n\t\t\t\tCmd: litCall(\"f1\"),\n\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\tWord: litWord(\"EOF1\"),\n\t\t\t\t\tHdoc: litWord(\"h1\\n\"),\n\t\t\t\t}},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCmd: litCall(\"f2\"),\n\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\tWord: litWord(\"EOF2\"),\n\t\t\t\t\tHdoc: litWord(\"h2\\n\"),\n\t\t\t\t}},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"a <<EOF\\nfoo\\nEOF\\nb\\nb\\nb\\nb\\nb\\nb\\nb\\nb\\nb\",\n\t\t\t\"a <<EOF;b;b;b;b;b;b;b;b;b\\nfoo\\nEOF\",\n\t\t},\n\t\tlangFile([]*Stmt{\n\t\t\t{\n\t\t\t\tCmd: litCall(\"a\"),\n\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\t\tHdoc: litWord(\"foo\\n\"),\n\t\t\t\t}},\n\t\t\t},\n\t\t\tlitStmt(\"b\"), litStmt(\"b\"), litStmt(\"b\"),\n\t\t\tlitStmt(\"b\"), litStmt(\"b\"), litStmt(\"b\"),\n\t\t\tlitStmt(\"b\"), litStmt(\"b\"), litStmt(\"b\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo \\\"\\narg\\\" <<EOF\\nbar\\nEOF\",\n\t\t\t\"foo <<EOF \\\"\\narg\\\"\\nbar\\nEOF\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: call(\n\t\t\t\tlitWord(\"foo\"),\n\t\t\t\tword(dblQuoted(lit(\"\\narg\"))),\n\t\t\t),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"bar\\n\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo >&2 <&0 2>file 345>file <>f2\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: DplOut, Word: litWord(\"2\")},\n\t\t\t\t{Op: DplIn, Word: litWord(\"0\")},\n\t\t\t\t{Op: RdrOut, N: lit(\"2\"), Word: litWord(\"file\")},\n\t\t\t\t{Op: RdrOut, N: lit(\"345\"), Word: litWord(\"file\")},\n\t\t\t\t{Op: RdrInOut, Word: litWord(\"f2\")},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo bar >file\",\n\t\t\t\"foo bar>file\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\", \"bar\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"file\")},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"true &>a\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"true\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrAll, Word: litWord(\"a\")},\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:6: `&>` redirects are a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t\tflipConfirm2(LangPOSIX), // POSIX shells tend to parse &> as & > hence it runs as two commands\n\t),\n\tfileTest(\n\t\t[]string{\"true &>>b\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"true\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: AppAll, Word: litWord(\"b\")},\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:6: `&>>` redirects are a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t\tflipConfirm2(LangPOSIX), // POSIX shells tend to parse &> as & > hence it runs as two commands\n\t),\n\tfileTest(\n\t\t[]string{\"foo 2>file bar\", \"2>file foo bar\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\", \"bar\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, N: lit(\"2\"), Word: litWord(\"file\")},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a >f1\\nb >f2\", \"a >f1; b >f2\"},\n\t\tlangFile([]*Stmt{\n\t\t\t{\n\t\t\t\tCmd:    litCall(\"a\"),\n\t\t\t\tRedirs: []*Redirect{{Op: RdrOut, Word: litWord(\"f1\")}},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCmd:    litCall(\"b\"),\n\t\t\t\tRedirs: []*Redirect{{Op: RdrOut, Word: litWord(\"f2\")}},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo >|bar\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrClob, Word: litWord(\"bar\")},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo >>|a &>|b &>>|c\"},\n\t\tlangErr2(\"1:5: `>>|` redirects are a zsh feature; tried parsing as LANG\"),\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: AppClob, Word: litWord(\"a\")},\n\t\t\t\t{Op: RdrAllClob, Word: litWord(\"b\")},\n\t\t\t\t{Op: AppAllClob, Word: litWord(\"c\")},\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo >|a >>|b &>|c &>>|d\",\n\t\t\t\"foo >!a >>!b &>!c &>>!d\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrClob, Word: litWord(\"a\")},\n\t\t\t\t{Op: AppClob, Word: litWord(\"b\")},\n\t\t\t\t{Op: RdrAllClob, Word: litWord(\"c\")},\n\t\t\t\t{Op: AppAllClob, Word: litWord(\"d\")},\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo &>|a &>>c &>>|d\",\n\t\t\t\"foo >&|a >>&c >>&|d\",\n\t\t\t\"foo &>!a >>&c >>&!d\",\n\t\t\t\"foo >&!a >>&c >>&!d\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrAllClob, Word: litWord(\"a\")},\n\t\t\t\t{Op: AppAll, Word: litWord(\"c\")},\n\t\t\t\t{Op: AppAllClob, Word: litWord(\"d\")},\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"foo <<<input\",\n\t\t\t\"foo <<< input\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   WordHdoc,\n\t\t\t\tWord: litWord(\"input\"),\n\t\t\t}},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:5: herestrings are a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`foo <<<\"spaced input\"`,\n\t\t\t`foo <<< \"spaced input\"`,\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   WordHdoc,\n\t\t\t\tWord: word(dblQuoted(lit(\"spaced input\"))),\n\t\t\t}},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"foo >(foo)\"},\n\t\tlangFile(call(\n\t\t\tlitWord(\"foo\"),\n\t\t\tword(&ProcSubst{\n\t\t\t\tOp:    CmdOut,\n\t\t\t\tStmts: litStmts(\"foo\"),\n\t\t\t}),\n\t\t), LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"foo < <(foo)\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp: RdrIn,\n\t\t\t\tWord: word(&ProcSubst{\n\t\t\t\t\tOp:    CmdIn,\n\t\t\t\t\tStmts: litStmts(\"foo\"),\n\t\t\t\t}),\n\t\t\t}},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"a<(b) c>(d)\"},\n\t\tlangFile(call(\n\t\t\tword(lit(\"a\"), &ProcSubst{\n\t\t\t\tOp:    CmdIn,\n\t\t\t\tStmts: litStmts(\"b\"),\n\t\t\t}),\n\t\t\tword(lit(\"c\"), &ProcSubst{\n\t\t\t\tOp:    CmdOut,\n\t\t\t\tStmts: litStmts(\"d\"),\n\t\t\t}),\n\t\t), LangBash|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"foo =(bar)\"},\n\t\tlangErr2(\"1:5: `=(` process substitutions are a zsh feature; tried parsing as LANG\"),\n\t\tlangFile(call(\n\t\t\tlitWord(\"foo\"), word(&ProcSubst{\n\t\t\t\tOp:    CmdInTemp,\n\t\t\t\tStmts: litStmts(\"bar\"),\n\t\t\t}),\n\t\t), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"foo {fd}<f\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: litCall(\"foo\"),\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrIn, N: lit(\"{fd}\"), Word: litWord(\"f\")},\n\t\t\t},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"! foo\"},\n\t\tlangFile(&Stmt{\n\t\t\tNegated: true,\n\t\t\tCmd:     litCall(\"foo\"),\n\t\t}),\n\t\tlangSkip(LangZsh), // fails to confirm?\n\t),\n\tfileTest(\n\t\t[]string{\"foo &\\nbar\", \"foo & bar\", \"foo&bar\"},\n\t\tlangFile([]*Stmt{\n\t\t\t{Cmd: litCall(\"foo\"), Background: true},\n\t\t\tlitStmt(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"! if foo; then bar; fi >/dev/null &\",\n\t\t\t\"! if foo; then bar; fi>/dev/null&\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tNegated: true,\n\t\t\tCmd: &IfClause{\n\t\t\t\tCond: litStmts(\"foo\"),\n\t\t\t\tThen: litStmts(\"bar\"),\n\t\t\t},\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"/dev/null\")},\n\t\t\t},\n\t\t\tBackground: true,\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t// TODO: should we allow formatting a redirect at the start?\n\t\t\t\"if foo; then bar; fi >/dev/null\",\n\t\t\t\">/dev/null if foo; then bar; fi\",\n\t\t},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: &IfClause{\n\t\t\t\tCond: litStmts(\"foo\"),\n\t\t\t\tThen: litStmts(\"bar\"),\n\t\t\t},\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"/dev/null\")},\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"! foo && bar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: AndStmt,\n\t\t\tX: &Stmt{\n\t\t\t\tCmd:     litCall(\"foo\"),\n\t\t\t\tNegated: true,\n\t\t\t},\n\t\t\tY: litStmt(\"bar\"),\n\t\t}),\n\t\tlangSkip(LangZsh), // fails to confirm?\n\t),\n\tfileTest(\n\t\t[]string{\"! foo | bar\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: &BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  litStmt(\"foo\"),\n\t\t\t\tY:  litStmt(\"bar\"),\n\t\t\t},\n\t\t\tNegated: true,\n\t\t}),\n\t\tlangSkip(LangZsh), // fails to confirm?\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"a && b &\\nc\",\n\t\t\t\"a && b & c\",\n\t\t},\n\t\tlangFile([]*Stmt{\n\t\t\t{\n\t\t\t\tCmd: &BinaryCmd{\n\t\t\t\t\tOp: AndStmt,\n\t\t\t\t\tX:  litStmt(\"a\"),\n\t\t\t\t\tY:  litStmt(\"b\"),\n\t\t\t\t},\n\t\t\t\tBackground: true,\n\t\t\t},\n\t\t\tlitStmt(\"c\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a | b &\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: &BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  litStmt(\"a\"),\n\t\t\t\tY:  litStmt(\"b\"),\n\t\t\t},\n\t\t\tBackground: true,\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo#bar\"},\n\t\tlangFile(litWord(\"foo#bar\")),\n\t),\n\tfileTest(\n\t\t[]string{\"$foo#bar foo#$bar\"},\n\t\tlangFile(call(\n\t\t\tword(litParamExp(\"foo\"), lit(\"#bar\")),\n\t\t\tword(lit(\"foo#\"), litParamExp(\"bar\")),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"$(foo)#bar\", \"`foo`#bar\"},\n\t\tlangFile(call(\n\t\t\tword(cmdSubst(litStmt(\"foo\")), lit(\"#bar\")),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"{ foo } }; }\"},\n\t\tlangFile(block(litStmt(\"foo\", \"}\", \"}\"))),\n\t\t// TODO: turn these nil files into error tests\n\t\tlangSkip(LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"foo {\"},\n\t\tlangFile(litStmt(\"foo\", \"{\")),\n\t),\n\tfileTest(\n\t\t[]string{\"foo }\"},\n\t\tlangFile(litStmt(\"foo\", \"}\"), LangBash),\n\t),\n\tfileTest(\n\t\t// TODO: should shfmt lean towards no semicolons in Zsh mode?\n\t\t// Note that zsh seems to support \"{foo}\" too, but that is undocumented,\n\t\t// and hopefully noone actually relies on that.\n\t\t[]string{\"{ foo; }\", \"{ foo }\"},\n\t\tlangFile(block(litStmt(\"foo\")), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"{ }\"},\n\t\tlangErr2(\"1:1: `{` must be followed by a statement list\"),\n\t\tlangFile(block(), LangZsh|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"{ }\", \"{}\"}, // Note that \"{}\" is a command in POSIX/Bash.\n\t\tlangFile(block(), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"( )\"},\n\t\tlangErr2(\"1:1: `(` must be followed by a statement list\"),\n\t\tlangFile(subshell(), LangZsh|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"if; then; fi\"},\n\t\tlangErr2(\"1:1: `if` must be followed by a statement list\"),\n\t\tlangFile(&IfClause{}, LangZsh|LangMirBSDKorn),\n\t\tlangSkip(LangMirBSDKorn), // only allows empty lists with newlines?\n\t),\n\tfileTest(\n\t\t[]string{\"if foo; then; fi\"},\n\t\tlangErr2(\"1:9: `then` must be followed by a statement list\"),\n\t\tlangFile(&IfClause{Cond: litStmts(\"foo\")}, LangZsh|LangMirBSDKorn),\n\t\tlangSkip(LangMirBSDKorn), // only allows empty lists with newlines?\n\t),\n\tfileTest(\n\t\t[]string{\"while; do exit; done\", \"while\\ndo exit\\ndone\"},\n\t\tlangErr2(\"1:1: `while` must be followed by a statement list\"),\n\t\tlangFile(&WhileClause{Do: litStmts(\"exit\")}, LangZsh|LangMirBSDKorn),\n\t\tlangSkip(LangMirBSDKorn), // only allows empty lists with newlines?\n\t),\n\tfileTest(\n\t\t[]string{\"while false; do; done\"},\n\t\tlangErr2(\"1:14: `do` must be followed by a statement list\"),\n\t\tlangFile(&WhileClause{Cond: litStmts(\"false\")}, LangZsh|LangMirBSDKorn),\n\t\tlangSkip(LangMirBSDKorn), // only allows empty lists with newlines?\n\t),\n\tfileTest(\n\t\t[]string{\"$({ foo; })\"},\n\t\tlangFile(cmdSubst(stmt(\n\t\t\tblock(litStmt(\"foo\")),\n\t\t))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$( (echo foo bar))\",\n\t\t\t\"$( (echo foo bar) )\",\n\t\t\t\"`(echo foo bar)`\",\n\t\t},\n\t\tlangFile(cmdSubst(stmt(\n\t\t\tsubshell(litStmt(\"echo\", \"foo\", \"bar\")),\n\t\t))),\n\t),\n\tfileTest(\n\t\t[]string{\"$()\"},\n\t\tlangFile(cmdSubst()),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$(\\n\\t(a)\\n\\tb\\n)\",\n\t\t\t\"$( (a); b)\",\n\t\t\t\"`(a); b`\",\n\t\t},\n\t\tlangFile(cmdSubst(\n\t\t\tstmt(subshell(litStmt(\"a\"))),\n\t\t\tlitStmt(\"b\"),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`$(echo \\')`,\n\t\t\t\"`\" + `echo \\\\'` + \"`\",\n\t\t},\n\t\tlangFile(cmdSubst(litStmt(\"echo\", `\\'`))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`$(echo \\\\)`,\n\t\t\t\"`\" + `echo \\\\\\\\` + \"`\",\n\t\t},\n\t\tlangFile(cmdSubst(litStmt(\"echo\", `\\\\`))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`$(echo '\\' 'a\\b' \"\\\\\" \"a\\a\")`,\n\t\t\t\"`\" + `echo '\\' 'a\\\\b' \"\\\\\\\\\" \"a\\a\"` + \"`\",\n\t\t},\n\t\tlangFile(cmdSubst(stmt(call(\n\t\t\tlitWord(\"echo\"),\n\t\t\tword(sglQuoted(`\\`)),\n\t\t\tword(sglQuoted(`a\\b`)),\n\t\t\tword(dblQuoted(lit(`\\\\`))),\n\t\t\tword(dblQuoted(lit(`a\\a`))),\n\t\t)))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$(echo $(x))\",\n\t\t\t\"`echo \\\\`x\\\\``\",\n\t\t},\n\t\tlangFile(cmdSubst(stmt(call(\n\t\t\tlitWord(\"echo\"),\n\t\t\tword(cmdSubst(litStmt(\"x\"))),\n\t\t)))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$($(foo bar))\",\n\t\t\t\"`\\\\`foo bar\\\\``\",\n\t\t},\n\t\tlangFile(cmdSubst(stmt(call(\n\t\t\tword(cmdSubst(litStmt(\"foo\", \"bar\"))),\n\t\t)))),\n\t),\n\tfileTest(\n\t\t[]string{\"$( (a) | b)\"},\n\t\tlangFile(cmdSubst(\n\t\t\tstmt(&BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  stmt(subshell(litStmt(\"a\"))),\n\t\t\t\tY:  litStmt(\"b\"),\n\t\t\t}),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`\"$( (foo))\"`},\n\t\tlangFile(dblQuoted(cmdSubst(stmt(\n\t\t\tsubshell(litStmt(\"foo\")),\n\t\t)))),\n\t),\n\tfileTest(\n\t\t[]string{\"\\\"foo\\\\\\nbar\\\"\"},\n\t\tlangFile(dblQuoted(lit(\"foo\"), lit(\"bar\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"'foo\\\\\\nbar'\", \"'foo\\\\\\r\\nbar'\"},\n\t\tlangFile(sglQuoted(\"foo\\\\\\nbar\")),\n\t),\n\tfileTest(\n\t\t[]string{\"$({ echo; })\", \"`{ echo; }`\"},\n\t\tlangFile(cmdSubst(stmt(\n\t\t\tblock(litStmt(\"echo\")),\n\t\t))),\n\t),\n\tfileTest(\n\t\t[]string{`{foo}`},\n\t\tlangFile(litWord(`{foo}`)),\n\t),\n\tfileTest(\n\t\t[]string{`{\"foo\"`},\n\t\tlangFile(word(lit(\"{\"), dblQuoted(lit(\"foo\")))),\n\t\tlangSkip(LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`foo\"bar\"`, \"fo\\\\\\no\\\"bar\\\"\", \"fo\\\\\\r\\no\\\"bar\\\"\"},\n\t\tlangFile(word(lit(\"foo\"), dblQuoted(lit(\"bar\")))),\n\t),\n\tfileTest(\n\t\t[]string{`!foo`},\n\t\tlangFile(litWord(`!foo`)),\n\t),\n\tfileTest(\n\t\t[]string{\"$(foo bar)\", \"`foo bar`\"},\n\t\tlangFile(cmdSubst(litStmt(\"foo\", \"bar\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"$(foo | bar)\", \"`foo | bar`\"},\n\t\tlangFile(cmdSubst(\n\t\t\tstmt(&BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  litStmt(\"foo\"),\n\t\t\t\tY:  litStmt(\"bar\"),\n\t\t\t}),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"$(foo | >f)\", \"`foo | >f`\"},\n\t\tlangFile(cmdSubst(\n\t\t\tstmt(&BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  litStmt(\"foo\"),\n\t\t\t\tY: &Stmt{Redirs: []*Redirect{{\n\t\t\t\t\tOp:   RdrOut,\n\t\t\t\t\tWord: litWord(\"f\"),\n\t\t\t\t}}},\n\t\t\t}),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"$(foo $(b1 b2))\"},\n\t\tlangFile(cmdSubst(stmt(call(\n\t\t\tlitWord(\"foo\"),\n\t\t\tword(cmdSubst(litStmt(\"b1\", \"b2\"))),\n\t\t)))),\n\t),\n\tfileTest(\n\t\t[]string{`\"$(foo \"bar\")\"`},\n\t\tlangFile(dblQuoted(cmdSubst(stmt(call(\n\t\t\tlitWord(\"foo\"),\n\t\t\tword(dblQuoted(lit(\"bar\"))),\n\t\t))))),\n\t),\n\tfileTest(\n\t\t[]string{\"$(foo)\", \"`fo\\\\\\no`\"},\n\t\tlangFile(cmdSubst(litStmt(\"foo\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"foo $(bar)\", \"foo `bar`\"},\n\t\tlangFile(call(\n\t\t\tlitWord(\"foo\"),\n\t\t\tword(cmdSubst(litStmt(\"bar\"))),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"$(foo 'bar')\", \"`foo 'bar'`\"},\n\t\tlangFile(cmdSubst(stmt(call(\n\t\t\tlitWord(\"foo\"),\n\t\t\tword(sglQuoted(\"bar\")),\n\t\t)))),\n\t),\n\tfileTest(\n\t\t[]string{`$(foo \"bar\")`, \"`foo \\\"bar\\\"`\"},\n\t\tlangFile(cmdSubst(stmt(call(\n\t\t\tlitWord(\"foo\"),\n\t\t\tword(dblQuoted(lit(\"bar\"))),\n\t\t)))),\n\t),\n\tfileTest(\n\t\t[]string{`\"$(foo \"bar\")\"`, \"\\\"`foo \\\"bar\\\"`\\\"\"},\n\t\tlangFile(dblQuoted(cmdSubst(stmt(call(\n\t\t\tlitWord(\"foo\"),\n\t\t\tword(dblQuoted(lit(\"bar\"))),\n\t\t))))),\n\t),\n\tfileTest(\n\t\t[]string{\"${ foo;}\", \"${\\nfoo; }\", \"${\\n\\tfoo; }\", \"${\\tfoo;}\"},\n\t\tlangFile(&CmdSubst{\n\t\t\tStmts:    litStmts(\"foo\"),\n\t\t\tTempFile: true,\n\t\t}, LangBash|LangMirBSDKorn),\n\t\tlangErr2(\"1:1: `${ stmts;}` is a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"${\\n\\tfoo\\n\\tbar\\n}\", \"${ foo; bar;}\"},\n\t\tlangFile(&CmdSubst{\n\t\t\tStmts:    litStmts(\"foo\", \"bar\"),\n\t\t\tTempFile: true,\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"${|foo;}\", \"${| foo; }\"},\n\t\tlangFile(&CmdSubst{\n\t\t\tStmts:    litStmts(\"foo\"),\n\t\t\tReplyVar: true,\n\t\t}, LangBash|LangMirBSDKorn),\n\t\tlangErr2(\"1:1: `${|stmts;}` is a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"${|\\n\\tfoo\\n\\tbar\\n}\", \"${|foo; bar;}\"},\n\t\tlangFile(&CmdSubst{\n\t\t\tStmts:    litStmts(\"foo\", \"bar\"),\n\t\t\tReplyVar: true,\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`\"$foo\"`},\n\t\tlangFile(dblQuoted(litParamExp(\"foo\"))),\n\t),\n\tfileTest(\n\t\t[]string{`\"#foo\"`},\n\t\tlangFile(dblQuoted(lit(\"#foo\"))),\n\t),\n\tfileTest(\n\t\t[]string{`$2foo $3[bar] \"ba$4z\"`},\n\t\tlangFile(call(\n\t\t\tword(litParamExp(\"2\"), lit(\"foo\")),\n\t\t\tword(litParamExp(\"3\"), lit(\"[bar]\")),\n\t\t\tword(dblQuoted(lit(\"ba\"), litParamExp(\"4\"), lit(\"z\"))),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`$@a $*a $#a $$a $?a $!a $-a $0a $30a $_a`},\n\t\tlangFile(call(\n\t\t\tword(litParamExp(\"@\"), lit(\"a\")),\n\t\t\tword(litParamExp(\"*\"), lit(\"a\")),\n\t\t\tword(litParamExp(\"#\"), lit(\"a\")),\n\t\t\tword(litParamExp(\"$\"), lit(\"a\")),\n\t\t\tword(litParamExp(\"?\"), lit(\"a\")),\n\t\t\tword(litParamExp(\"!\"), lit(\"a\")),\n\t\t\tword(litParamExp(\"-\"), lit(\"a\")),\n\t\t\tword(litParamExp(\"0\"), lit(\"a\")),\n\t\t\tword(litParamExp(\"3\"), lit(\"0a\")),\n\t\t\tword(litParamExp(\"_a\")),\n\t\t)),\n\t\tlangSkip(LangZsh), // TODO: $#a parses as ParamExp, but $!a does not\n\t),\n\tfileTest(\n\t\t[]string{`$`, `$ #`},\n\t\tlangFile(litWord(\"$\")),\n\t),\n\tfileTest(\n\t\t[]string{`${@} ${*} ${#} ${$} ${?} ${!} ${0} ${29} ${-}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{Param: lit(\"@\")}),\n\t\t\tword(&ParamExp{Param: lit(\"*\")}),\n\t\t\tword(&ParamExp{Param: lit(\"#\")}),\n\t\t\tword(&ParamExp{Param: lit(\"$\")}),\n\t\t\tword(&ParamExp{Param: lit(\"?\")}),\n\t\t\tword(&ParamExp{Param: lit(\"!\")}),\n\t\t\tword(&ParamExp{Param: lit(\"0\")}),\n\t\t\tword(&ParamExp{Param: lit(\"29\")}),\n\t\t\tword(&ParamExp{Param: lit(\"-\")}),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`${#$} ${#@} ${#*} ${##}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{Length: true, Param: lit(\"$\")}),\n\t\t\tword(&ParamExp{Length: true, Param: lit(\"@\")}),\n\t\t\tword(&ParamExp{Length: true, Param: lit(\"*\")}),\n\t\t\tword(&ParamExp{Length: true, Param: lit(\"#\")}),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`${foo}`},\n\t\tlangFile(&ParamExp{Param: lit(\"foo\")}),\n\t),\n\tfileTest(\n\t\t[]string{`${foo}\"bar\"`},\n\t\tlangFile(word(\n\t\t\t&ParamExp{Param: lit(\"foo\")},\n\t\t\tdblQuoted(lit(\"bar\")),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`$a/b $a-b $a:b $a}b $a]b $a.b $a,b $a*b $a_b $a2b`},\n\t\tlangFile(call(\n\t\t\tword(litParamExp(\"a\"), lit(\"/b\")),\n\t\t\tword(litParamExp(\"a\"), lit(\"-b\")),\n\t\t\tword(litParamExp(\"a\"), lit(\":b\")),\n\t\t\tword(litParamExp(\"a\"), lit(\"}b\")),\n\t\t\tword(litParamExp(\"a\"), lit(\"]b\")),\n\t\t\tword(litParamExp(\"a\"), lit(\".b\")),\n\t\t\tword(litParamExp(\"a\"), lit(\",b\")),\n\t\t\tword(litParamExp(\"a\"), lit(\"*b\")),\n\t\t\tword(litParamExp(\"a_b\")),\n\t\t\tword(litParamExp(\"a2b\")),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`$aàb $àb $,b`},\n\t\tlangFile(call(\n\t\t\tword(litParamExp(\"a\"), lit(\"àb\")),\n\t\t\tword(lit(\"$\"), lit(\"àb\")),\n\t\t\tword(lit(\"$\"), lit(\",b\")),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"$à\", \"$\\\\\\nà\", \"$\\\\\\r\\nà\"},\n\t\tlangFile(word(lit(\"$\"), lit(\"à\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"$foobar\", \"$foo\\\\\\nbar\"},\n\t\tlangFile(call(\n\t\t\tword(litParamExp(\"foobar\")),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"$foo\\\\bar\"},\n\t\tlangFile(call(\n\t\t\tword(litParamExp(\"foo\"), lit(\"\\\\bar\")),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`echo -e \"$foo\\nbar\"`},\n\t\tlangFile(call(\n\t\t\tlitWord(\"echo\"), litWord(\"-e\"),\n\t\t\tword(dblQuoted(\n\t\t\t\tlitParamExp(\"foo\"), lit(`\\nbar`),\n\t\t\t)),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`${foo-bar}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   DefaultUnset,\n\t\t\t\tWord: litWord(\"bar\"),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`${foo+}\"bar\"`},\n\t\tlangFile(word(\n\t\t\t&ParamExp{\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tExp:   &Expansion{Op: AlternateUnset},\n\t\t\t},\n\t\t\tdblQuoted(lit(\"bar\")),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`${foo:=<\"bar\"}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   AssignUnsetOrNull,\n\t\t\t\tWord: word(lit(\"<\"), dblQuoted(lit(\"bar\"))),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"${foo:=b${c}$(d)}\",\n\t\t\t\"${foo:=b${c}`d`}\",\n\t\t},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp: AssignUnsetOrNull,\n\t\t\t\tWord: word(\n\t\t\t\t\tlit(\"b\"),\n\t\t\t\t\t&ParamExp{Param: lit(\"c\")},\n\t\t\t\t\tcmdSubst(litStmt(\"d\")),\n\t\t\t\t),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`${foo?\"${bar}\"}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp: ErrorUnset,\n\t\t\t\tWord: word(dblQuoted(\n\t\t\t\t\t&ParamExp{Param: lit(\"bar\")},\n\t\t\t\t)),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`${foo:?bar1 bar2}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   ErrorUnsetOrNull,\n\t\t\t\tWord: litWord(\"bar1 bar2\"),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`${a:+b}${a:-b}${a=b}`},\n\t\tlangFile(word(\n\t\t\t&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   AlternateUnsetOrNull,\n\t\t\t\t\tWord: litWord(\"b\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   DefaultUnsetOrNull,\n\t\t\t\t\tWord: litWord(\"b\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   AssignUnset,\n\t\t\t\t\tWord: litWord(\"b\"),\n\t\t\t\t},\n\t\t\t},\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`${3:-'$x'}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"3\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   DefaultUnsetOrNull,\n\t\t\t\tWord: word(sglQuoted(\"$x\")),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`${@:-$x}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"@\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   DefaultUnsetOrNull,\n\t\t\t\tWord: word(litParamExp(\"x\")),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`${var#*'=\"'}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"var\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   RemSmallPrefix,\n\t\t\t\tWord: word(lit(\"*\"), sglQuoted(`=\"`)),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`${var/'a'/b'c'd}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"var\"),\n\t\t\tRepl: &Replace{\n\t\t\t\tOrig: word(sglQuoted(\"a\")),\n\t\t\t\tWith: word(lit(\"b\"), sglQuoted(\"c\"), lit(\"d\")),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo%bar}${foo%%bar*}`},\n\t\tlangFile(word(\n\t\t\t&ParamExp{\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   RemSmallSuffix,\n\t\t\t\t\tWord: litWord(\"bar\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t&ParamExp{\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   RemLargeSuffix,\n\t\t\t\t\tWord: litWord(\"bar*\"),\n\t\t\t\t},\n\t\t\t},\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`${3#bar}${-##bar*}`},\n\t\tlangFile(word(\n\t\t\t&ParamExp{\n\t\t\t\tParam: lit(\"3\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   RemSmallPrefix,\n\t\t\t\t\tWord: litWord(\"bar\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t&ParamExp{\n\t\t\t\tParam: lit(\"-\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   RemLargePrefix,\n\t\t\t\t\tWord: litWord(\"bar*\"),\n\t\t\t\t},\n\t\t\t},\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`${foo%?}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   RemSmallSuffix,\n\t\t\t\tWord: litWord(\"?\"),\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo=force_expansion\\necho ${foo:#bar}\"},\n\t\tlangErr2(\"2:11: ${name:#arg} is a zsh feature; tried parsing as LANG\"),\n\t\tlangFile(stmts(\n\t\t\t&CallExpr{Assigns: litAssigns(\"foo=force_expansion\")},\n\t\t\tcall(litWord(\"echo\"), word(&ParamExp{\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   MatchEmpty,\n\t\t\t\t\tWord: litWord(\"bar\"),\n\t\t\t\t},\n\t\t\t})),\n\t\t), LangZsh),\n\t\tflipConfirm2(LangMirBSDKorn), // TODO: why is this a valid substitution in mksh?\n\t),\n\tfileTest(\n\t\t[]string{\"foo=force_expansion\\necho ${foo:|bar}\"},\n\t\tlangErr2(\"2:11: ${name:|arg} is a zsh feature; tried parsing as LANG\"),\n\t\tlangFile(stmts(\n\t\t\t&CallExpr{Assigns: litAssigns(\"foo=force_expansion\")},\n\t\t\tcall(litWord(\"echo\"), word(&ParamExp{\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   ArrayExclude,\n\t\t\t\t\tWord: litWord(\"bar\"),\n\t\t\t\t},\n\t\t\t})),\n\t\t), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"foo=force_expansion\\necho ${foo:*bar}\"},\n\t\tlangErr2(\"2:11: ${name:*arg} is a zsh feature; tried parsing as LANG\"),\n\t\tlangFile(stmts(\n\t\t\t&CallExpr{Assigns: litAssigns(\"foo=force_expansion\")},\n\t\t\tcall(litWord(\"echo\"), word(&ParamExp{\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   ArrayIntersect,\n\t\t\t\t\tWord: litWord(\"bar\"),\n\t\t\t\t},\n\t\t\t})),\n\t\t), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`${foo[1]}`,\n\t\t\t`${foo[ 1 ]}`,\n\t\t},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: litWord(\"1\"),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:6: arrays are a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`$foo[1]`},\n\t\tlangFile(&ParamExp{\n\t\t\tShort: true,\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: litWord(\"1\"),\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`a$foo[1]b`},\n\t\tlangFile(word(lit(\"a\"), litParamExp(\"foo\"), lit(\"[1]b\"))),\n\t\tlangFile(word(\n\t\t\tlit(\"a\"),\n\t\t\t&ParamExp{\n\t\t\t\tShort: true,\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tIndex: litWord(\"1\"),\n\t\t\t},\n\t\t\tlit(\"b\"),\n\t\t), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`\"a$foo[1]b\"`},\n\t\tlangFile(dblQuoted(\n\t\t\tlit(\"a\"),\n\t\t\t&ParamExp{\n\t\t\t\tShort: true,\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tIndex: litWord(\"1\"),\n\t\t\t},\n\t\t\tlit(\"b\"),\n\t\t), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[1,3]}`, `${foo[ 1 , 3 ]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: &BinaryArithm{\n\t\t\t\tOp: Comma,\n\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\tY:  litWord(\"3\"),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:6: arrays are a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[1,-1]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: &BinaryArithm{\n\t\t\t\tOp: Comma,\n\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\tY: &UnaryArithm{\n\t\t\t\t\tOp: Minus,\n\t\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:6: arrays are a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`$foo[1,3]`},\n\t\tlangFile(&ParamExp{\n\t\t\tShort: true,\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: &BinaryArithm{\n\t\t\t\tOp: Comma,\n\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\tY:  litWord(\"3\"),\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${signals[(i)QUIT]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"signals\"),\n\t\t\tIndex: &FlagsArithm{\n\t\t\t\tFlags: lit(\"i\"),\n\t\t\t\tX:     litWord(\"QUIT\"),\n\t\t\t},\n\t\t}, LangZsh),\n\t\tlangErr2(\"1:11: subscript flags are a zsh feature; tried parsing as LANG\", LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`${ZSH_VERSION[(s:.:w)2]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"ZSH_VERSION\"),\n\t\t\tIndex: &FlagsArithm{\n\t\t\t\tFlags: lit(\"s:.:w\"),\n\t\t\t\tX:     litWord(\"2\"),\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`$foo[(r)pattern]`},\n\t\tlangFile(&ParamExp{\n\t\t\tShort: true,\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: &FlagsArithm{\n\t\t\t\tFlags: lit(\"r\"),\n\t\t\t\tX:     litWord(\"pattern\"),\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${array[(i)]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"array\"),\n\t\t\tIndex: &FlagsArithm{\n\t\t\t\tFlags: lit(\"i\"),\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[(r)ab,(r)cd]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: &BinaryArithm{\n\t\t\t\tOp: Comma,\n\t\t\t\tX: &FlagsArithm{\n\t\t\t\t\tFlags: lit(\"r\"),\n\t\t\t\t\tX:     litWord(\"ab\"),\n\t\t\t\t},\n\t\t\t\tY: &FlagsArithm{\n\t\t\t\t\tFlags: lit(\"r\"),\n\t\t\t\t\tX:     litWord(\"cd\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[-1]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: &UnaryArithm{\n\t\t\t\tOp: Minus,\n\t\t\t\tX:  litWord(\"1\"),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[@]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: litWord(\"@\"),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[*]-etc}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: litWord(\"*\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   DefaultUnset,\n\t\t\t\tWord: litWord(\"etc\"),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[bar]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: litWord(\"bar\"),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[$bar]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: word(litParamExp(\"bar\")),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo[${bar}]}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tIndex: word(&ParamExp{Param: lit(\"bar\")}),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo:1}`, `${foo: 1 }`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{Offset: litWord(\"1\")},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:6: slicing is a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`${foo:1:2}`, `${foo: 1 : 2 }`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{\n\t\t\t\tOffset: litWord(\"1\"),\n\t\t\t\tLength: litWord(\"2\"),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo:a:b}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{\n\t\t\t\tOffset: litWord(\"a\"),\n\t\t\t\tLength: litWord(\"b\"),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`${foo:1:-2}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{\n\t\t\t\tOffset: litWord(\"1\"),\n\t\t\t\tLength: &UnaryArithm{Op: Minus, X: litWord(\"2\")},\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn), // TODO: zsh -n is buggy here\n\t),\n\tfileTest(\n\t\t[]string{`${foo::+3}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{\n\t\t\t\tLength: &UnaryArithm{Op: Plus, X: litWord(\"3\")},\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo: -1}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{\n\t\t\t\tOffset: &UnaryArithm{Op: Minus, X: litWord(\"1\")},\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo: +2+3}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{\n\t\t\t\tOffset: &BinaryArithm{\n\t\t\t\t\tOp: Add,\n\t\t\t\t\tX:  &UnaryArithm{Op: Plus, X: litWord(\"2\")},\n\t\t\t\t\tY:  litWord(\"3\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo:a?1:2:3}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{\n\t\t\t\tOffset: &BinaryArithm{\n\t\t\t\t\tOp: TernQuest,\n\t\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\t\tY: &BinaryArithm{\n\t\t\t\t\t\tOp: TernColon,\n\t\t\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\t\t\tY:  litWord(\"2\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tLength: litWord(\"3\"),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`${foo/a/b}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl:  &Replace{Orig: litWord(\"a\"), With: litWord(\"b\")},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr2(\"1:6: search and replace is a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"${foo/ /\\t}\"},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl:  &Replace{Orig: litWord(\" \"), With: litWord(\"\\t\")},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo/[/]-}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl:  &Replace{Orig: litWord(\"[\"), With: litWord(\"]-\")},\n\t\t}, LangBash|LangMirBSDKorn), // TODO: zsh parses as a pattern?\n\t),\n\tfileTest(\n\t\t[]string{`${foo/bar/b/a/r}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl: &Replace{\n\t\t\t\tOrig: litWord(\"bar\"),\n\t\t\t\tWith: litWord(\"b/a/r\"),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo/$a/$'\\''}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl: &Replace{\n\t\t\t\tOrig: word(litParamExp(\"a\")),\n\t\t\t\tWith: word(sglDQuoted(`\\'`)),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo//b1/b2}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl: &Replace{\n\t\t\t\tAll:  true,\n\t\t\t\tOrig: litWord(\"b1\"),\n\t\t\t\tWith: litWord(\"b2\"),\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo///}`, `${foo//}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl:  &Replace{All: true},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo/-//}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl:  &Replace{Orig: litWord(\"-\"), With: litWord(\"/\")},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo//#/}`, `${foo//#}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl:  &Replace{All: true, Orig: litWord(\"#\")},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${foo//[42]/}`},\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tRepl:  &Replace{All: true, Orig: litWord(\"[42]\")},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${a^b} ${a^^b} ${a,b} ${a,,b}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   UpperFirst,\n\t\t\t\t\tWord: litWord(\"b\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   UpperAll,\n\t\t\t\t\tWord: litWord(\"b\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   LowerFirst,\n\t\t\t\t\tWord: litWord(\"b\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   LowerAll,\n\t\t\t\t\tWord: litWord(\"b\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t), LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`${a@E} ${b@a} ${@@Q} ${!ref@P}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   OtherParamOps,\n\t\t\t\t\tWord: litWord(\"E\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"b\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   OtherParamOps,\n\t\t\t\t\tWord: litWord(\"a\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"@\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   OtherParamOps,\n\t\t\t\t\tWord: litWord(\"Q\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tExcl:  true,\n\t\t\t\tParam: lit(\"ref\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   OtherParamOps,\n\t\t\t\t\tWord: litWord(\"P\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t), LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`${a@K} ${b@k}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   OtherParamOps,\n\t\t\t\t\tWord: litWord(\"K\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"b\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   OtherParamOps,\n\t\t\t\t\tWord: litWord(\"k\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t), LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`${a@Q} ${b@#}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   OtherParamOps,\n\t\t\t\t\tWord: litWord(\"Q\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"b\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   OtherParamOps,\n\t\t\t\t\tWord: litWord(\"#\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t), LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`${#foo}`},\n\t\tlangFile(&ParamExp{\n\t\t\tLength: true,\n\t\t\tParam:  lit(\"foo\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`${%foo}`},\n\t\tlangFile(&ParamExp{\n\t\t\tWidth: true,\n\t\t\tParam: lit(\"foo\"),\n\t\t}, LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`${!foo} ${!bar[@]}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tExcl:  true,\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tExcl:  true,\n\t\t\t\tParam: lit(\"bar\"),\n\t\t\t\tIndex: litWord(\"@\"),\n\t\t\t}),\n\t\t), LangBash|LangMirBSDKorn),\n\t\tlangErr2(\"1:1: `${!foo}` is a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`${!foo*} ${!bar@}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tExcl:  true,\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tNames: NamesPrefix,\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tExcl:  true,\n\t\t\t\tParam: lit(\"bar\"),\n\t\t\t\tNames: NamesPrefixWords,\n\t\t\t}),\n\t\t), LangBash),\n\t\tlangErr2(\"1:1: `${!foo}` is a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t\tlangErr2(\"1:1: `${!foo*}` is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`${#?}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{Length: true, Param: lit(\"?\")}),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`${#-foo} ${#?bar}`},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"#\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   DefaultUnset,\n\t\t\t\t\tWord: litWord(\"foo\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tParam: lit(\"#\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   ErrorUnset,\n\t\t\t\t\tWord: litWord(\"bar\"),\n\t\t\t\t},\n\t\t\t}),\n\t\t)),\n\t\tlangSkip(LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`\"${foo}\"`},\n\t\tlangFile(dblQuoted(&ParamExp{Param: lit(\"foo\")})),\n\t),\n\tfileTest(\n\t\t[]string{`\"(foo)\"`},\n\t\tlangFile(dblQuoted(lit(\"(foo)\"))),\n\t),\n\tfileTest(\n\t\t[]string{`\"${foo}>\"`},\n\t\tlangFile(dblQuoted(\n\t\t\t&ParamExp{Param: lit(\"foo\")},\n\t\t\tlit(\">\"),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`\"$(foo)\"`, \"\\\"`foo`\\\"\"},\n\t\tlangFile(dblQuoted(cmdSubst(litStmt(\"foo\")))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`\"$(foo bar)\"`,\n\t\t\t`\"$(foo  bar)\"`,\n\t\t\t\"\\\"`foo bar`\\\"\",\n\t\t\t\"\\\"`foo  bar`\\\"\",\n\t\t},\n\t\tlangFile(dblQuoted(cmdSubst(litStmt(\"foo\", \"bar\")))),\n\t),\n\tfileTest(\n\t\t[]string{`'${foo}'`},\n\t\tlangFile(sglQuoted(\"${foo}\")),\n\t),\n\tfileTest(\n\t\t[]string{\"$((1))\"},\n\t\tlangFile(arithmExp(litWord(\"1\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"$((1 + 3))\", \"$((1+3))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Add,\n\t\t\tX:  litWord(\"1\"),\n\t\t\tY:  litWord(\"3\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`\"$((foo))\"`},\n\t\tlangFile(dblQuoted(arithmExp(\n\t\t\tlitWord(\"foo\"),\n\t\t))),\n\t),\n\tfileTest(\n\t\t[]string{`$((a)) b`},\n\t\tlangFile(call(\n\t\t\tword(arithmExp(litWord(\"a\"))),\n\t\t\tlitWord(\"b\"),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{`$((arr[0]++))`},\n\t\tlangFile(arithmExp(&UnaryArithm{\n\t\t\tOp: Inc, Post: true,\n\t\t\tX: word(&ParamExp{\n\t\t\t\tShort: true,\n\t\t\t\tParam: lit(\"arr\"),\n\t\t\t\tIndex: litWord(\"0\"),\n\t\t\t}),\n\t\t}), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`$((++arr[0]))`},\n\t\tlangFile(arithmExp(&UnaryArithm{\n\t\t\tOp: Inc,\n\t\t\tX: word(&ParamExp{\n\t\t\t\tShort: true,\n\t\t\t\tParam: lit(\"arr\"),\n\t\t\t\tIndex: litWord(\"0\"),\n\t\t\t}),\n\t\t}), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`$((${a:-1}))`},\n\t\tlangFile(arithmExp(word(&ParamExp{\n\t\t\tParam: lit(\"a\"),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   DefaultUnsetOrNull,\n\t\t\t\tWord: litWord(\"1\"),\n\t\t\t},\n\t\t})), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$((5 * 2 - 1))\", \"$((5*2-1))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Sub,\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: Mul,\n\t\t\t\tX:  litWord(\"5\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t},\n\t\t\tY: litWord(\"1\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((i | 13))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Or,\n\t\t\tX:  litWord(\"i\"),\n\t\t\tY:  litWord(\"13\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$(((a) + ((b))))\",\n\t\t\t\"$((\\n(a) + \\n(\\n(b)\\n)\\n))\",\n\t\t},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Add,\n\t\t\tX:  parenArit(litWord(\"a\")),\n\t\t\tY:  parenArit(parenArit(litWord(\"b\"))),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$((3 % 7))\",\n\t\t\t\"$((3\\n% 7))\",\n\t\t\t\"$((3\\\\\\n % 7))\",\n\t\t\t\"$((3\\\\\\r\\n % 7))\",\n\t\t},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Rem,\n\t\t\tX:  litWord(\"3\"),\n\t\t\tY:  litWord(\"7\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`\"$((1 / 3))\"`},\n\t\tlangFile(dblQuoted(arithmExp(&BinaryArithm{\n\t\t\tOp: Quo,\n\t\t\tX:  litWord(\"1\"),\n\t\t\tY:  litWord(\"3\"),\n\t\t}))),\n\t),\n\tfileTest(\n\t\t[]string{\"$((2 ** 10))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Pow,\n\t\t\tX:  litWord(\"2\"),\n\t\t\tY:  litWord(\"10\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$(((1) ^ 3))`},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Xor,\n\t\t\tX:  parenArit(litWord(\"1\")),\n\t\t\tY:  litWord(\"3\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((1 >> (3 << 2)))`},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Shr,\n\t\t\tX:  litWord(\"1\"),\n\t\t\tY: parenArit(&BinaryArithm{\n\t\t\t\tOp: Shl,\n\t\t\t\tX:  litWord(\"3\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t}),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((-(1)))`},\n\t\tlangFile(arithmExp(&UnaryArithm{\n\t\t\tOp: Minus,\n\t\t\tX:  parenArit(litWord(\"1\")),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((i++))`},\n\t\tlangFile(arithmExp(&UnaryArithm{\n\t\t\tOp:   Inc,\n\t\t\tPost: true,\n\t\t\tX:    litWord(\"i\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((--i))`},\n\t\tlangFile(arithmExp(&UnaryArithm{Op: Dec, X: litWord(\"i\")})),\n\t),\n\tfileTest(\n\t\t[]string{`$((!i))`},\n\t\tlangFile(arithmExp(&UnaryArithm{Op: Not, X: litWord(\"i\")})),\n\t),\n\tfileTest(\n\t\t[]string{`$((~i))`},\n\t\tlangFile(arithmExp(&UnaryArithm{Op: BitNegation, X: litWord(\"i\")})),\n\t),\n\tfileTest(\n\t\t[]string{`$((-!+i))`},\n\t\tlangFile(arithmExp(&UnaryArithm{\n\t\t\tOp: Minus,\n\t\t\tX: &UnaryArithm{\n\t\t\t\tOp: Not,\n\t\t\t\tX:  &UnaryArithm{Op: Plus, X: litWord(\"i\")},\n\t\t\t},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((!!i))`},\n\t\tlangFile(arithmExp(&UnaryArithm{\n\t\t\tOp: Not,\n\t\t\tX:  &UnaryArithm{Op: Not, X: litWord(\"i\")},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((~~i))`},\n\t\tlangFile(arithmExp(&UnaryArithm{\n\t\t\tOp: BitNegation,\n\t\t\tX:  &UnaryArithm{Op: BitNegation, X: litWord(\"i\")},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((1 < 3))`},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Lss,\n\t\t\tX:  litWord(\"1\"),\n\t\t\tY:  litWord(\"3\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((i = 2))`, `$((i=2))`},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Assgn,\n\t\t\tX:  litWord(\"i\"),\n\t\t\tY:  litWord(\"2\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`((a[i] = 4))`, `((a[i]=4))`},\n\t\tlangFile(arithmCmd(&BinaryArithm{\n\t\t\tOp: Assgn,\n\t\t\tX: word(&ParamExp{\n\t\t\t\tShort: true,\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tIndex: litWord(\"i\"),\n\t\t\t}),\n\t\t\tY: litWord(\"4\"),\n\t\t}), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a += 2, b -= 3))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Comma,\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: AddAssgn,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t},\n\t\t\tY: &BinaryArithm{\n\t\t\t\tOp: SubAssgn,\n\t\t\t\tX:  litWord(\"b\"),\n\t\t\t\tY:  litWord(\"3\"),\n\t\t\t},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a >>= 2, b <<= 3))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Comma,\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: ShrAssgn,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t},\n\t\t\tY: &BinaryArithm{\n\t\t\t\tOp: ShlAssgn,\n\t\t\t\tX:  litWord(\"b\"),\n\t\t\t\tY:  litWord(\"3\"),\n\t\t\t},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a ^^ 2))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: XorBool,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"2\"),\n\t\t}), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a ^^= 2, b **= 3))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Comma,\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: XorBoolAssgn,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t},\n\t\t\tY: &BinaryArithm{\n\t\t\t\tOp: PowAssgn,\n\t\t\t\tX:  litWord(\"b\"),\n\t\t\t\tY:  litWord(\"3\"),\n\t\t\t},\n\t\t}), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a &&= 2, b ||= 3))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Comma,\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: AndBoolAssgn,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t},\n\t\t\tY: &BinaryArithm{\n\t\t\t\tOp: OrBoolAssgn,\n\t\t\t\tX:  litWord(\"b\"),\n\t\t\t\tY:  litWord(\"3\"),\n\t\t\t},\n\t\t}), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a == b && c > d))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: AndArit,\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: Eql,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"b\"),\n\t\t\t},\n\t\t\tY: &BinaryArithm{\n\t\t\t\tOp: Gtr,\n\t\t\t\tX:  litWord(\"c\"),\n\t\t\t\tY:  litWord(\"d\"),\n\t\t\t},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a != b))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Neq,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a &= b))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: AndAssgn,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a |= b))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: OrAssgn,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a %= b))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: RemAssgn,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a /= b))\", \"$((a/=b))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: QuoAssgn,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((a ^= b))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: XorAssgn,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((i *= 3))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: MulAssgn,\n\t\t\tX:  litWord(\"i\"),\n\t\t\tY:  litWord(\"3\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((2 >= 10))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Geq,\n\t\t\tX:  litWord(\"2\"),\n\t\t\tY:  litWord(\"10\"),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"$((foo ? b1 : b2))\"},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: TernQuest,\n\t\t\tX:  litWord(\"foo\"),\n\t\t\tY: &BinaryArithm{\n\t\t\t\tOp: TernColon,\n\t\t\t\tX:  litWord(\"b1\"),\n\t\t\t\tY:  litWord(\"b2\"),\n\t\t\t},\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`$((a <= (1 || 2)))`},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Leq,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY: parenArit(&BinaryArithm{\n\t\t\t\tOp: OrArit,\n\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t}),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{\"foo$\", \"foo$\\n\"},\n\t\tlangFile(word(lit(\"foo\"), lit(\"$\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"foo$\", \"foo$\\\\\\n\", \"foo$\\\\\\r\\n\"},\n\t\tlangFile(word(lit(\"foo\"), lit(\"$\"))),\n\t),\n\tfileTest(\n\t\t[]string{`$''`},\n\t\tlangFile(sglDQuoted(\"\"), LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(word(lit(\"$\"), sglQuoted(\"\")), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`$\"\"`},\n\t\tlangFile(dblDQuoted(), LangBash|LangMirBSDKorn),\n\t\tlangFile(word(lit(\"$\"), dblQuoted()), LangPOSIX|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`$'foo'`},\n\t\tlangFile(sglDQuoted(\"foo\"), LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(word(lit(\"$\"), sglQuoted(\"foo\")), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`$'f+oo${'`},\n\t\tlangFile(sglDQuoted(\"f+oo${\"), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$'foo bar`'\"},\n\t\tlangFile(sglDQuoted(\"foo bar`\"), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$'a ${b} c'\"},\n\t\tlangFile(sglDQuoted(\"a ${b} c\"), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`$\"a ${b} c\"`},\n\t\tlangFile(dblDQuoted(\n\t\t\tlit(\"a \"),\n\t\t\t&ParamExp{Param: lit(\"b\")},\n\t\t\tlit(\" c\"),\n\t\t), LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`\"a $b c\"`},\n\t\tlangFile(dblQuoted(lit(\"a \"), litParamExp(\"b\"), lit(\" c\"))),\n\t),\n\tfileTest(\n\t\t[]string{`$\"a $b c\"`},\n\t\tlangFile(dblDQuoted(\n\t\t\tlit(\"a \"),\n\t\t\tlitParamExp(\"b\"),\n\t\t\tlit(\" c\"),\n\t\t), LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"$'f\\\\'oo\\n'\"},\n\t\tlangFile(sglDQuoted(\"f\\\\'oo\\n\"), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`$\"foo\"`},\n\t\tlangFile(dblDQuoted(lit(\"foo\")), LangBash|LangMirBSDKorn),\n\t\tlangFile(word(lit(\"$\"), dblQuoted(lit(\"foo\"))), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`$\"foo$\"`},\n\t\tlangFile(dblDQuoted(lit(\"foo\"), lit(\"$\")), LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`$\"foo bar\"`},\n\t\tlangFile(dblDQuoted(lit(\"foo bar\")), LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`$'f\\'oo'`},\n\t\tlangFile(sglDQuoted(`f\\'oo`), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`$\"f\\\"oo\"`},\n\t\tlangFile(dblDQuoted(lit(`f\\\"oo`)), LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`\"foo$\"`},\n\t\tlangFile(dblQuoted(lit(\"foo\"), lit(\"$\"))),\n\t),\n\tfileTest(\n\t\t[]string{`\"foo$$\"`},\n\t\tlangFile(dblQuoted(lit(\"foo\"), litParamExp(\"$\"))),\n\t),\n\tfileTest(\n\t\t[]string{`\"a $\\\"b\\\" c\"`},\n\t\tlangFile(dblQuoted(lit(`a `), lit(`$`), lit(`\\\"b\\\" c`))),\n\t),\n\tfileTest(\n\t\t[]string{\"$(foo$)\", \"`foo$`\"},\n\t\tlangFile(cmdSubst(\n\t\t\tstmt(call(word(lit(\"foo\"), lit(\"$\")))),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"foo$bar\"},\n\t\tlangFile(word(lit(\"foo\"), litParamExp(\"bar\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"foo$(bar)\"},\n\t\tlangFile(word(lit(\"foo\"), cmdSubst(litStmt(\"bar\")))),\n\t),\n\tfileTest(\n\t\t[]string{\"foo${bar}\"},\n\t\tlangFile(word(lit(\"foo\"), &ParamExp{Param: lit(\"bar\")})),\n\t),\n\tfileTest(\n\t\t[]string{\"'foo${bar'\"},\n\t\tlangFile(sglQuoted(\"foo${bar\")),\n\t),\n\tfileTest(\n\t\t[]string{\"(foo)\\nbar\", \"(foo); bar\"},\n\t\tlangFile([]Command{\n\t\t\tsubshell(litStmt(\"foo\")),\n\t\t\tlitCall(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo\\n(bar)\", \"foo; (bar)\"},\n\t\tlangFile([]Command{\n\t\t\tlitCall(\"foo\"),\n\t\t\tsubshell(litStmt(\"bar\")),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo\\n(bar)\", \"foo; (bar)\"},\n\t\tlangFile([]Command{\n\t\t\tlitCall(\"foo\"),\n\t\t\tsubshell(litStmt(\"bar\")),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"case $i in 1) foo ;; 2 | 3*) bar ;; esac\",\n\t\t\t\"case $i in 1) foo;; 2 | 3*) bar; esac\",\n\t\t\t\"case $i in (1) foo;; 2 | 3*) bar;; esac\",\n\t\t\t\"case $i\\nin\\n#etc\\n1)\\nfoo\\n;;\\n2 | 3*)\\nbar\\n;;\\nesac\",\n\t\t},\n\t\tlangFile(&CaseClause{\n\t\t\tWord: word(litParamExp(\"i\")),\n\t\t\tItems: []*CaseItem{\n\t\t\t\t{\n\t\t\t\t\tOp:       Break,\n\t\t\t\t\tPatterns: litWords(\"1\"),\n\t\t\t\t\tStmts:    litStmts(\"foo\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tOp:       Break,\n\t\t\t\t\tPatterns: litWords(\"2\", \"3*\"),\n\t\t\t\t\tStmts:    litStmts(\"bar\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"case i in 1) a ;& 2) ;; esac\"},\n\t\tlangFile(&CaseClause{\n\t\t\tWord: litWord(\"i\"),\n\t\t\tItems: []*CaseItem{\n\t\t\t\t{\n\t\t\t\t\tOp:       Fallthrough,\n\t\t\t\t\tPatterns: litWords(\"1\"),\n\t\t\t\t\tStmts:    litStmts(\"a\"),\n\t\t\t\t},\n\t\t\t\t{Op: Break, Patterns: litWords(\"2\")},\n\t\t\t},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"case i in 1) a ;; esac\",\n\t\t\t\"case i { 1) a ;; }\",\n\t\t\t\"case i {\\n1) a ;;\\n}\",\n\t\t},\n\t\tlangFile(&CaseClause{\n\t\t\tWord: litWord(\"i\"),\n\t\t\tItems: []*CaseItem{{\n\t\t\t\tOp:       Break,\n\t\t\t\tPatterns: litWords(\"1\"),\n\t\t\t\tStmts:    litStmts(\"a\"),\n\t\t\t}},\n\t\t}, LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"case i in 1) a ;;& 2) b ;; esac\"},\n\t\tlangFile(&CaseClause{\n\t\t\tWord: litWord(\"i\"),\n\t\t\tItems: []*CaseItem{\n\t\t\t\t{\n\t\t\t\t\tOp:       Resume,\n\t\t\t\t\tPatterns: litWords(\"1\"),\n\t\t\t\t\tStmts:    litStmts(\"a\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tOp:       Break,\n\t\t\t\t\tPatterns: litWords(\"2\"),\n\t\t\t\t\tStmts:    litStmts(\"b\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"case i in 1) a ;| 2) b ;; esac\"},\n\t\tlangFile(&CaseClause{\n\t\t\tWord: litWord(\"i\"),\n\t\t\tItems: []*CaseItem{\n\t\t\t\t{\n\t\t\t\t\tOp:       ResumeKorn,\n\t\t\t\t\tPatterns: litWords(\"1\"),\n\t\t\t\t\tStmts:    litStmts(\"a\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tOp:       Break,\n\t\t\t\t\tPatterns: litWords(\"2\"),\n\t\t\t\t\tStmts:    litStmts(\"b\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}, LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"case $i in 1) cat <<EOF ;;\\nfoo\\nEOF\\nesac\"},\n\t\tlangFile(&CaseClause{\n\t\t\tWord: word(litParamExp(\"i\")),\n\t\t\tItems: []*CaseItem{{\n\t\t\t\tOp:       Break,\n\t\t\t\tPatterns: litWords(\"1\"),\n\t\t\t\tStmts: []*Stmt{{\n\t\t\t\t\tCmd: litCall(\"cat\"),\n\t\t\t\t\tRedirs: []*Redirect{{\n\t\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\t\t\tHdoc: litWord(\"foo\\n\"),\n\t\t\t\t\t}},\n\t\t\t\t}},\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo | while read a; do b; done\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: Pipe,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY: stmt(&WhileClause{\n\t\t\t\tCond: []*Stmt{litStmt(\"read\", \"a\")},\n\n\t\t\t\tDo: litStmts(\"b\"),\n\t\t\t}),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"while read l; do foo || bar; done\"},\n\t\tlangFile(&WhileClause{\n\t\t\tCond: []*Stmt{litStmt(\"read\", \"l\")},\n\t\t\tDo: stmts(&BinaryCmd{\n\t\t\t\tOp: OrStmt,\n\t\t\t\tX:  litStmt(\"foo\"),\n\t\t\t\tY:  litStmt(\"bar\"),\n\t\t\t}),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"echo if while\"},\n\t\tlangFile(litCall(\"echo\", \"if\", \"while\")),\n\t),\n\tfileTest(\n\t\t[]string{\"${foo}if\"},\n\t\tlangFile(word(&ParamExp{Param: lit(\"foo\")}, lit(\"if\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"$if'|'\"},\n\t\tlangFile(word(litParamExp(\"if\"), sglQuoted(\"|\"))),\n\t),\n\tfileTest(\n\t\t[]string{\"if a; then b=; fi\", \"if a; then b=\\nfi\"},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tThen: stmts(&CallExpr{Assigns: litAssigns(\"b=\")}),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"if a; then >f; fi\", \"if a; then >f\\nfi\"},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tThen: []*Stmt{{\n\t\t\t\tRedirs: []*Redirect{\n\t\t\t\t\t{Op: RdrOut, Word: litWord(\"f\")},\n\t\t\t\t},\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"if a; then (a); fi\", \"if a; then (a) fi\"},\n\t\tlangFile(&IfClause{\n\t\t\tCond: litStmts(\"a\"),\n\t\t\tThen: stmts(subshell(litStmt(\"a\"))),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"a=b\\nc=d\", \"a=b; c=d\"},\n\t\tlangFile([]Command{\n\t\t\t&CallExpr{Assigns: litAssigns(\"a=b\")},\n\t\t\t&CallExpr{Assigns: litAssigns(\"c=d\")},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo && write | read\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: AndStmt,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY: stmt(&BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  litStmt(\"write\"),\n\t\t\t\tY:  litStmt(\"read\"),\n\t\t\t}),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"write | read && bar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: AndStmt,\n\t\t\tX: stmt(&BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  litStmt(\"write\"),\n\t\t\t\tY:  litStmt(\"read\"),\n\t\t\t}),\n\t\t\tY: litStmt(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo >f | bar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: Pipe,\n\t\t\tX: &Stmt{\n\t\t\t\tCmd: litCall(\"foo\"),\n\t\t\t\tRedirs: []*Redirect{\n\t\t\t\t\t{Op: RdrOut, Word: litWord(\"f\")},\n\t\t\t\t},\n\t\t\t},\n\t\t\tY: litStmt(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"(foo) >f | bar\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: Pipe,\n\t\t\tX: &Stmt{\n\t\t\t\tCmd: subshell(litStmt(\"foo\")),\n\t\t\t\tRedirs: []*Redirect{\n\t\t\t\t\t{Op: RdrOut, Word: litWord(\"f\")},\n\t\t\t\t},\n\t\t\t},\n\t\t\tY: litStmt(\"bar\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo | >f\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: Pipe,\n\t\t\tX:  litStmt(\"foo\"),\n\t\t\tY: &Stmt{Redirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"f\")},\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a ]]\"},\n\t\tlangFile(&TestClause{X: litWord(\"a\")}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(litStmt(\"[[\", \"a\", \"]]\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a ]]\\nb\"},\n\t\tlangFile(stmts(\n\t\t\t&TestClause{X: litWord(\"a\")},\n\t\t\tlitCall(\"b\"),\n\t\t), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a > b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsAfter,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ 1 -nt 2 ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsNewer,\n\t\t\tX:  litWord(\"1\"),\n\t\t\tY:  litWord(\"2\"),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ 1 -eq 2 ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsEql,\n\t\t\tX:  litWord(\"1\"),\n\t\t\tY:  litWord(\"2\"),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -1 -eq -1 ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsEql,\n\t\t\t// TODO: parse as unary expressions\n\t\t\tX: litWord(\"-1\"),\n\t\t\tY: litWord(\"-1\"),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ +3 -eq 1+2 ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsEql,\n\t\t\t// TODO: parse as unary and binary expressions\n\t\t\tX: litWord(\"+3\"),\n\t\t\tY: litWord(\"1+2\"),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"[[ -R a ]]\",\n\t\t\t\"[[\\n-R a\\n]]\",\n\t\t},\n\t\tlangFile(&TestClause{X: &UnaryTest{\n\t\t\tOp: TsRefVar,\n\t\t\tX:  litWord(\"a\"),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a =~ b ]]\", \"[[ a =~ b ]];\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsReMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a =~ \" foo \"$bar ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsReMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY: word(\n\t\t\t\tdblQuoted(lit(\" foo \")),\n\t\t\t\tlitParamExp(\"bar\"),\n\t\t\t),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a =~ foo\"bar\" ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsReMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY: word(\n\t\t\t\tlit(\"foo\"),\n\t\t\t\tdblQuoted(lit(\"bar\")),\n\t\t\t),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a =~ [ab](c |d) ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsReMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"[ab](c |d)\"),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a =~ ( ]]<>;&) ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsReMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"( ]]<>;&)\"),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a =~ ($foo) ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsReMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  word(lit(\"(\"), litParamExp(\"foo\"), lit(\")\")),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a =~ b\\ c|d ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsReMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(`b\\ c|d`),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a == -n ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"-n\"),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a == (b|c)* ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  word(lit(\"(b|c)\"), lit(\"*\")),\n\t\t}}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a == (#i)bar ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  word(lit(\"(#i)\"), lit(\"bar\")),\n\t\t}}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`[[ a =~ -n ]]`},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsReMatch,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"-n\"),\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a =~ b$ || c =~ d$ ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: OrTest,\n\t\t\tX: &BinaryTest{\n\t\t\t\tOp: TsReMatch,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  word(lit(\"b\"), lit(\"$\")),\n\t\t\t},\n\t\t\tY: &BinaryTest{\n\t\t\t\tOp: TsReMatch,\n\t\t\t\tX:  litWord(\"c\"),\n\t\t\t\tY:  word(lit(\"d\"), lit(\"$\")),\n\t\t\t},\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -n $a ]]\"},\n\t\tlangFile(&TestClause{\n\t\t\tX: &UnaryTest{Op: TsNempStr, X: word(litParamExp(\"a\"))},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ ! $a < 'b' ]]\"},\n\t\tlangFile(&TestClause{X: &UnaryTest{\n\t\t\tOp: TsNot,\n\t\t\tX: &BinaryTest{\n\t\t\t\tOp: TsBefore,\n\t\t\t\tX:  word(litParamExp(\"a\")),\n\t\t\t\tY:  word(sglQuoted(\"b\")),\n\t\t\t},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"[[ ! -e $a ]]\",\n\t\t\t\"[[ ! -a $a ]]\",\n\t\t\t\"[[\\n!\\n-a $a\\n]]\",\n\t\t},\n\t\tlangFile(&TestClause{X: &UnaryTest{\n\t\t\tOp: TsNot,\n\t\t\tX:  &UnaryTest{Op: TsExists, X: word(litParamExp(\"a\"))},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"[[ a && b ]]\",\n\t\t\t\"[[\\na &&\\nb ]]\",\n\t\t\t\"[[\\n\\na &&\\n\\nb ]]\",\n\t\t},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ (a && b) ]]\"},\n\t\tlangFile(&TestClause{X: parenTest(&BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  litWord(\"b\"),\n\t\t})}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"[[ a && (b) ]]\",\n\t\t\t\"[[ a &&\\n(\\nb) ]]\",\n\t\t},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY:  parenTest(litWord(\"b\")),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ (a && b) || -f c ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: OrTest,\n\t\t\tX: parenTest(&BinaryTest{\n\t\t\t\tOp: AndTest,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"b\"),\n\t\t\t}),\n\t\t\tY: &UnaryTest{Op: TsRegFile, X: litWord(\"c\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"[[ -S a && -L b ]]\",\n\t\t\t\"[[ -S a && -h b ]]\",\n\t\t},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsSocket, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsSmbLink, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -k a && -N b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsSticky, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsModif, X: litWord(\"b\")},\n\t\t}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -G a && -O b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsGrpOwn, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsUsrOwn, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -d a && -c b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsDirect, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsCharSp, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -b a && -p b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsBlckSp, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsNmPipe, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -g a && -u b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsGIDSet, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsUIDSet, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -r a && -w b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsRead, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsWrite, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -x a && -s b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsExec, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsNoEmpty, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -t a && -z b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsFdTerm, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsEmpStr, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ -o a && -v b ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX:  &UnaryTest{Op: TsOptSet, X: litWord(\"a\")},\n\t\t\tY:  &UnaryTest{Op: TsVarSet, X: litWord(\"b\")},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a -ot b && c -ef d ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX: &BinaryTest{\n\t\t\t\tOp: TsOlder,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"b\"),\n\t\t\t},\n\t\t\tY: &BinaryTest{\n\t\t\t\tOp: TsDevIno,\n\t\t\t\tX:  litWord(\"c\"),\n\t\t\t\tY:  litWord(\"d\"),\n\t\t\t},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a = b && c != d ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX: &BinaryTest{\n\t\t\t\tOp: TsMatchShort,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"b\"),\n\t\t\t},\n\t\t\tY: &BinaryTest{\n\t\t\t\tOp: TsNoMatch,\n\t\t\t\tX:  litWord(\"c\"),\n\t\t\t\tY:  litWord(\"d\"),\n\t\t\t},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a -ne b && c -le d ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX: &BinaryTest{\n\t\t\t\tOp: TsNeq,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"b\"),\n\t\t\t},\n\t\t\tY: &BinaryTest{\n\t\t\t\tOp: TsLeq,\n\t\t\t\tX:  litWord(\"c\"),\n\t\t\t\tY:  litWord(\"d\"),\n\t\t\t},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ c -ge d ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsGeq,\n\t\t\tX:  litWord(\"c\"),\n\t\t\tY:  litWord(\"d\"),\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ a -lt b && c -gt d ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: AndTest,\n\t\t\tX: &BinaryTest{\n\t\t\t\tOp: TsLss,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  litWord(\"b\"),\n\t\t\t},\n\t\t\tY: &BinaryTest{\n\t\t\t\tOp: TsGtr,\n\t\t\t\tX:  litWord(\"c\"),\n\t\t\t\tY:  litWord(\"d\"),\n\t\t\t},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"[[ 1.2 -gt 0.3 ]]\"},\n\t\tlangFile(&TestClause{X: &BinaryTest{\n\t\t\tOp: TsGtr,\n\t\t\tX:  litWord(\"1.2\"),\n\t\t\tY:  litWord(\"0.3\"),\n\t\t}}, LangZsh),\n\t\t// TODO: reject floating point here, just like with arithmetic expressions\n\t\t// langErr2(``, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"declare -f func\"},\n\t\tlangFile(litStmt(\"declare\", \"-f\", \"func\")),\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"declare\"),\n\t\t\tArgs: []*Assign{\n\t\t\t\t{Naked: true, Value: litWord(\"-f\")},\n\t\t\t\t{Naked: true, Name: lit(\"func\")},\n\t\t\t},\n\t\t}, langBashLike|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"(local bar)\"},\n\t\tlangFile(subshell(stmt(&DeclClause{\n\t\t\tVariant: lit(\"local\"),\n\t\t\tArgs:    litAssigns(\"bar\"),\n\t\t})), LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(subshell(litStmt(\"local\", \"bar\")), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"local {a,b}_c=1\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"local\"),\n\t\t\tArgs:    []*Assign{{Naked: true, Value: litWord(\"{a,b}_c=1\")}},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(litStmt(\"local\", \"{a,b}_c=1\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"typeset\"},\n\t\tlangFile(&DeclClause{Variant: lit(\"typeset\")}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(litStmt(\"typeset\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"export bar\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"export\"),\n\t\t\tArgs:    litAssigns(\"bar\"),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(litStmt(\"export\", \"bar\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"readonly -n\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"readonly\"),\n\t\t\tArgs:    []*Assign{{Naked: true, Value: litWord(\"-n\")}},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(litStmt(\"readonly\", \"-n\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"nameref bar=\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"nameref\"),\n\t\t\tArgs: []*Assign{{\n\t\t\t\tName: lit(\"bar\"),\n\t\t\t}},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(litStmt(\"nameref\", \"bar=\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"declare -a +n -b$o foo=bar\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"declare\"),\n\t\t\tArgs: []*Assign{\n\t\t\t\t{Naked: true, Value: litWord(\"-a\")},\n\t\t\t\t{Naked: true, Value: litWord(\"+n\")},\n\t\t\t\t{Naked: true, Value: word(lit(\"-b\"), litParamExp(\"o\"))},\n\t\t\t\t{Name: lit(\"foo\"), Value: litWord(\"bar\")},\n\t\t\t},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"declare -a foo=(b1 $(b2))\",\n\t\t\t\"declare -a foo=(b1 `b2`)\",\n\t\t},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"declare\"),\n\t\t\tArgs: []*Assign{\n\t\t\t\t{Naked: true, Value: litWord(\"-a\")},\n\t\t\t\t{\n\t\t\t\t\tName: lit(\"foo\"),\n\t\t\t\t\tArray: arrValues(\n\t\t\t\t\t\tlitWord(\"b1\"),\n\t\t\t\t\t\tword(cmdSubst(litStmt(\"b2\"))),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t}, LangBash),\n\t\tlangErr2(\"1:16: the `declare` builtin is a bash feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"local -a foo=(b1)\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"local\"),\n\t\t\tArgs: []*Assign{\n\t\t\t\t{Naked: true, Value: litWord(\"-a\")},\n\t\t\t\t{\n\t\t\t\t\tName:  lit(\"foo\"),\n\t\t\t\t\tArray: arrValues(litWord(\"b1\")),\n\t\t\t\t},\n\t\t\t},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"declare -A foo=([a]=b)\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"declare\"),\n\t\t\tArgs: []*Assign{\n\t\t\t\t{Naked: true, Value: litWord(\"-A\")},\n\t\t\t\t{\n\t\t\t\t\tName: lit(\"foo\"),\n\t\t\t\t\tArray: &ArrayExpr{Elems: []*ArrayElem{{\n\t\t\t\t\t\tIndex: litWord(\"a\"),\n\t\t\t\t\t\tValue: litWord(\"b\"),\n\t\t\t\t\t}}},\n\t\t\t\t},\n\t\t\t},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"declare foo[a]=\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"declare\"),\n\t\t\tArgs: []*Assign{{\n\t\t\t\tName:  lit(\"foo\"),\n\t\t\t\tIndex: litWord(\"a\"),\n\t\t\t}},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"declare foo[*]\"},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"declare\"),\n\t\t\tArgs: []*Assign{{\n\t\t\t\tName:  lit(\"foo\"),\n\t\t\t\tIndex: litWord(\"*\"),\n\t\t\t\tNaked: true,\n\t\t\t}},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`declare foo[\"x y\"]`},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"declare\"),\n\t\t\tArgs: []*Assign{{\n\t\t\t\tName:  lit(\"foo\"),\n\t\t\t\tIndex: word(dblQuoted(lit(\"x y\"))),\n\t\t\t\tNaked: true,\n\t\t\t}},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`declare foo['x y']`},\n\t\tlangFile(&DeclClause{\n\t\t\tVariant: lit(\"declare\"),\n\t\t\tArgs: []*Assign{{\n\t\t\t\tName:  lit(\"foo\"),\n\t\t\t\tIndex: word(sglQuoted(\"x y\")),\n\t\t\t\tNaked: true,\n\t\t\t}},\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"foo=([)\"},\n\t\tlangFile(&CallExpr{Assigns: []*Assign{{\n\t\t\tName:  lit(\"foo\"),\n\t\t\tArray: arrValues(litWord(\"[\")),\n\t\t}}}, LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"a && b=(c)\\nd\",\n\t\t\t\"a && b=(c); d\",\n\t\t},\n\t\tlangFile(stmts(\n\t\t\t&BinaryCmd{\n\t\t\t\tOp: AndStmt,\n\t\t\t\tX:  litStmt(\"a\"),\n\t\t\t\tY: stmt(&CallExpr{Assigns: []*Assign{{\n\t\t\t\t\tName:  lit(\"b\"),\n\t\t\t\t\tArray: arrValues(litWord(\"c\")),\n\t\t\t\t}}}),\n\t\t\t},\n\t\t\tlitCall(\"d\"),\n\t\t), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"declare -f $func >/dev/null\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd: &DeclClause{\n\t\t\t\tVariant: lit(\"declare\"),\n\t\t\t\tArgs: []*Assign{\n\t\t\t\t\t{Naked: true, Value: litWord(\"-f\")},\n\t\t\t\t\t{\n\t\t\t\t\t\tNaked: true,\n\t\t\t\t\t\tValue: word(litParamExp(\"func\")),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tRedirs: []*Redirect{\n\t\t\t\t{Op: RdrOut, Word: litWord(\"/dev/null\")},\n\t\t\t},\n\t\t}, LangBash|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"declare a\\n{ x; }\"},\n\t\tlangFile(stmts(\n\t\t\t&DeclClause{\n\t\t\t\tVariant: lit(\"declare\"),\n\t\t\t\tArgs:    litAssigns(\"a\"),\n\t\t\t},\n\t\t\tblock(litStmt(\"x\")),\n\t\t), LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"eval a=b foo\"},\n\t\tlangFile(litStmt(\"eval\", \"a=b\", \"foo\")),\n\t),\n\tfileTest(\n\t\t[]string{\"time\", \"time\\n\"},\n\t\tlangFile(litStmt(\"time\"), LangPOSIX),\n\t\tlangFile(&TimeClause{}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"time -p\"},\n\t\tlangFile(litStmt(\"time\", \"-p\"), LangPOSIX),\n\t\tlangFile(&TimeClause{PosixFormat: true}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"time -a\"},\n\t\tlangFile(litStmt(\"time\", \"-a\"), LangPOSIX),\n\t\tlangFile(&TimeClause{Stmt: litStmt(\"-a\")}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"time --\"},\n\t\tlangFile(litStmt(\"time\", \"--\"), LangPOSIX),\n\t\tlangFile(&TimeClause{Stmt: litStmt(\"--\")}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"time foo\"},\n\t\tlangFile(&TimeClause{Stmt: litStmt(\"foo\")}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"time { foo; }\"},\n\t\tlangFile(&TimeClause{Stmt: stmt(block(litStmt(\"foo\")))}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"time\\nfoo\"},\n\t\tlangFile([]*Stmt{\n\t\t\tstmt(&TimeClause{}),\n\t\t\tlitStmt(\"foo\"),\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"coproc foo bar\"},\n\t\tlangFile(litStmt(\"coproc\", \"foo\", \"bar\")),\n\t\tlangFile(&CoprocClause{Stmt: litStmt(\"foo\", \"bar\")}, langBashLike),\n\t),\n\tfileTest(\n\t\t[]string{\"coproc name { foo; }\"},\n\t\tlangFile(&CoprocClause{\n\t\t\tName: litWord(\"name\"),\n\t\t\tStmt: stmt(block(litStmt(\"foo\"))),\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"coproc $namevar { foo; }\"},\n\t\tlangFile(&CoprocClause{\n\t\t\tName: word(litParamExp(\"namevar\")),\n\t\t\tStmt: stmt(block(litStmt(\"foo\"))),\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"coproc foo\", \"coproc foo;\"},\n\t\tlangFile(&CoprocClause{Stmt: litStmt(\"foo\")}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"coproc { foo; }\"},\n\t\tlangFile(&CoprocClause{\n\t\t\tStmt: stmt(block(litStmt(\"foo\"))),\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"coproc (foo)\"},\n\t\tlangFile(&CoprocClause{\n\t\t\tStmt: stmt(subshell(litStmt(\"foo\"))),\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"coproc name foo | bar\"},\n\t\tlangFile(&CoprocClause{\n\t\t\tName: litWord(\"name\"),\n\t\t\tStmt: stmt(&BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX:  litStmt(\"foo\"),\n\t\t\t\tY:  litStmt(\"bar\"),\n\t\t\t}),\n\t\t}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"coproc $()\", \"coproc ``\"},\n\t\tlangFile(&CoprocClause{Stmt: stmt(call(\n\t\t\tword(cmdSubst()),\n\t\t))}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`let i++`},\n\t\tlangFile(letClause(\n\t\t\t&UnaryArithm{Op: Inc, Post: true, X: litWord(\"i\")},\n\t\t), LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(litStmt(\"let\", \"i++\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`let a++ b++ c +d`},\n\t\tlangFile(letClause(\n\t\t\t&UnaryArithm{Op: Inc, Post: true, X: litWord(\"a\")},\n\t\t\t&UnaryArithm{Op: Inc, Post: true, X: litWord(\"b\")},\n\t\t\tlitWord(\"c\"),\n\t\t\t&UnaryArithm{Op: Plus, X: litWord(\"d\")},\n\t\t), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`let ++i >/dev/null`},\n\t\tlangFile(&Stmt{\n\t\t\tCmd:    letClause(&UnaryArithm{Op: Inc, X: litWord(\"i\")}),\n\t\t\tRedirs: []*Redirect{{Op: RdrOut, Word: litWord(\"/dev/null\")}},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`let a=(1 + 2) b=3+4`,\n\t\t\t`let a=(1+2) b=3+4`,\n\t\t},\n\t\tlangFile(letClause(\n\t\t\t&BinaryArithm{\n\t\t\t\tOp: Assgn,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY: parenArit(&BinaryArithm{\n\t\t\t\t\tOp: Add,\n\t\t\t\t\tX:  litWord(\"1\"),\n\t\t\t\t\tY:  litWord(\"2\"),\n\t\t\t\t}),\n\t\t\t},\n\t\t\t&BinaryArithm{\n\t\t\t\tOp: Assgn,\n\t\t\t\tX:  litWord(\"b\"),\n\t\t\t\tY: &BinaryArithm{\n\t\t\t\t\tOp: Add,\n\t\t\t\t\tX:  litWord(\"3\"),\n\t\t\t\t\tY:  litWord(\"4\"),\n\t\t\t\t},\n\t\t\t},\n\t\t), LangBash),\n\t\tlangErr2(\"1:7: the `let` builtin is a bash feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`let a=$(echo 3)`,\n\t\t\t\"let a=`echo 3`\",\n\t\t},\n\t\tlangFile(letClause(\n\t\t\t&BinaryArithm{\n\t\t\t\tOp: Assgn,\n\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\tY:  word(cmdSubst(litStmt(\"echo\", \"3\"))),\n\t\t\t},\n\t\t), LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"(foo-bar)\"},\n\t\tlangFile(subshell(litStmt(\"foo-bar\"))),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"let i++\\nbar\",\n\t\t\t\"let i++ \\nbar\",\n\t\t\t\"let i++; bar\",\n\t\t},\n\t\tlangFile(stmts(\n\t\t\tletClause(&UnaryArithm{\n\t\t\t\tOp:   Inc,\n\t\t\t\tPost: true,\n\t\t\t\tX:    litWord(\"i\"),\n\t\t\t}),\n\t\t\tlitCall(\"bar\"),\n\t\t), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"let i++\\nfoo=(bar)\",\n\t\t\t\"let i++; foo=(bar)\",\n\t\t\t\"let i++; foo=(bar)\\n\",\n\t\t},\n\t\tlangFile(stmts(\n\t\t\tletClause(&UnaryArithm{\n\t\t\t\tOp:   Inc,\n\t\t\t\tPost: true,\n\t\t\t\tX:    litWord(\"i\"),\n\t\t\t}),\n\t\t\t&CallExpr{Assigns: []*Assign{{\n\t\t\t\tName:  lit(\"foo\"),\n\t\t\t\tArray: arrValues(litWord(\"bar\")),\n\t\t\t}}},\n\t\t), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"case a in b) let i++ ;; esac\",\n\t\t\t\"case a in b) let i++;; esac\",\n\t\t},\n\t\tlangFile(&CaseClause{\n\t\t\tWord: word(lit(\"a\")),\n\t\t\tItems: []*CaseItem{{\n\t\t\t\tOp:       Break,\n\t\t\t\tPatterns: litWords(\"b\"),\n\t\t\t\tStmts: stmts(letClause(&UnaryArithm{\n\t\t\t\t\tOp:   Inc,\n\t\t\t\t\tPost: true,\n\t\t\t\t\tX:    litWord(\"i\"),\n\t\t\t\t})),\n\t\t\t}},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"a+=1\"},\n\t\tlangFile(&CallExpr{\n\t\t\tAssigns: []*Assign{{\n\t\t\t\tAppend: true,\n\t\t\t\tName:   lit(\"a\"),\n\t\t\t\tValue:  litWord(\"1\"),\n\t\t\t}},\n\t\t}, LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(litStmt(\"a+=1\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"b+=(2 3)\"},\n\t\tlangFile(&CallExpr{Assigns: []*Assign{{\n\t\t\tAppend: true,\n\t\t\tName:   lit(\"b\"),\n\t\t\tArray:  arrValues(litWords(\"2\", \"3\")...),\n\t\t}}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"a[1]=(b c)\"},\n\t\tlangFile(&CallExpr{Assigns: []*Assign{{\n\t\t\tName:  lit(\"a\"),\n\t\t\tIndex: litWord(\"1\"),\n\t\t\tArray: arrValues(litWords(\"b\", \"c\")...),\n\t\t}}}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"a[2]=b c[-3]= d[x]+=e\"},\n\t\tlangFile(litStmt(\"a[2]=b\", \"c[-3]=\", \"d[x]+=e\"), LangPOSIX),\n\t\tlangFile(&CallExpr{Assigns: []*Assign{\n\t\t\t{\n\t\t\t\tName:  lit(\"a\"),\n\t\t\t\tIndex: litWord(\"2\"),\n\t\t\t\tValue: litWord(\"b\"),\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: lit(\"c\"),\n\t\t\t\tIndex: &UnaryArithm{\n\t\t\t\t\tOp: Minus,\n\t\t\t\t\tX:  litWord(\"3\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:   lit(\"d\"),\n\t\t\t\tIndex:  litWord(\"x\"),\n\t\t\t\tAppend: true,\n\t\t\t\tValue:  litWord(\"e\"),\n\t\t\t},\n\t\t}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"*[i]=x\"},\n\t\tlangFile(word(lit(\"*\"), lit(\"[i]=x\")), LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangFile(lit(\"*[i]=x\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\"arr[0,1]=x\"},\n\t\tlangFile(&CallExpr{Assigns: []*Assign{{\n\t\t\tName: lit(\"arr\"),\n\t\t\tIndex: &BinaryArithm{\n\t\t\t\tOp: Comma,\n\t\t\t\tX:  litWord(\"0\"),\n\t\t\t\tY:  litWord(\"1\"),\n\t\t\t},\n\t\t\tValue: litWord(\"x\"),\n\t\t}}}, LangZsh),\n\t\tlangFile(lit(\"arr[0,1]=x\"), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"b[i]+=2\",\n\t\t\t\"b[ i ]+=2\",\n\t\t},\n\t\tlangFile(&CallExpr{Assigns: []*Assign{{\n\t\t\tAppend: true,\n\t\t\tName:   lit(\"b\"),\n\t\t\tIndex:  litWord(\"i\"),\n\t\t\tValue:  litWord(\"2\"),\n\t\t}}}, LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`$((a + \"b + $c\"))`},\n\t\tlangFile(arithmExp(&BinaryArithm{\n\t\t\tOp: Add,\n\t\t\tX:  litWord(\"a\"),\n\t\t\tY: word(dblQuoted(\n\t\t\t\tlit(\"b + \"),\n\t\t\t\tlitParamExp(\"c\"),\n\t\t\t)),\n\t\t})),\n\t),\n\tfileTest(\n\t\t[]string{`let 'i++'`},\n\t\tlangFile(letClause(word(sglQuoted(\"i++\"))), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`echo ${a[\"x y\"]}`},\n\t\tlangFile(call(litWord(\"echo\"), word(&ParamExp{\n\t\t\tParam: lit(\"a\"),\n\t\t\tIndex: word(dblQuoted(lit(\"x y\"))),\n\t\t})), LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`a[$\"x y\"]=b`,\n\t\t\t`a[ $\"x y\" ]=b`,\n\t\t},\n\t\tlangFile(&CallExpr{Assigns: []*Assign{{\n\t\t\tName: lit(\"a\"),\n\t\t\tIndex: word(&DblQuoted{Dollar: true, Parts: []WordPart{\n\t\t\t\tlit(\"x y\"),\n\t\t\t}}),\n\t\t\tValue: litWord(\"b\"),\n\t\t}}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{`((a[\"x y\"] = b))`, `((a[\"x y\"]=b))`},\n\t\tlangFile(arithmCmd(&BinaryArithm{\n\t\t\tOp: Assgn,\n\t\t\tX: word(&ParamExp{\n\t\t\t\tShort: true,\n\t\t\t\tParam: lit(\"a\"),\n\t\t\t\tIndex: word(dblQuoted(lit(\"x y\"))),\n\t\t\t}),\n\t\t\tY: litWord(\"b\"),\n\t\t}), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t`a=([\"x y\"]=b)`,\n\t\t\t`a=( [ \"x y\" ]=b)`,\n\t\t},\n\t\tlangFile(&CallExpr{Assigns: []*Assign{{\n\t\t\tName: lit(\"a\"),\n\t\t\tArray: &ArrayExpr{Elems: []*ArrayElem{{\n\t\t\t\tIndex: word(dblQuoted(lit(\"x y\"))),\n\t\t\t\tValue: litWord(\"b\"),\n\t\t\t}}},\n\t\t}}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"a=([x]= [y]=)\",\n\t\t\t\"a=(\\n[x]=\\n[y]=\\n)\",\n\t\t},\n\t\tlangFile(&CallExpr{Assigns: []*Assign{{\n\t\t\tName: lit(\"a\"),\n\t\t\tArray: &ArrayExpr{Elems: []*ArrayElem{\n\t\t\t\t{Index: litWord(\"x\")},\n\t\t\t\t{Index: litWord(\"y\")},\n\t\t\t}},\n\t\t}}}, LangBash),\n\t),\n\tfileTest(\n\t\t[]string{\"a]b\"},\n\t\tlangFile(litStmt(\"a]b\")),\n\t),\n\tfileTest(\n\t\t[]string{\"echo a[b c[de]f\"},\n\t\tlangFile(litStmt(\"echo\", \"a[b\", \"c[de]f\"), LangPOSIX),\n\t\tlangFile(call(litWord(\"echo\"),\n\t\t\tword(lit(\"a\"), lit(\"[b\")),\n\t\t\tword(lit(\"c\"), lit(\"[de]f\")),\n\t\t), LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"<<EOF | b\\nfoo\\nEOF\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: Pipe,\n\t\t\tX: &Stmt{Redirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"foo\\n\"),\n\t\t\t}}},\n\t\t\tY: litStmt(\"b\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"<<EOF1 <<EOF2 | c && d\\nEOF1\\nEOF2\"},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: AndStmt,\n\t\t\tX: stmt(&BinaryCmd{\n\t\t\t\tOp: Pipe,\n\t\t\t\tX: &Stmt{Redirs: []*Redirect{\n\t\t\t\t\t{Op: Hdoc, Word: litWord(\"EOF1\")},\n\t\t\t\t\t{Op: Hdoc, Word: litWord(\"EOF2\")},\n\t\t\t\t}},\n\t\t\t\tY: litStmt(\"c\"),\n\t\t\t}),\n\t\t\tY: litStmt(\"d\"),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"<<EOF && { bar; }\\nhdoc\\nEOF\",\n\t\t\t\"<<EOF &&\\nhdoc\\nEOF\\n{ bar; }\",\n\t\t},\n\t\tlangFile(&BinaryCmd{\n\t\t\tOp: AndStmt,\n\t\t\tX: &Stmt{Redirs: []*Redirect{{\n\t\t\t\tOp:   Hdoc,\n\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\tHdoc: litWord(\"hdoc\\n\"),\n\t\t\t}}},\n\t\t\tY: stmt(block(litStmt(\"bar\"))),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo() {\\n\\t<<EOF && { bar; }\\nhdoc\\nEOF\\n}\"},\n\t\tlangFile(&FuncDecl{\n\t\t\tParens: true,\n\t\t\tName:   lit(\"foo\"),\n\t\t\tBody: stmt(block(stmt(&BinaryCmd{\n\t\t\t\tOp: AndStmt,\n\t\t\t\tX: &Stmt{Redirs: []*Redirect{{\n\t\t\t\t\tOp:   Hdoc,\n\t\t\t\t\tWord: litWord(\"EOF\"),\n\t\t\t\t\tHdoc: litWord(\"hdoc\\n\"),\n\t\t\t\t}}},\n\t\t\t\tY: stmt(block(litStmt(\"bar\"))),\n\t\t\t}))),\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{`\"a$(\"\")\"`, \"\\\"a`\\\"\\\"`\\\"\"},\n\t\tlangFile(dblQuoted(\n\t\t\tlit(\"a\"),\n\t\t\tcmdSubst(stmt(call(\n\t\t\t\tword(dblQuoted()),\n\t\t\t))),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"echo ?(b)*(c)+(d)@(e)!(f)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(\n\t\t\t&ExtGlob{Op: GlobZeroOrOne, Pattern: lit(\"b\")},\n\t\t\t&ExtGlob{Op: GlobZeroOrMore, Pattern: lit(\"c\")},\n\t\t\t&ExtGlob{Op: GlobOneOrMore, Pattern: lit(\"d\")},\n\t\t\t&ExtGlob{Op: GlobOne, Pattern: lit(\"e\")},\n\t\t\t&ExtGlob{Op: GlobExcept, Pattern: lit(\"f\")},\n\t\t)), LangBash|LangMirBSDKorn),\n\t\tlangFile(call(litWord(\"echo\"), word(\n\t\t\tlit(\"?\"), lit(\"(b)\"),\n\t\t\tlit(\"*\"), lit(\"(c)\"),\n\t\t\tlit(\"+\"), lit(\"(d)\"),\n\t\t\tlit(\"@\"), lit(\"(e)\"),\n\t\t\tlit(\"!\"), lit(\"(f)\"),\n\t\t)), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo foo@(b*(c|d))bar\"},\n\t\tlangFile(call(litWord(\"echo\"), word(\n\t\t\tlit(\"foo\"),\n\t\t\t&ExtGlob{Op: GlobOne, Pattern: lit(\"b*(c|d)\")},\n\t\t\tlit(\"bar\"),\n\t\t)), LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"echo $a@(b)$c?(d)$e*(f)$g+(h)$i!(j)$k\"},\n\t\tlangFile(call(litWord(\"echo\"), word(\n\t\t\tlitParamExp(\"a\"),\n\t\t\t&ExtGlob{Op: GlobOne, Pattern: lit(\"b\")},\n\t\t\tlitParamExp(\"c\"),\n\t\t\t&ExtGlob{Op: GlobZeroOrOne, Pattern: lit(\"d\")},\n\t\t\tlitParamExp(\"e\"),\n\t\t\t&ExtGlob{Op: GlobZeroOrMore, Pattern: lit(\"f\")},\n\t\t\tlitParamExp(\"g\"),\n\t\t\t&ExtGlob{Op: GlobOneOrMore, Pattern: lit(\"h\")},\n\t\t\tlitParamExp(\"i\"),\n\t\t\t&ExtGlob{Op: GlobExcept, Pattern: lit(\"j\")},\n\t\t\tlitParamExp(\"k\"),\n\t\t)), LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{`\"^$1[[:space:]]\"`},\n\t\tlangFile(dblQuoted(\n\t\t\tlit(\"^\"),\n\t\t\tlitParamExp(\"1\"),\n\t\t\tlit(\"[[:space:]]\"),\n\t\t)),\n\t),\n\t// Zsh glob qualifiers are parsed as part of the word.\n\tfileTest(\n\t\t[]string{\"echo *(.)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\"*\"), lit(\"(.)\"))), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo **(/)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\"**\"), lit(\"(/)\"))), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo *.txt(@)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\"*.txt\"), lit(\"(@)\"))), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo *(om[1,5])\"},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\"*\"), lit(\"(om[1,5])\"))), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo /bin/sh(:t)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\"/bin/sh\"), lit(\"(:t)\"))), LangZsh),\n\t),\n\t// Zsh glob qualifiers on words without glob metacharacters or slashes.\n\tfileTest(\n\t\t[]string{\"echo .(:a)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\".\"), lit(\"(:a)\"))), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo ~(:t)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\"~\"), lit(\"(:t)\"))), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`echo {go.mod,shfmt}(N*)`},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\"{go.mod,shfmt}\"), lit(\"(N*)\"))), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo $var(Nms-3)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(litParamExp(\"var\"), lit(\"(Nms-3)\"))), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo a(A) b(B)\"},\n\t\tlangFile(call(litWord(\"echo\"), word(lit(\"a\"), lit(\"(A)\")), word(lit(\"b\"), lit(\"(B)\"))), LangZsh),\n\t),\n\t// Zsh numeric range globs.\n\tfileTest(\n\t\t[]string{\"echo <->\"},\n\t\tlangFile(litCall(\"echo\", \"<->\"), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo <5-10>\"},\n\t\tlangFile(litCall(\"echo\", \"<5-10>\"), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo foo<->.txt\"},\n\t\tlangFile(litCall(\"echo\", \"foo<->.txt\"), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"echo <5->.*\"},\n\t\tlangFile(litCall(\"echo\", \"<5->.*\"), LangZsh),\n\t),\n\tfileTest(\n\t\t// <2-3 is a redirect, not a numeric range glob (no closing >).\n\t\t[]string{\"echo <2-3\"},\n\t\tlangFile(&Stmt{\n\t\t\tCmd:    litCall(\"echo\"),\n\t\t\tRedirs: []*Redirect{{Op: RdrIn, Word: litWord(\"2-3\")}},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"@test \\\"desc\\\" { body; }\"},\n\t\tlangFile(&TestDecl{\n\t\t\tDescription: word(dblQuoted(lit(\"desc\"))),\n\t\t\tBody:        stmt(block(litStmt(\"body\"))),\n\t\t}, LangBats),\n\t),\n\tfileTest(\n\t\t[]string{\"@test 'desc' {\\n\\tmultiple\\n\\tstatements\\n}\"},\n\t\tlangFile(&TestDecl{\n\t\t\tDescription: word(sglQuoted(\"desc\")),\n\t\t\tBody:        stmt(block(litStmts(\"multiple\", \"statements\")...)),\n\t\t}, LangBats),\n\t),\n\tfileTest(\n\t\t[]string{\"${+foo}\"},\n\t\tlangFile(&ParamExp{\n\t\t\tIsSet: true,\n\t\t\tParam: lit(\"foo\"),\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$+foo $#bar\"},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tShort: true,\n\t\t\t\tIsSet: true,\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tShort:  true,\n\t\t\t\tLength: true,\n\t\t\t\tParam:  lit(\"bar\"),\n\t\t\t}),\n\t\t), LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"${${foo#head}%tail}\"},\n\t\tlangFile(&ParamExp{\n\t\t\tNestedParam: &ParamExp{\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t\tExp: &Expansion{\n\t\t\t\t\tOp:   RemSmallPrefix,\n\t\t\t\t\tWord: litWord(\"head\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   RemSmallSuffix,\n\t\t\t\tWord: litWord(\"tail\"),\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{`${#\"${foo}\"}`},\n\t\tlangFile(&ParamExp{\n\t\t\tLength:      true,\n\t\t\tNestedParam: dblQuoted(&ParamExp{Param: lit(\"foo\")}),\n\t\t}, LangZsh),\n\t\tflipConfirm2(LangZsh), // TODO: why is this a bad substitution in zsh?\n\t),\n\tfileTest(\n\t\t[]string{\"${$(echo footail)%tail}\"},\n\t\tlangFile(&ParamExp{\n\t\t\tNestedParam: cmdSubst(litStmt(\"echo\", \"footail\")),\n\t\t\tExp: &Expansion{\n\t\t\t\tOp:   RemSmallSuffix,\n\t\t\t\tWord: litWord(\"tail\"),\n\t\t\t},\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"${foo:u}\"},\n\t\tlangFile(&ParamExp{\n\t\t\tParam:     lit(\"foo\"),\n\t\t\tModifiers: lits(\"u\"),\n\t\t}, LangZsh),\n\t\tlangFile(&ParamExp{\n\t\t\tParam: lit(\"foo\"),\n\t\t\tSlice: &Slice{Offset: litWord(\"u\")},\n\t\t}, LangBash|LangMirBSDKorn),\n\t),\n\tfileTest(\n\t\t[]string{\"${foo:t5:h2:l}\"},\n\t\tlangFile(&ParamExp{\n\t\t\tParam:     lit(\"foo\"),\n\t\t\tModifiers: lits(\"t5\", \"h2\", \"l\"),\n\t\t}, LangZsh),\n\t),\n\tfileTest(\n\t\t[]string{\"$${foo}\"},\n\t\tlangFile(word(\n\t\t\tlitParamExp(\"$\"),\n\t\t\tlit(\"{foo}\"),\n\t\t)),\n\t),\n\tfileTest(\n\t\t[]string{\"${(aO)foo} ${(s/x/)foo}\"},\n\t\tlangFile(call(\n\t\t\tword(&ParamExp{\n\t\t\t\tFlags: lit(\"aO\"),\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t}),\n\t\t\tword(&ParamExp{\n\t\t\t\tFlags: lit(\"s/x/\"),\n\t\t\t\tParam: lit(\"foo\"),\n\t\t\t}),\n\t\t), LangZsh),\n\t),\n}\n\n// these don't have a canonical format with the same syntax tree\nvar fileTestsNoPrint = []fileTestCase{\n\tfileTest(\n\t\t[]string{`$[foo]`},\n\t\tlangFile(word(lit(\"$\"), lit(\"[foo]\")), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`\"$[foo]\"`},\n\t\tlangFile(dblQuoted(lit(\"$\"), lit(\"[foo]\")), LangPOSIX),\n\t),\n\tfileTest(\n\t\t[]string{`\"$[1 + 3]\"`},\n\t\tlangFile(dblQuoted(arithmExpBr(&BinaryArithm{\n\t\t\tOp: Add,\n\t\t\tX:  litWord(\"1\"),\n\t\t\tY:  litWord(\"3\"),\n\t\t})), LangBash),\n\t),\n}\n\n// these parse with comments\nvar fileTestsKeepComments = []fileTestCase{\n\tfileTest(\n\t\t[]string{\"# foo\\ncmd\\n# bar\"},\n\t\tlangFile(&File{\n\t\t\tStmts: []*Stmt{{\n\t\t\t\tComments: []Comment{{Text: \" foo\"}},\n\t\t\t\tCmd:      litCall(\"cmd\"),\n\t\t\t}},\n\t\t\tLast: []Comment{{Text: \" bar\"}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\"foo # bar # baz\"},\n\t\tlangFile(&File{\n\t\t\tStmts: []*Stmt{{\n\t\t\t\tComments: []Comment{{Text: \" bar # baz\"}},\n\t\t\t\tCmd:      litCall(\"foo\"),\n\t\t\t}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"$(\\n\\t# foo\\n)\",\n\t\t\t\"`\\n\\t# foo\\n`\",\n\t\t\t\"`# foo\\n`\",\n\t\t},\n\t\tlangFile(&CmdSubst{\n\t\t\tLast: []Comment{{Text: \" foo\"}},\n\t\t}),\n\t),\n\tfileTest(\n\t\t[]string{\n\t\t\t\"`# foo`\",\n\t\t\t\"` # foo`\",\n\t\t},\n\t\tlangFile(&CmdSubst{\n\t\t\tLast: []Comment{{Text: \" foo\"}},\n\t\t}),\n\t),\n}\n\ntype sanityChecker struct {\n\ttb   testing.TB\n\tsrc  string\n\tfile *File // nil if not checking a whole file\n}\n\nfunc (c sanityChecker) checkPos(node Node, pos Pos, strs ...string) {\n\tif !pos.IsValid() {\n\t\tc.tb.Fatalf(\"invalid Pos in %T\", node)\n\t}\n\toffs := pos.Offset()\n\tif offs > uint(len(c.src)) {\n\t\tc.tb.Errorf(\"Pos offset %d in %T is out of bounds in %q\",\n\t\t\toffs, node, c.src)\n\t\treturn\n\t}\n\tif len(strs) == 0 {\n\t\treturn\n\t}\n\tif strings.Contains(c.src, \"<<-\") {\n\t\t// since the tab indentation in <<- heredoc bodies\n\t\t// aren't part of the final literals\n\t\treturn\n\t}\n\tvar gotErr string\n\tfor i, want := range strs {\n\t\tgot := c.src[offs:]\n\t\tif i == 0 {\n\t\t\tgotErr = got\n\t\t}\n\t\tgot = strings.ReplaceAll(got, \"\\x00\", \"\")\n\t\tgot = strings.ReplaceAll(got, \"\\r\\n\", \"\\n\")\n\t\tif !strings.Contains(want, \"\\\\\\n\") {\n\t\t\t// Hack to let \"foobar\" match the input \"foo\\\\\\nbar\".\n\t\t\tgot = strings.ReplaceAll(got, \"\\\\\\n\", \"\")\n\t\t}\n\t\tif strings.HasPrefix(got, want) {\n\t\t\treturn\n\t\t}\n\t}\n\tc.tb.Errorf(\"Expected one of %q at %s in %q, found %q\",\n\t\tstrs, pos, c.src, gotErr)\n}\n\nfunc (c sanityChecker) visit(node Node) bool {\n\tif node == nil {\n\t\treturn true\n\t}\n\tif f := c.file; f != nil {\n\t\tif !node.Pos().IsValid() && len(f.Stmts) > 0 {\n\t\t\tc.tb.Fatalf(\"Invalid Pos\")\n\t\t}\n\t}\n\tif node.Pos().After(node.End()) {\n\t\tc.tb.Errorf(\"Found End() before Pos() in %T\", node)\n\t}\n\tswitch node := node.(type) {\n\tcase *Comment:\n\t\tif f := c.file; f != nil {\n\t\t\tif f.Pos().After(node.Pos()) {\n\t\t\t\tc.tb.Fatalf(\"A Comment is before its File\")\n\t\t\t}\n\t\t\tif node.End().After(f.End()) {\n\t\t\t\tc.tb.Fatalf(\"A Comment is after its File\")\n\t\t\t}\n\t\t}\n\t\tc.checkPos(node, node.Hash, \"#\"+node.Text)\n\tcase *Stmt:\n\t\tendOff := int(node.End().Offset())\n\t\tif endOff < len(c.src) {\n\t\t\tend := c.src[endOff]\n\t\t\tswitch {\n\t\t\tcase end == ' ', end == '\\n', end == '\\t', end == '\\r':\n\t\t\t\t// ended by whitespace\n\t\t\tcase regOps(rune(end)):\n\t\t\t\t// ended by end character\n\t\t\tcase endOff > 0 && c.src[endOff-1] == ';':\n\t\t\t\t// ended by semicolon\n\t\t\tcase endOff > 0 && c.src[endOff-1] == '&':\n\t\t\t\t// ended by & or |&\n\t\t\tcase end == '\\\\' && c.src[endOff+1] == '`':\n\t\t\t\t// ended by an escaped backquote\n\t\t\tdefault:\n\t\t\t\tc.tb.Errorf(\"Unexpected Stmt.End() %d %q in %q\",\n\t\t\t\t\tendOff, end, c.src)\n\t\t\t}\n\t\t}\n\t\tif c.src[node.Position.Offset()] == '#' {\n\t\t\tc.tb.Errorf(\"Stmt.Pos() should not be a comment\")\n\t\t}\n\t\tc.checkPos(node, node.Position)\n\t\tif node.Semicolon.IsValid() {\n\t\t\tc.checkPos(node, node.Semicolon, \";\", \"&\", \"|&\")\n\t\t}\n\t\tfor _, r := range node.Redirs {\n\t\t\tstrs := []string{r.Op.String()}\n\t\t\t// Zsh supports alternate forms of redirect operators:\n\t\t\t// ! instead of |, and >& or >>& prefixes instead of &> and &>>.\n\t\t\tswitch r.Op {\n\t\t\tcase RdrClob:\n\t\t\t\tstrs = append(strs, \">!\")\n\t\t\tcase AppClob:\n\t\t\t\tstrs = append(strs, \">>!\")\n\t\t\tcase RdrAllClob:\n\t\t\t\tstrs = append(strs, \"&>!\", \">&|\", \">&!\")\n\t\t\tcase AppAll:\n\t\t\t\tstrs = append(strs, \">>&\")\n\t\t\tcase AppAllClob:\n\t\t\t\tstrs = append(strs, \"&>>!\", \">>&|\", \">>&!\")\n\t\t\t}\n\t\t\tc.checkPos(node, r.OpPos, strs...)\n\t\t}\n\tcase *Lit:\n\t\tpos, end := int(node.Pos().Offset()), int(node.End().Offset())\n\t\twant := pos + len(node.Value)\n\t\tval := node.Value\n\t\tposLine := node.Pos().Line()\n\t\tendLine := node.End().Line()\n\t\tswitch {\n\t\tcase strings.Contains(c.src, \"\\\\\\n\"), strings.Contains(c.src, \"\\\\\\r\\n\"):\n\t\tcase !strings.Contains(node.Value, \"\\n\") && posLine != endLine:\n\t\t\tc.tb.Errorf(\"Lit without newlines has Pos/End lines %d and %d\",\n\t\t\t\tposLine, endLine)\n\t\tcase strings.Contains(c.src, \"`\") && strings.Contains(c.src, \"\\\\\"):\n\t\t\t// removed backslashes inside backquote cmd substs\n\t\t\tval = \"\"\n\t\tcase end < len(c.src) && (c.src[end] == '\\n' || c.src[end] == '`'):\n\t\t\t// heredoc literals that end with the\n\t\t\t// stop word and a newline or closing backquote\n\t\tcase end == len(c.src):\n\t\t\t// same as above, but with word and EOF\n\t\tcase end != want:\n\t\t\tc.tb.Errorf(\"Unexpected Lit %q End() %d (wanted %d for pos %d) in %q\",\n\t\t\t\tval, end, want, pos, c.src)\n\t\t}\n\t\tc.checkPos(node, node.ValuePos, val)\n\t\tc.checkPos(node, node.ValueEnd)\n\tcase *Subshell:\n\t\tc.checkPos(node, node.Lparen, \"(\")\n\t\tc.checkPos(node, node.Rparen, \")\")\n\tcase *Block:\n\t\tc.checkPos(node, node.Lbrace, \"{\")\n\t\tc.checkPos(node, node.Rbrace, \"}\")\n\tcase *IfClause:\n\t\tif node.ThenPos.IsValid() {\n\t\t\tc.checkPos(node, node.Position, \"if\", \"elif\")\n\t\t\tc.checkPos(node, node.ThenPos, \"then\")\n\t\t} else {\n\t\t\tc.checkPos(node, node.Position, \"else\")\n\t\t}\n\t\tc.checkPos(node, node.FiPos, \"fi\")\n\tcase *WhileClause:\n\t\trsrv := \"while\"\n\t\tif node.Until {\n\t\t\trsrv = \"until\"\n\t\t}\n\t\tc.checkPos(node, node.WhilePos, rsrv)\n\t\tc.checkPos(node, node.DoPos, \"do\")\n\t\tc.checkPos(node, node.DonePos, \"done\")\n\tcase *ForClause:\n\t\tif node.Select {\n\t\t\tc.checkPos(node, node.ForPos, \"select\")\n\t\t} else {\n\t\t\tc.checkPos(node, node.ForPos, \"for\")\n\t\t}\n\t\tif node.Braces {\n\t\t\tc.checkPos(node, node.DoPos, \"{\")\n\t\t\tc.checkPos(node, node.DonePos, \"}\")\n\t\t\t// Zero out Braces, to not duplicate all the test cases.\n\t\t\t// The printer ignores the field anyway.\n\t\t\tnode.Braces = false\n\t\t} else {\n\t\t\tc.checkPos(node, node.DoPos, \"do\")\n\t\t\tc.checkPos(node, node.DonePos, \"done\")\n\t\t}\n\tcase *WordIter:\n\t\tif node.InPos.IsValid() {\n\t\t\tc.checkPos(node, node.InPos, \"in\")\n\t\t}\n\tcase *CStyleLoop:\n\t\tc.checkPos(node, node.Lparen, \"((\")\n\t\tc.checkPos(node, node.Rparen, \"))\")\n\tcase *SglQuoted:\n\t\tc.checkPos(node, posAddCol(node.End(), -1), \"'\")\n\t\tvaluePos := posAddCol(node.Left, 1)\n\t\tif node.Dollar {\n\t\t\tvaluePos = posAddCol(valuePos, 1)\n\t\t}\n\t\tval := node.Value\n\t\tif strings.Contains(c.src, \"`\") && strings.Contains(c.src, \"\\\\\") {\n\t\t\t// removed backslashes inside backquote cmd substs\n\t\t\tval = \"\"\n\t\t}\n\t\tc.checkPos(node, valuePos, val)\n\t\tif node.Dollar {\n\t\t\tc.checkPos(node, node.Left, \"$'\")\n\t\t} else {\n\t\t\tc.checkPos(node, node.Left, \"'\")\n\t\t}\n\t\tc.checkPos(node, node.Right, \"'\")\n\tcase *DblQuoted:\n\t\tc.checkPos(node, posAddCol(node.End(), -1), `\"`)\n\t\tif node.Dollar {\n\t\t\tc.checkPos(node, node.Left, `$\"`)\n\t\t} else {\n\t\t\tc.checkPos(node, node.Left, `\"`)\n\t\t}\n\t\tc.checkPos(node, node.Right, `\"`)\n\tcase *UnaryArithm:\n\t\tc.checkPos(node, node.OpPos, node.Op.String())\n\tcase *UnaryTest:\n\t\tstrs := []string{node.Op.String()}\n\t\tswitch node.Op {\n\t\tcase TsExists:\n\t\t\tstrs = append(strs, \"-a\")\n\t\tcase TsSmbLink:\n\t\t\tstrs = append(strs, \"-h\")\n\t\t}\n\t\tc.checkPos(node, node.OpPos, strs...)\n\tcase *BinaryCmd:\n\t\tc.checkPos(node, node.OpPos, node.Op.String())\n\tcase *BinaryArithm:\n\t\tc.checkPos(node, node.OpPos, node.Op.String())\n\tcase *BinaryTest:\n\t\tstrs := []string{node.Op.String()}\n\t\tswitch node.Op {\n\t\tcase TsMatch:\n\t\t\tstrs = append(strs, \"=\")\n\t\t}\n\t\tc.checkPos(node, node.OpPos, strs...)\n\tcase *ParenArithm:\n\t\tc.checkPos(node, node.Lparen, \"(\")\n\t\tc.checkPos(node, node.Rparen, \")\")\n\tcase *FlagsArithm:\n\tcase *ParenTest:\n\t\tc.checkPos(node, node.Lparen, \"(\")\n\t\tc.checkPos(node, node.Rparen, \")\")\n\tcase *FuncDecl:\n\t\tif node.RsrvWord {\n\t\t\tc.checkPos(node, node.Position, \"function\")\n\t\t} else {\n\t\t\tc.checkPos(node, node.Position)\n\t\t}\n\tcase *ParamExp:\n\t\tif node.nakedIndex() {\n\t\t\t// Dollar is unset; Pos falls back to Param.\n\t\t} else {\n\t\t\tc.checkPos(node, node.Dollar, \"$\")\n\t\t}\n\t\tif !node.Short {\n\t\t\tc.checkPos(node, node.Rbrace, \"}\")\n\t\t} else if node.nakedIndex() {\n\t\t\tc.checkPos(node, posAddCol(node.End(), -1), \"]\")\n\t\t}\n\tcase *ArithmExp:\n\t\tif node.Bracket {\n\t\t\t// deprecated $(( form\n\t\t\tc.checkPos(node, node.Left, \"$[\")\n\t\t\tc.checkPos(node, node.Right, \"]\")\n\t\t} else {\n\t\t\tc.checkPos(node, node.Left, \"$((\")\n\t\t\tc.checkPos(node, node.Right, \"))\")\n\t\t}\n\tcase *ArithmCmd:\n\t\tc.checkPos(node, node.Left, \"((\")\n\t\tc.checkPos(node, node.Right, \"))\")\n\tcase *CmdSubst:\n\t\tswitch {\n\t\tcase node.TempFile:\n\t\t\tc.checkPos(node, node.Left, \"${ \", \"${\\t\", \"${\\n\")\n\t\t\tc.checkPos(node, node.Right, \"}\")\n\t\tcase node.ReplyVar:\n\t\t\tc.checkPos(node, node.Left, \"${|\")\n\t\t\tc.checkPos(node, node.Right, \"}\")\n\t\tcase node.Backquotes:\n\t\t\tc.checkPos(node, node.Left, \"`\", \"\\\\`\")\n\t\t\tc.checkPos(node, node.Right, \"`\", \"\\\\`\")\n\t\t\t// Zero out Backquotes, to not duplicate all the test\n\t\t\t// cases. The printer ignores the field anyway.\n\t\t\tnode.Backquotes = false\n\t\tdefault:\n\t\t\tc.checkPos(node, node.Left, \"$(\")\n\t\t\tc.checkPos(node, node.Right, \")\")\n\t\t}\n\tcase *CaseClause:\n\t\tc.checkPos(node, node.Case, \"case\")\n\t\tif node.Braces {\n\t\t\tc.checkPos(node, node.In, \"{\")\n\t\t\tc.checkPos(node, node.Esac, \"}\")\n\t\t\t// Zero out Braces, to not duplicate all the test cases.\n\t\t\t// The printer ignores the field anyway.\n\t\t\tnode.Braces = false\n\t\t} else {\n\t\t\tc.checkPos(node, node.In, \"in\")\n\t\t\tc.checkPos(node, node.Esac, \"esac\")\n\t\t}\n\tcase *CaseItem:\n\t\tif node.OpPos.IsValid() {\n\t\t\tc.checkPos(node, node.OpPos, node.Op.String(), \"esac\")\n\t\t}\n\tcase *TestClause:\n\t\tc.checkPos(node, node.Left, \"[[\")\n\t\tc.checkPos(node, node.Right, \"]]\")\n\tcase *TimeClause:\n\t\tc.checkPos(node, node.Time, \"time\")\n\tcase *CoprocClause:\n\t\tc.checkPos(node, node.Coproc, \"coproc\")\n\tcase *LetClause:\n\t\tc.checkPos(node, node.Let, \"let\")\n\tcase *TestDecl:\n\t\tc.checkPos(node, node.Position, \"@test\")\n\tcase *ArrayExpr:\n\t\tc.checkPos(node, node.Lparen, \"(\")\n\t\tc.checkPos(node, node.Rparen, \")\")\n\tcase *ExtGlob:\n\t\tc.checkPos(node, node.OpPos, node.Op.String())\n\t\tc.checkPos(node, posAddCol(node.End(), -1), \")\")\n\tcase *ProcSubst:\n\t\tc.checkPos(node, node.OpPos, node.Op.String())\n\t\tc.checkPos(node, node.Rparen, \")\")\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "syntax/fuzz_test.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"io\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-quicktest/qt\"\n)\n\nfunc FuzzQuote(f *testing.F) {\n\tif _, err := exec.LookPath(\"bash\"); err != nil {\n\t\tf.Skipf(\"requires bash to verify quoted strings\")\n\t}\n\n\t// Keep in sync with ExampleQuote.\n\tf.Add(\"foo\", uint8(LangBash))\n\tf.Add(\"bar $baz\", uint8(LangBash))\n\tf.Add(`\"won't\"`, uint8(LangBash))\n\tf.Add(`~/home`, uint8(LangBash))\n\tf.Add(\"#1304\", uint8(LangBash))\n\tf.Add(\"name=value\", uint8(LangBash))\n\tf.Add(`glob-*`, uint8(LangBash))\n\tf.Add(\"invalid-\\xe2'\", uint8(LangBash))\n\tf.Add(\"nonprint-\\x0b\\x1b\", uint8(LangBash))\n\tf.Fuzz(func(t *testing.T, s string, langVariant uint8) {\n\t\tlang := LangVariant(langVariant)\n\t\tquoted, err := Quote(s, lang)\n\t\tif err != nil {\n\t\t\t// Cannot be quoted; not interesting.\n\t\t\tt.Skip()\n\t\t}\n\n\t\texternal, ok := externalShells[lang]\n\t\tif !ok {\n\t\t\tt.Skip() // invalid/untested lang variant\n\t\t}\n\t\texternal.require(t)\n\n\t\t// Verify that our parser ends up with a simple command with one word.\n\t\tf, err := NewParser(Variant(lang)).Parse(strings.NewReader(quoted), \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"parse error on %q quoted as %s: %v\", s, quoted, err)\n\t\t}\n\t\tqt.Assert(t, qt.Equals(len(f.Stmts), 1), qt.Commentf(\"in: %q, quoted: %s\", s, quoted))\n\t\tcall, ok := f.Stmts[0].Cmd.(*CallExpr)\n\t\tqt.Assert(t, qt.IsTrue(ok), qt.Commentf(\"in: %q, quoted: %s\", s, quoted))\n\t\tqt.Assert(t, qt.Equals(len(call.Args), 1), qt.Commentf(\"in: %q, quoted: %s\", s, quoted))\n\n\t\t// Also check that the single word only uses literals or quoted strings.\n\t\tWalk(call.Args[0], func(node Node) bool {\n\t\t\tswitch node.(type) {\n\t\t\tcase nil, *Word, *Lit, *SglQuoted, *DblQuoted:\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unexpected node type: %T\", node)\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\n\t\t// The process below shouldn't run arbitrary code,\n\t\t// since our parser checks above should catch the use of ';' or '$',\n\t\t// in the case that Quote were too naive to quote them.\n\t\tout, err := exec.Command(external.cmd, \"-c\", \"printf %s \"+quoted).CombinedOutput()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%s error on %q quoted as %s: %v: %s\", external.cmd, s, quoted, err, out)\n\t\t}\n\t\twant, got := s, string(out)\n\t\tif want != got {\n\t\t\tt.Fatalf(\"%s output mismatch on %q quoted as %s: got %q (len=%d)\",\n\t\t\t\texternal.cmd, want, quoted, got, len(got))\n\t\t}\n\t})\n}\n\nfunc FuzzParsePrint(f *testing.F) {\n\tadd := func(src string, variant LangVariant) {\n\t\t// For now, default to just KeepComments.\n\t\tf.Add(src, uint8(variant), true, false,\n\t\t\tuint8(0), false, false, false, false, false, false, false)\n\t}\n\n\tfor _, test := range errorCases {\n\t\tadd(test.in, LangBash)\n\t}\n\tfor _, test := range printTests {\n\t\tadd(test.in, LangBash)\n\t}\n\tfor _, test := range fileTests {\n\t\tfor _, in := range test.inputs {\n\t\t\tfor lang := range langResolvedVariants.bits() {\n\t\t\t\tif test.byLangIndex[lang.index()] != nil {\n\t\t\t\t\tadd(in, lang)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tf.Fuzz(func(t *testing.T,\n\t\tsrc string,\n\n\t\t// parser options\n\t\t// TODO: also fuzz StopAt\n\t\tlangVariant uint8,\n\t\tkeepComments bool,\n\n\t\tsimplify bool,\n\n\t\t// printer options\n\t\tindent uint8, // 0-255\n\t\tbinaryNextLine bool,\n\t\tswitchCaseIndent bool,\n\t\tspaceRedirects bool,\n\t\tkeepPadding bool,\n\t\tminify bool,\n\t\tsingleLine bool,\n\t\tfunctionNextLine bool,\n\t) {\n\t\tlang := LangVariant(langVariant)\n\t\tif lang.count() != 1 || !lang.in(langResolvedVariants) {\n\t\t\tt.Skip()\n\t\t}\n\t\tif indent > 16 {\n\t\t\tt.Skip() // more indentation won't really be interesting\n\t\t}\n\n\t\tparser := NewParser(Variant(lang), KeepComments(keepComments))\n\t\tt.Logf(\"input: %q\", src)\n\t\tt.Logf(\"LangVariant: %s\", lang)\n\t\tt.Logf(\"KeepComments: %t\", keepComments)\n\t\tprog, err := parser.Parse(strings.NewReader(src), \"\")\n\t\tif err != nil {\n\t\t\tt.Skip() // not valid shell syntax\n\t\t}\n\n\t\tif simplify {\n\t\t\tSimplify(prog)\n\t\t}\n\n\t\tprinter := NewPrinter()\n\t\tIndent(uint(indent))(printer)\n\t\tBinaryNextLine(binaryNextLine)(printer)\n\t\tSwitchCaseIndent(switchCaseIndent)(printer)\n\t\tSpaceRedirects(spaceRedirects)(printer)\n\t\tKeepPadding(keepPadding)(printer)\n\t\tMinify(minify)(printer)\n\t\tSingleLine(singleLine)(printer)\n\t\tFunctionNextLine(functionNextLine)(printer)\n\n\t\tif err := printer.Print(io.Discard, prog); err != nil {\n\t\t\tt.Skip() // e.g. invalid option\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "syntax/lexer.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"unicode/utf8\"\n)\n\nfunc asciiLetter[T rune | byte](r T) bool {\n\treturn ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z')\n}\n\nfunc asciiDigit[T rune | byte](r T) bool {\n\treturn r >= '0' && r <= '9'\n}\n\n// bytes that form or start a token\nfunc regOps(r rune) bool {\n\tswitch r {\n\tcase ';', '\"', '\\'', '(', ')', '$', '|', '&', '>', '<', '`':\n\t\treturn true\n\t}\n\treturn false\n}\n\n// tokenize these inside parameter expansions\nfunc paramOps(r rune) bool {\n\tswitch r {\n\tcase '}', '#', '!', ':', '-', '+', '=', '?', '%', '[', ']', '/', '^',\n\t\t',', '@', '*':\n\t\treturn true\n\t}\n\treturn false\n}\n\n// tokenize these inside arithmetic expansions\nfunc arithmOps(r rune) bool {\n\tswitch r {\n\tcase '+', '-', '!', '~', '*', '/', '%', '(', ')', '^', '<', '>', ':', '=',\n\t\t',', '?', '|', '&', '[', ']', '#', '.':\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc bquoteEscaped(b byte) bool {\n\tswitch b {\n\tcase '$', '`', '\\\\':\n\t\treturn true\n\t}\n\treturn false\n}\n\nconst escNewl rune = utf8.RuneSelf + 1\n\nfunc (p *Parser) rune() rune {\n\tif p.r == '\\n' || p.r == escNewl {\n\t\t// p.r instead of b so that newline\n\t\t// character positions don't have col 0.\n\t\tp.line++\n\t\tp.col = 0\n\t}\n\tp.col += int64(p.w)\n\tbquotes := 0\nretry:\n\tif p.bsp >= uint(len(p.bs)) && p.fill() == 0 {\n\t\tif len(p.bs) == 0 {\n\t\t\t// Necessary for the last position to be correct.\n\t\t\t// TODO: this is not exactly intuitive; figure out a better way.\n\t\t\tp.bsp = 1\n\t\t}\n\t\tp.r = utf8.RuneSelf\n\t\tp.w = 1\n\t\treturn p.r\n\t}\n\tif b := p.bs[p.bsp]; b < utf8.RuneSelf {\n\t\tp.bsp++\n\t\tswitch b {\n\t\tcase '\\x00':\n\t\t\t// Ignore null bytes while parsing, like bash.\n\t\t\tp.col++\n\t\t\tgoto retry\n\t\tcase '\\r':\n\t\t\tif p.peek() == '\\n' { // \\r\\n turns into \\n\n\t\t\t\tp.col++\n\t\t\t\tgoto retry\n\t\t\t}\n\t\tcase '\\\\':\n\t\t\tif p.r == '\\\\' {\n\t\t\t} else if p.peek() == '\\n' {\n\t\t\t\tp.bsp++\n\t\t\t\tp.w, p.r = 1, escNewl\n\t\t\t\treturn escNewl\n\t\t\t} else if p1, p2 := p.peekTwo(); p1 == '\\r' && p2 == '\\n' { // \\\\\\r\\n turns into \\\\\\n\n\t\t\t\tp.col++\n\t\t\t\tp.bsp += 2\n\t\t\t\tp.w, p.r = 2, escNewl\n\t\t\t\treturn escNewl\n\t\t\t}\n\t\t\t// TODO: why is this necessary to ensure correct position info?\n\t\t\tp.readEOF = false\n\t\t\tif p.openBquotes > 0 && bquotes < p.openBquotes &&\n\t\t\t\tp.bsp < uint(len(p.bs)) && bquoteEscaped(p.bs[p.bsp]) {\n\t\t\t\t// We turn backquote command substitutions into $(),\n\t\t\t\t// so we remove the extra backslashes needed by the backquotes.\n\t\t\t\tbquotes++\n\t\t\t\tp.col++\n\t\t\t\tgoto retry\n\t\t\t}\n\t\t}\n\t\tif b == '`' {\n\t\t\tp.lastBquoteEsc = bquotes\n\t\t}\n\t\tif p.litBs != nil {\n\t\t\tp.litBs = append(p.litBs, b)\n\t\t}\n\t\tp.w, p.r = 1, rune(b)\n\t\treturn p.r\n\t}\ndecodeRune:\n\tvar w int\n\tp.r, w = utf8.DecodeRune(p.bs[p.bsp:])\n\tif p.r == utf8.RuneError && !utf8.FullRune(p.bs[p.bsp:]) {\n\t\t// we need more bytes to read a full non-ascii rune\n\t\tif p.fill() > 0 {\n\t\t\tgoto decodeRune\n\t\t}\n\t}\n\tif p.litBs != nil {\n\t\tp.litBs = append(p.litBs, p.bs[p.bsp:p.bsp+uint(w)]...)\n\t}\n\tp.bsp += uint(w)\n\tif p.r == utf8.RuneError && w == 1 {\n\t\tp.posErr(p.nextPos(), \"invalid UTF-8 encoding\")\n\t}\n\tp.w = w\n\treturn p.r\n}\n\n// fill reads more bytes from the input src into readBuf.\n// Any bytes that had not yet been used at the end of the buffer\n// are slid into the beginning of the buffer.\n// The number of read bytes is returned, which is at least one\n// unless a read error occurred, such as [io.EOF].\nfunc (p *Parser) fill() (n int) {\n\tif p.readEOF || p.r == utf8.RuneSelf {\n\t\t// If the reader already gave us [io.EOF], do not try again.\n\t\t// If we decided to stop for any reason, do not bother reading either.\n\t\treturn 0\n\t}\n\tp.offs += int64(p.bsp)\n\tleft := len(p.bs) - int(p.bsp)\n\tcopy(p.readBuf[:left], p.readBuf[p.bsp:])\nreadAgain:\n\tn, err := 0, p.readErr\n\tif err == nil {\n\t\tn, err = p.src.Read(p.readBuf[left:])\n\t\tp.readErr = err\n\t\tif err == io.EOF {\n\t\t\tp.readEOF = true\n\t\t}\n\t}\n\tif n == 0 {\n\t\tif err == nil {\n\t\t\tgoto readAgain\n\t\t}\n\t\t// don't use p.errPass as we don't want to overwrite p.tok\n\t\tif err != io.EOF {\n\t\t\tp.err = err\n\t\t}\n\t\tif left > 0 {\n\t\t\tp.bs = p.readBuf[:left]\n\t\t} else {\n\t\t\tp.bs = nil\n\t\t}\n\t} else {\n\t\tp.bs = p.readBuf[:left+n]\n\t}\n\tp.bsp = 0\n\treturn n\n}\n\nfunc (p *Parser) nextKeepSpaces() {\n\tr := p.r\n\tif p.quote != hdocBody && p.quote != hdocBodyTabs {\n\t\t// Heredocs handle escaped newlines in a special way, but others do not.\n\t\tfor r == escNewl {\n\t\t\tr = p.rune()\n\t\t}\n\t}\n\tp.pos = p.nextPos()\n\tswitch p.quote {\n\tcase runeByRune:\n\t\tp.tok = illegalTok\n\tcase dblQuotes:\n\t\tswitch r {\n\t\tcase '`', '\"', '$':\n\t\t\tp.tok = p.dqToken(r)\n\t\tdefault:\n\t\t\tp.advanceLitDquote(r)\n\t\t}\n\tcase hdocBody, hdocBodyTabs:\n\t\tswitch r {\n\t\tcase '`', '$':\n\t\t\tp.tok = p.dqToken(r)\n\t\tdefault:\n\t\t\tp.advanceLitHdoc(r)\n\t\t}\n\tcase paramExpRepl:\n\t\tif r == '/' {\n\t\t\tp.rune()\n\t\t\tp.tok = slash\n\t\t\tbreak\n\t\t}\n\t\tfallthrough\n\tcase paramExpExp:\n\t\tswitch r {\n\t\tcase '}':\n\t\t\tp.tok = p.paramToken(r)\n\t\tcase '`', '\"', '$', '\\'':\n\t\t\tp.tok = p.regToken(r)\n\t\tdefault:\n\t\t\tp.advanceLitOther(r)\n\t\t}\n\t}\n\tif p.err != nil {\n\t\tp.tok = _EOF\n\t}\n}\n\nfunc (p *Parser) next() {\n\tif p.r == utf8.RuneSelf {\n\t\tp.tok = _EOF\n\t\treturn\n\t}\n\tp.spaced = false\n\tif p.quote&allKeepSpaces != 0 {\n\t\tp.nextKeepSpaces()\n\t\treturn\n\t}\n\tr := p.r\n\tfor r == escNewl {\n\t\tr = p.rune()\n\t}\nskipSpace:\n\tfor {\n\t\tswitch r {\n\t\tcase utf8.RuneSelf:\n\t\t\tp.tok = _EOF\n\t\t\treturn\n\t\tcase escNewl:\n\t\t\tr = p.rune()\n\t\tcase ' ', '\\t', '\\r':\n\t\t\tp.spaced = true\n\t\t\tr = p.rune()\n\t\tcase '\\n':\n\t\t\tif p.tok == _Newl {\n\t\t\t\t// merge consecutive newline tokens\n\t\t\t\tr = p.rune()\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tp.spaced = true\n\t\t\tp.tok = _Newl\n\t\t\tif p.quote != hdocWord && len(p.heredocs) > p.buriedHdocs {\n\t\t\t\tp.doHeredocs()\n\t\t\t}\n\t\t\treturn\n\t\tdefault:\n\t\t\tbreak skipSpace\n\t\t}\n\t}\n\tif p.stopAt != nil && (p.spaced || p.tok == illegalTok || p.stopToken()) {\n\t\tw := utf8.RuneLen(r)\n\t\tif bytes.HasPrefix(p.bs[p.bsp-uint(w):], p.stopAt) {\n\t\t\tp.r = utf8.RuneSelf\n\t\t\tp.w = 1\n\t\t\tp.tok = _EOF\n\t\t\treturn\n\t\t}\n\t}\n\tp.pos = p.nextPos()\n\tswitch {\n\tcase p.quote&allRegTokens != 0:\n\t\tswitch r {\n\t\tcase ';', '\"', '\\'', '(', ')', '$', '|', '&', '>', '<', '`':\n\t\t\tif r == '<' && p.lang.in(LangZsh) && p.zshNumRange() {\n\t\t\t\tp.advanceLitNone(r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tp.tok = p.regToken(r)\n\t\tcase '#':\n\t\t\t// If we're parsing $foo#bar, ${foo}#bar, 'foo'#bar, or \"foo\"#bar,\n\t\t\t// #bar is a continuation of the same word, not a comment.\n\t\t\tif p.quote == unquotedWordCont && !p.spaced {\n\t\t\t\tp.advanceLitNone(r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tr = p.rune()\n\t\t\tp.newLit(r)\n\t\truneLoop:\n\t\t\tfor {\n\t\t\t\tswitch r {\n\t\t\t\tcase '\\n', utf8.RuneSelf:\n\t\t\t\t\tbreak runeLoop\n\t\t\t\tcase escNewl:\n\t\t\t\t\tp.litBs = append(p.litBs, '\\\\', '\\n')\n\t\t\t\t\tbreak runeLoop\n\t\t\t\tcase '`':\n\t\t\t\t\tif p.backquoteEnd() {\n\t\t\t\t\t\tbreak runeLoop\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tr = p.rune()\n\t\t\t}\n\t\t\tif p.keepComments {\n\t\t\t\t*p.curComs = append(*p.curComs, Comment{\n\t\t\t\t\tHash: p.pos,\n\t\t\t\t\tText: p.endLit(),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tp.litBs = nil\n\t\t\t}\n\t\t\tp.next()\n\t\tcase '[':\n\t\t\tif p.quote == arrayElems {\n\t\t\t\tp.rune()\n\t\t\t\tp.tok = leftBrack\n\t\t\t} else {\n\t\t\t\tp.advanceLitNone(r)\n\t\t\t}\n\t\tcase '=':\n\t\t\tif p.peek() == '(' {\n\t\t\t\tp.rune()\n\t\t\t\tp.rune()\n\t\t\t\tp.tok = assgnParen\n\t\t\t} else if p.quote == arrayElems {\n\t\t\t\tp.rune()\n\t\t\t\tp.tok = assgn\n\t\t\t} else {\n\t\t\t\tp.advanceLitNone(r)\n\t\t\t}\n\t\tcase '?', '*', '+', '@', '!':\n\t\t\tif p.extendedGlob() {\n\t\t\t\tswitch r {\n\t\t\t\tcase '?':\n\t\t\t\t\tp.tok = globQuest\n\t\t\t\tcase '*':\n\t\t\t\t\tp.tok = globStar\n\t\t\t\tcase '+':\n\t\t\t\t\tp.tok = globPlus\n\t\t\t\tcase '@':\n\t\t\t\t\tp.tok = globAt\n\t\t\t\tcase '!':\n\t\t\t\t\tp.tok = globExcl\n\t\t\t\t}\n\t\t\t\tp.rune()\n\t\t\t\tp.rune()\n\t\t\t} else {\n\t\t\t\tp.advanceLitNone(r)\n\t\t\t}\n\t\tdefault:\n\t\t\tp.advanceLitNone(r)\n\t\t}\n\tcase p.quote&allArithmExpr != 0 && arithmOps(r):\n\t\tp.tok = p.arithmToken(r)\n\tcase p.quote&allParamExp != 0 && paramOps(r):\n\t\tp.tok = p.paramToken(r)\n\tcase p.quote == testExprRegexp:\n\t\tif !p.rxFirstPart && p.spaced {\n\t\t\tp.quote = testExpr\n\t\t\tgoto skipSpace\n\t\t}\n\t\tp.rxFirstPart = false\n\t\tswitch r {\n\t\tcase ';', '\"', '\\'', '$', '&', '>', '<', '`':\n\t\t\tp.tok = p.regToken(r)\n\t\tcase ')':\n\t\t\tif p.rxOpenParens > 0 {\n\t\t\t\t// continuation of open paren\n\t\t\t\tp.advanceLitRe(r)\n\t\t\t} else {\n\t\t\t\tp.tok = rightParen\n\t\t\t\tp.quote = testExpr\n\t\t\t\tp.rune() // we are tokenizing manually\n\t\t\t}\n\t\tdefault: // including '(', '|'\n\t\t\tp.advanceLitRe(r)\n\t\t}\n\tcase regOps(r):\n\t\tp.tok = p.regToken(r)\n\tdefault:\n\t\tp.advanceLitOther(r)\n\t}\n\tif p.err != nil {\n\t\tp.tok = _EOF\n\t}\n}\n\n// extendedGlob determines whether we're parsing a Bash extended globbing expression.\n// For example, whether `*` or `@` are followed by `(` to form `@(foo)`.\nfunc (p *Parser) extendedGlob() bool {\n\tif p.lang.in(LangZsh) {\n\t\t// Zsh supports Bash extended globs via the KSH_GLOB option.\n\t\t// In Bash we would parse extended globs as [ExtGlob] nodes,\n\t\t// but trying to do that in Zsh would cause ambiguity with glob qualifiers.\n\t\t// Just like glob qualifiers, parse extended globs as literals in Zsh.\n\t\treturn false\n\t}\n\tif p.val == \"function\" {\n\t\t// We don't support e.g. `function @() { ... }` at the moment, but we could.\n\t\treturn false\n\t}\n\tif p.peek() == '(' {\n\t\t// NOTE: empty pattern list is a valid globbing syntax like `@()`,\n\t\t// but we'll operate on the \"likelihood\" that it is a function;\n\t\t// only tokenize if its a non-empty pattern list.\n\t\t// We do this after peeking for just one byte, so that the input `echo *`\n\t\t// followed by a newline does not hang an interactive shell parser until\n\t\t// another byte is input.\n\t\t_, p2 := p.peekTwo()\n\t\treturn p2 != ')'\n\t}\n\treturn false\n}\n\nfunc (p *Parser) peek() byte {\n\tif int(p.bsp) >= len(p.bs) {\n\t\tp.fill()\n\t}\n\tif int(p.bsp) >= len(p.bs) {\n\t\treturn utf8.RuneSelf\n\t}\n\treturn p.bs[p.bsp]\n}\n\nfunc (p *Parser) peekTwo() (byte, byte) {\n\t// TODO: This should loop for slow readers, e.g. those providing one byte at\n\t// a time. Use a loop and test it with [testing/iotest.OneByteReader].\n\tif int(p.bsp+1) >= len(p.bs) {\n\t\tp.fill()\n\t}\n\tif int(p.bsp) >= len(p.bs) {\n\t\treturn utf8.RuneSelf, utf8.RuneSelf\n\t}\n\tif int(p.bsp+1) >= len(p.bs) {\n\t\treturn p.bs[p.bsp], utf8.RuneSelf\n\t}\n\treturn p.bs[p.bsp], p.bs[p.bsp+1]\n}\n\nfunc (p *Parser) regToken(r rune) token {\n\tswitch r {\n\tcase '\\'':\n\t\tp.rune()\n\t\treturn sglQuote\n\tcase '\"':\n\t\tp.rune()\n\t\treturn dblQuote\n\tcase '`':\n\t\t// Don't call p.rune, as we need to work out p.openBquotes to\n\t\t// properly handle backslashes in the lexer.\n\t\treturn bckQuote\n\tcase '&':\n\t\tswitch p.rune() {\n\t\tcase '&':\n\t\t\tp.rune()\n\t\t\treturn andAnd\n\t\tcase '>':\n\t\t\tswitch p.rune() {\n\t\t\tcase '|':\n\t\t\t\tp.rune()\n\t\t\t\treturn rdrAllClob\n\t\t\tcase '!':\n\t\t\t\tif p.lang.in(LangZsh) {\n\t\t\t\t\tp.rune()\n\t\t\t\t\treturn rdrAllClob\n\t\t\t\t}\n\t\t\tcase '>':\n\t\t\t\tswitch p.rune() {\n\t\t\t\tcase '|':\n\t\t\t\t\tp.rune()\n\t\t\t\t\treturn appAllClob\n\t\t\t\tcase '!':\n\t\t\t\t\tif p.lang.in(LangZsh) {\n\t\t\t\t\t\tp.rune()\n\t\t\t\t\t\treturn appAllClob\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn appAll\n\t\t\t}\n\t\t\treturn rdrAll\n\t\tcase '|':\n\t\t\tif p.lang.in(LangZsh) {\n\t\t\t\tp.rune()\n\t\t\t\treturn andPipe\n\t\t\t}\n\t\tcase '!':\n\t\t\tif p.lang.in(LangZsh) {\n\t\t\t\tp.rune()\n\t\t\t\treturn andBang\n\t\t\t}\n\t\t}\n\t\treturn and\n\tcase '|':\n\t\tswitch p.rune() {\n\t\tcase '|':\n\t\t\tp.rune()\n\t\t\treturn orOr\n\t\tcase '&':\n\t\t\tif !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn orAnd\n\t\t}\n\t\treturn or\n\tcase '$':\n\t\tswitch p.rune() {\n\t\tcase '\\'':\n\t\t\tif !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn dollSglQuote\n\t\tcase '\"':\n\t\t\tif !p.lang.in(langBashLike | LangMirBSDKorn) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn dollDblQuote\n\t\tcase '{':\n\t\t\tp.rune()\n\t\t\treturn dollBrace\n\t\tcase '[':\n\t\t\tif !p.lang.in(langBashLike) {\n\t\t\t\t// latter to not tokenise ${$[@]} as $[\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn dollBrack\n\t\tcase '(':\n\t\t\tif p.rune() == '(' {\n\t\t\t\tp.rune()\n\t\t\t\treturn dollDblParen\n\t\t\t}\n\t\t\treturn dollParen\n\t\t}\n\t\treturn dollar\n\tcase '(':\n\t\tif p.rune() == '(' && p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.quote != testExpr {\n\t\t\tp.rune()\n\t\t\treturn dblLeftParen\n\t\t}\n\t\treturn leftParen\n\tcase ')':\n\t\tp.rune()\n\t\treturn rightParen\n\tcase ';':\n\t\tswitch p.rune() {\n\t\tcase ';':\n\t\t\tif p.rune() == '&' && p.lang.in(langBashLike) {\n\t\t\t\tp.rune()\n\t\t\t\treturn dblSemiAnd\n\t\t\t}\n\t\t\treturn dblSemicolon\n\t\tcase '&':\n\t\t\tif !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn semiAnd\n\t\tcase '|':\n\t\t\tif !p.lang.in(LangMirBSDKorn) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn semiOr\n\t\t}\n\t\treturn semicolon\n\tcase '<':\n\t\tswitch p.rune() {\n\t\tcase '<':\n\t\t\tswitch p.rune() {\n\t\t\tcase '-':\n\t\t\t\tp.rune()\n\t\t\t\treturn dashHdoc\n\t\t\tcase '<':\n\t\t\t\tp.rune()\n\t\t\t\treturn wordHdoc\n\t\t\t}\n\t\t\treturn hdoc\n\t\tcase '>':\n\t\t\tp.rune()\n\t\t\treturn rdrInOut\n\t\tcase '&':\n\t\t\tp.rune()\n\t\t\treturn dplIn\n\t\tcase '(':\n\t\t\tif !p.lang.in(langBashLike | LangZsh) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn cmdIn\n\t\t}\n\t\treturn rdrIn\n\tcase '>':\n\t\tswitch p.rune() {\n\t\tcase '>':\n\t\t\tswitch p.rune() {\n\t\t\tcase '|':\n\t\t\t\tp.rune()\n\t\t\t\treturn appClob\n\t\t\tcase '!':\n\t\t\t\tif p.lang.in(LangZsh) {\n\t\t\t\t\tp.rune()\n\t\t\t\t\treturn appClob\n\t\t\t\t}\n\t\t\tcase '&':\n\t\t\t\tif !p.lang.in(LangZsh) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tswitch p.rune() {\n\t\t\t\tcase '|':\n\t\t\t\t\tp.rune()\n\t\t\t\t\treturn appAllClob // >>&| is an alias for &>>|\n\t\t\t\tcase '!':\n\t\t\t\t\tp.rune()\n\t\t\t\t\treturn appAllClob // >>&! is an alias for &>>|\n\t\t\t\t}\n\t\t\t\treturn appAll // >>& is an alias for &>>\n\t\t\t}\n\t\t\treturn appOut\n\t\tcase '&':\n\t\t\tr = p.rune()\n\t\t\tif p.lang.in(LangZsh) {\n\t\t\t\tswitch r {\n\t\t\t\tcase '|':\n\t\t\t\t\tp.rune()\n\t\t\t\t\treturn rdrAllClob // >&| is an alias for &>|\n\t\t\t\tcase '!':\n\t\t\t\t\tp.rune()\n\t\t\t\t\treturn rdrAllClob // >&! is an alias for &>|\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn dplOut\n\t\tcase '|':\n\t\t\tp.rune()\n\t\t\treturn rdrClob\n\t\tcase '!':\n\t\t\tif p.lang.in(LangZsh) {\n\t\t\t\tp.rune()\n\t\t\t\treturn rdrClob\n\t\t\t}\n\t\tcase '(':\n\t\t\tif !p.lang.in(langBashLike | LangZsh) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn cmdOut\n\t\t}\n\t\treturn rdrOut\n\t}\n\tpanic(\"unreachable\")\n}\n\nfunc (p *Parser) dqToken(r rune) token {\n\tswitch r {\n\tcase '\"':\n\t\tp.rune()\n\t\treturn dblQuote\n\tcase '`':\n\t\t// Don't call p.rune, as we need to work out p.openBquotes to\n\t\t// properly handle backslashes in the lexer.\n\t\treturn bckQuote\n\tcase '$':\n\t\tswitch p.rune() {\n\t\tcase '{':\n\t\t\tp.rune()\n\t\t\treturn dollBrace\n\t\tcase '[':\n\t\t\tif !p.lang.in(langBashLike) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.rune()\n\t\t\treturn dollBrack\n\t\tcase '(':\n\t\t\tif p.rune() == '(' {\n\t\t\t\tp.rune()\n\t\t\t\treturn dollDblParen\n\t\t\t}\n\t\t\treturn dollParen\n\t\t}\n\t\treturn dollar\n\t}\n\tpanic(\"unreachable\")\n}\n\nfunc (p *Parser) paramToken(r rune) token {\n\tswitch r {\n\tcase '}':\n\t\tp.rune()\n\t\treturn rightBrace\n\tcase ':':\n\t\tswitch p.rune() {\n\t\tcase '+':\n\t\t\tp.rune()\n\t\t\treturn colPlus\n\t\tcase '-':\n\t\t\tp.rune()\n\t\t\treturn colMinus\n\t\tcase '?':\n\t\t\tp.rune()\n\t\t\treturn colQuest\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn colAssgn\n\t\tcase '#':\n\t\t\tp.rune()\n\t\t\treturn colHash\n\t\tcase '|':\n\t\t\tp.rune()\n\t\t\treturn colPipe\n\t\tcase '*':\n\t\t\tp.rune()\n\t\t\treturn colStar\n\t\t}\n\t\treturn colon\n\tcase '+':\n\t\tp.rune()\n\t\treturn plus\n\tcase '-':\n\t\tp.rune()\n\t\treturn minus\n\tcase '?':\n\t\tp.rune()\n\t\treturn quest\n\tcase '=':\n\t\tp.rune()\n\t\treturn assgn\n\tcase '%':\n\t\tif p.rune() == '%' {\n\t\t\tp.rune()\n\t\t\treturn dblPerc\n\t\t}\n\t\treturn perc\n\tcase '#':\n\t\tif p.rune() == '#' {\n\t\t\tp.rune()\n\t\t\treturn dblHash\n\t\t}\n\t\treturn hash\n\tcase '!':\n\t\tp.rune()\n\t\treturn exclMark\n\tcase ']':\n\t\tp.rune()\n\t\treturn rightBrack\n\tcase '/':\n\t\tif p.rune() == '/' {\n\t\t\tp.rune()\n\t\t\treturn dblSlash\n\t\t}\n\t\treturn slash\n\tcase '^':\n\t\tif p.rune() == '^' {\n\t\t\tp.rune()\n\t\t\treturn dblCaret\n\t\t}\n\t\treturn caret\n\tcase ',':\n\t\tif p.rune() == ',' {\n\t\t\tp.rune()\n\t\t\treturn dblComma\n\t\t}\n\t\treturn comma\n\tcase '@':\n\t\tp.rune()\n\t\treturn at\n\tcase '*':\n\t\tp.rune()\n\t\treturn star\n\n\t// This func gets called by the parser in [runeByRune] mode;\n\t// we need to handle EOF and unexpected runes.\n\tcase utf8.RuneSelf:\n\t\treturn _EOF\n\tdefault:\n\t\treturn illegalTok\n\t}\n}\n\nfunc (p *Parser) arithmToken(r rune) token {\n\tswitch r {\n\tcase '!':\n\t\tif p.rune() == '=' {\n\t\t\tp.rune()\n\t\t\treturn nequal\n\t\t}\n\t\treturn exclMark\n\tcase '=':\n\t\tif p.rune() == '=' {\n\t\t\tp.rune()\n\t\t\treturn equal\n\t\t}\n\t\treturn assgn\n\tcase '~':\n\t\tp.rune()\n\t\treturn tilde\n\tcase '(':\n\t\tp.rune()\n\t\treturn leftParen\n\tcase ')':\n\t\tp.rune()\n\t\treturn rightParen\n\tcase '&':\n\t\tswitch p.rune() {\n\t\tcase '&':\n\t\t\tif p.rune() == '=' && p.lang.in(LangZsh) {\n\t\t\t\tp.rune()\n\t\t\t\treturn andBoolAssgn\n\t\t\t}\n\t\t\treturn andAnd\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn andAssgn\n\t\t}\n\t\treturn and\n\tcase '|':\n\t\tswitch p.rune() {\n\t\tcase '|':\n\t\t\tif p.rune() == '=' && p.lang.in(LangZsh) {\n\t\t\t\tp.rune()\n\t\t\t\treturn orBoolAssgn\n\t\t\t}\n\t\t\treturn orOr\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn orAssgn\n\t\t}\n\t\treturn or\n\tcase '<':\n\t\tswitch p.rune() {\n\t\tcase '<':\n\t\t\tif p.rune() == '=' {\n\t\t\t\tp.rune()\n\t\t\t\treturn shlAssgn\n\t\t\t}\n\t\t\treturn hdoc\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn lequal\n\t\t}\n\t\treturn rdrIn\n\tcase '>':\n\t\tswitch p.rune() {\n\t\tcase '>':\n\t\t\tif p.rune() == '=' {\n\t\t\t\tp.rune()\n\t\t\t\treturn shrAssgn\n\t\t\t}\n\t\t\treturn appOut\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn gequal\n\t\t}\n\t\treturn rdrOut\n\tcase '+':\n\t\tswitch p.rune() {\n\t\tcase '+':\n\t\t\tp.rune()\n\t\t\treturn addAdd\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn addAssgn\n\t\t}\n\t\treturn plus\n\tcase '-':\n\t\tswitch p.rune() {\n\t\tcase '-':\n\t\t\tp.rune()\n\t\t\treturn subSub\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn subAssgn\n\t\t}\n\t\treturn minus\n\tcase '%':\n\t\tif p.rune() == '=' {\n\t\t\tp.rune()\n\t\t\treturn remAssgn\n\t\t}\n\t\treturn perc\n\tcase '*':\n\t\tswitch p.rune() {\n\t\tcase '*':\n\t\t\tif p.rune() == '=' && p.lang.in(LangZsh) {\n\t\t\t\tp.rune()\n\t\t\t\treturn powAssgn\n\t\t\t}\n\t\t\treturn power\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn mulAssgn\n\t\t}\n\t\treturn star\n\tcase '/':\n\t\tif p.rune() == '=' {\n\t\t\tp.rune()\n\t\t\treturn quoAssgn\n\t\t}\n\t\treturn slash\n\tcase '^':\n\t\tswitch p.rune() {\n\t\tcase '^':\n\t\t\tif p.rune() == '=' && p.lang.in(LangZsh) {\n\t\t\t\tp.rune()\n\t\t\t\treturn xorBoolAssgn\n\t\t\t}\n\t\t\treturn dblCaret\n\t\tcase '=':\n\t\t\tp.rune()\n\t\t\treturn xorAssgn\n\t\t}\n\t\treturn caret\n\tcase '[':\n\t\tp.rune()\n\t\treturn leftBrack\n\tcase ']':\n\t\tp.rune()\n\t\treturn rightBrack\n\tcase ',':\n\t\tp.rune()\n\t\treturn comma\n\tcase '?':\n\t\tp.rune()\n\t\treturn quest\n\tcase ':':\n\t\tp.rune()\n\t\treturn colon\n\tcase '#':\n\t\tp.rune()\n\t\treturn hash\n\tcase '.':\n\t\tp.rune()\n\t\treturn period\n\t}\n\tpanic(\"unreachable\")\n}\n\nfunc (p *Parser) newLit(r rune) {\n\tswitch {\n\tcase r < utf8.RuneSelf:\n\t\tp.litBs = p.litBuf[:1]\n\t\tp.litBs[0] = byte(r)\n\tcase r > escNewl:\n\t\tw := utf8.RuneLen(r)\n\t\tp.litBs = append(p.litBuf[:0], p.bs[p.bsp-uint(w):p.bsp]...)\n\tdefault:\n\t\t// don't let r == utf8.RuneSelf go to the second case as [utf8.RuneLen]\n\t\t// would return -1\n\t\tp.litBs = p.litBuf[:0]\n\t}\n}\n\nfunc (p *Parser) endLit() (s string) {\n\tif p.r == utf8.RuneSelf || p.r == escNewl {\n\t\ts = string(p.litBs)\n\t} else {\n\t\ts = string(p.litBs[:len(p.litBs)-p.w])\n\t}\n\tp.litBs = nil\n\treturn s\n}\n\nfunc (p *Parser) isLitRedir() bool {\n\tlit := p.litBs[:len(p.litBs)-1]\n\tif lit[0] == '{' && lit[len(lit)-1] == '}' {\n\t\treturn ValidName(string(lit[1 : len(lit)-1]))\n\t}\n\treturn numberLiteral(lit)\n}\n\nfunc positionalRuneParam[T rune | byte](r T) bool {\n\tswitch r {\n\tcase '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc singleRuneParam[T rune | byte](r T) bool {\n\tswitch r {\n\tcase '@', '*', '#', '$', '?', '!', '-':\n\t\treturn true\n\t}\n\treturn positionalRuneParam(r)\n}\n\nfunc paramNameRune[T rune | byte](r T) bool {\n\treturn asciiLetter(r) || asciiDigit(r) || r == '_'\n}\n\nfunc (p *Parser) advanceLitOther(r rune) {\n\ttok := _LitWord\nloop:\n\tfor p.newLit(r); r != utf8.RuneSelf; r = p.rune() {\n\t\tswitch r {\n\t\tcase '\\\\': // escaped byte follows\n\t\t\tp.rune()\n\t\tcase '\\'', '\"', '`', '$':\n\t\t\ttok = _Lit\n\t\t\tbreak loop\n\t\tcase '}':\n\t\t\tif p.quote&allParamExp != 0 {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\tcase '/':\n\t\t\tif p.quote != paramExpExp {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\tcase ':', '=', '%', '^', ',', '?', '!', '~', '*':\n\t\t\tif p.quote&allArithmExpr != 0 {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\tcase '.':\n\t\t\tif !p.lang.in(LangZsh) && p.quote&allArithmExpr != 0 {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\tcase '[', ']':\n\t\t\tif p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.quote&allArithmExpr != 0 {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase '+', '-', ' ', '\\t', ';', '&', '>', '<', '|', '(', ')', '\\n', '\\r':\n\t\t\tif p.quote&allKeepSpaces == 0 {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t}\n\t}\n\tp.tok, p.val = tok, p.endLit()\n}\n\n// zshNumRange peeks at the bytes after '<' to check for a zsh numeric\n// range glob pattern like <->, <5->, <-10>, or <5-10>.\nfunc (p *Parser) zshNumRange() bool {\n\t// Peeking a handful of bytes here should be enough.\n\t// TODO: This should loop for slow readers, e.g. those providing one byte at\n\t// a time. Use a loop and test it with [testing/iotest.OneByteReader].\n\tif int(p.bsp) >= len(p.bs) {\n\t\tp.fill()\n\t}\n\trest := p.bs[p.bsp:]\n\tfor len(rest) > 0 && rest[0] >= '0' && rest[0] <= '9' {\n\t\trest = rest[1:]\n\t}\n\tif len(rest) == 0 || rest[0] != '-' {\n\t\treturn false\n\t}\n\trest = rest[1:]\n\tfor len(rest) > 0 && rest[0] >= '0' && rest[0] <= '9' {\n\t\trest = rest[1:]\n\t}\n\treturn len(rest) > 0 && rest[0] == '>'\n}\n\nfunc (p *Parser) advanceLitNone(r rune) {\n\tp.eqlOffs = -1\n\ttok := _LitWord\nloop:\n\tfor p.newLit(r); r != utf8.RuneSelf; r = p.rune() {\n\t\tswitch r {\n\t\tcase ' ', '\\t', '\\n', '\\r', '&', '|', ';', ')':\n\t\t\tbreak loop\n\t\tcase '(':\n\t\t\tbreak loop\n\t\tcase '\\\\': // escaped byte follows\n\t\t\tp.rune()\n\t\tcase '>', '<':\n\t\t\tif r == '<' && p.lang.in(LangZsh) && p.zshNumRange() {\n\t\t\t\t// Zsh numeric range glob like <-> or <1-100>; consume until '>'.\n\t\t\t\tfor {\n\t\t\t\t\tif r = p.rune(); r == '>' || r == utf8.RuneSelf {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif p.peek() == '(' {\n\t\t\t\ttok = _Lit\n\t\t\t} else if p.isLitRedir() {\n\t\t\t\ttok = _LitRedir\n\t\t\t}\n\t\t\tbreak loop\n\t\tcase '`':\n\t\t\tif p.quote != subCmdBckquo {\n\t\t\t\ttok = _Lit\n\t\t\t}\n\t\t\tbreak loop\n\t\tcase '\"', '\\'', '$':\n\t\t\ttok = _Lit\n\t\t\tbreak loop\n\t\tcase '?', '*', '+', '@', '!':\n\t\t\tif p.extendedGlob() {\n\t\t\t\ttok = _Lit\n\t\t\t\tbreak loop\n\t\t\t}\n\t\tcase '=':\n\t\t\tif p.eqlOffs < 0 {\n\t\t\t\tp.eqlOffs = len(p.litBs) - 1\n\t\t\t}\n\t\tcase '[':\n\t\t\tif p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && len(p.litBs) > 1 && p.litBs[0] != '[' {\n\t\t\t\ttok = _Lit\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t}\n\t}\n\tp.tok, p.val = tok, p.endLit()\n}\n\nfunc (p *Parser) advanceLitDquote(r rune) {\n\ttok := _LitWord\nloop:\n\tfor p.newLit(r); r != utf8.RuneSelf; r = p.rune() {\n\t\tswitch r {\n\t\tcase '\"':\n\t\t\tbreak loop\n\t\tcase '\\\\': // escaped byte follows\n\t\t\tp.rune()\n\t\tcase escNewl, '`', '$':\n\t\t\ttok = _Lit\n\t\t\tbreak loop\n\t\t}\n\t}\n\tp.tok, p.val = tok, p.endLit()\n}\n\nfunc (p *Parser) advanceLitHdoc(r rune) {\n\t// Unlike the rest of nextKeepSpaces quote states, we handle escaped\n\t// newlines here. If lastTok==_Lit, then we know we're following an\n\t// escaped newline, so the first line can't end the heredoc.\n\tlastTok := p.tok\n\tfor r == escNewl {\n\t\tr = p.rune()\n\t\tlastTok = _Lit\n\t}\n\tp.pos = p.nextPos()\n\n\tp.tok = _Lit\n\tp.newLit(r)\n\tfor p.quote == hdocBodyTabs && r == '\\t' {\n\t\tr = p.rune()\n\t}\n\tlStart := len(p.litBs) - 1\n\tstop := p.hdocStops[len(p.hdocStops)-1]\n\tfor ; ; r = p.rune() {\n\t\tswitch r {\n\t\tcase escNewl, '$':\n\t\t\tp.val = p.endLit()\n\t\t\treturn\n\t\tcase '\\\\': // escaped byte follows\n\t\t\tp.rune()\n\t\tcase '`':\n\t\t\tif !p.backquoteEnd() {\n\t\t\t\tp.val = p.endLit()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase '\\n', utf8.RuneSelf:\n\t\t\tif p.parsingDoc {\n\t\t\t\tif r == utf8.RuneSelf {\n\t\t\t\t\tp.tok = _LitWord\n\t\t\t\t\tp.val = p.endLit()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else if lStart == 0 && lastTok == _Lit {\n\t\t\t\t// This line starts right after an escaped\n\t\t\t\t// newline, so it should never end the heredoc.\n\t\t\t} else if lStart >= 0 {\n\t\t\t\t// Compare the current line with the stop word.\n\t\t\t\tline := p.litBs[lStart:]\n\t\t\t\tif r != utf8.RuneSelf && len(line) > 0 {\n\t\t\t\t\tline = line[:len(line)-1] // minus trailing character\n\t\t\t\t}\n\t\t\t\tif bytes.Equal(line, stop) {\n\t\t\t\t\tp.tok = _LitWord\n\t\t\t\t\tp.val = p.endLit()[:lStart]\n\t\t\t\t\tif p.val == \"\" {\n\t\t\t\t\t\tp.tok = _Newl\n\t\t\t\t\t}\n\t\t\t\t\tp.hdocStops[len(p.hdocStops)-1] = nil\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tif r != '\\n' {\n\t\t\t\treturn // hit an unexpected EOF or closing backquote\n\t\t\t}\n\t\t\tfor p.quote == hdocBodyTabs && p.peek() == '\\t' {\n\t\t\t\tp.rune()\n\t\t\t}\n\t\t\tlStart = len(p.litBs)\n\t\t}\n\t}\n}\n\nfunc (p *Parser) quotedHdocWord() *Word {\n\tr := p.r\n\tp.newLit(r)\n\tpos := p.nextPos()\n\tstop := p.hdocStops[len(p.hdocStops)-1]\n\tfor ; ; r = p.rune() {\n\t\tif r == utf8.RuneSelf {\n\t\t\treturn nil\n\t\t}\n\t\tfor p.quote == hdocBodyTabs && r == '\\t' {\n\t\t\tr = p.rune()\n\t\t}\n\t\tlStart := len(p.litBs) - 1\n\truneLoop:\n\t\tfor {\n\t\t\tswitch r {\n\t\t\tcase utf8.RuneSelf, '\\n':\n\t\t\t\tbreak runeLoop\n\t\t\tcase '`':\n\t\t\t\tif p.backquoteEnd() {\n\t\t\t\t\tbreak runeLoop\n\t\t\t\t}\n\t\t\tcase escNewl:\n\t\t\t\tp.litBs = append(p.litBs, '\\\\', '\\n')\n\t\t\t\tbreak runeLoop\n\t\t\t}\n\t\t\tr = p.rune()\n\t\t}\n\t\tif lStart < 0 {\n\t\t\tcontinue\n\t\t}\n\t\t// Compare the current line with the stop word.\n\t\tline := p.litBs[lStart:]\n\t\tif r != utf8.RuneSelf && len(line) > 0 {\n\t\t\tline = line[:len(line)-1] // minus \\n\n\t\t}\n\t\tif bytes.Equal(line, stop) {\n\t\t\tp.hdocStops[len(p.hdocStops)-1] = nil\n\t\t\tval := p.endLit()[:lStart]\n\t\t\tif val == \"\" {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn p.wordOne(p.lit(pos, val))\n\t\t}\n\t}\n}\n\nfunc (p *Parser) advanceLitRe(r rune) {\n\tfor p.newLit(r); ; r = p.rune() {\n\t\tswitch r {\n\t\tcase '\\\\':\n\t\t\tp.rune()\n\t\tcase '(':\n\t\t\tp.rxOpenParens++\n\t\tcase ')':\n\t\t\tif p.rxOpenParens--; p.rxOpenParens < 0 {\n\t\t\t\tp.tok, p.val = _LitWord, p.endLit()\n\t\t\t\tp.quote = testExpr\n\t\t\t\treturn\n\t\t\t}\n\t\tcase ' ', '\\t', '\\r', '\\n', ';', '&', '>', '<':\n\t\t\tif p.rxOpenParens <= 0 {\n\t\t\t\tp.tok, p.val = _LitWord, p.endLit()\n\t\t\t\tp.quote = testExpr\n\t\t\t\treturn\n\t\t\t}\n\t\tcase '\"', '\\'', '$', '`':\n\t\t\tp.tok, p.val = _Lit, p.endLit()\n\t\t\treturn\n\t\tcase utf8.RuneSelf:\n\t\t\tp.tok, p.val = _LitWord, p.endLit()\n\t\t\tp.quote = noState\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc testUnaryOp(val string) UnTestOperator {\n\tswitch val {\n\tcase \"!\":\n\t\treturn TsNot\n\tcase \"-e\", \"-a\":\n\t\treturn TsExists\n\tcase \"-f\":\n\t\treturn TsRegFile\n\tcase \"-d\":\n\t\treturn TsDirect\n\tcase \"-c\":\n\t\treturn TsCharSp\n\tcase \"-b\":\n\t\treturn TsBlckSp\n\tcase \"-p\":\n\t\treturn TsNmPipe\n\tcase \"-S\":\n\t\treturn TsSocket\n\tcase \"-L\", \"-h\":\n\t\treturn TsSmbLink\n\tcase \"-k\":\n\t\treturn TsSticky\n\tcase \"-g\":\n\t\treturn TsGIDSet\n\tcase \"-u\":\n\t\treturn TsUIDSet\n\tcase \"-G\":\n\t\treturn TsGrpOwn\n\tcase \"-O\":\n\t\treturn TsUsrOwn\n\tcase \"-N\":\n\t\treturn TsModif\n\tcase \"-r\":\n\t\treturn TsRead\n\tcase \"-w\":\n\t\treturn TsWrite\n\tcase \"-x\":\n\t\treturn TsExec\n\tcase \"-s\":\n\t\treturn TsNoEmpty\n\tcase \"-t\":\n\t\treturn TsFdTerm\n\tcase \"-z\":\n\t\treturn TsEmpStr\n\tcase \"-n\":\n\t\treturn TsNempStr\n\tcase \"-o\":\n\t\treturn TsOptSet\n\tcase \"-v\":\n\t\treturn TsVarSet\n\tcase \"-R\":\n\t\treturn TsRefVar\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc testBinaryOp(val string) BinTestOperator {\n\tswitch val {\n\tcase \"=\":\n\t\treturn TsMatchShort\n\tcase \"==\":\n\t\treturn TsMatch\n\tcase \"!=\":\n\t\treturn TsNoMatch\n\tcase \"=~\":\n\t\treturn TsReMatch\n\tcase \"-nt\":\n\t\treturn TsNewer\n\tcase \"-ot\":\n\t\treturn TsOlder\n\tcase \"-ef\":\n\t\treturn TsDevIno\n\tcase \"-eq\":\n\t\treturn TsEql\n\tcase \"-ne\":\n\t\treturn TsNeq\n\tcase \"-le\":\n\t\treturn TsLeq\n\tcase \"-ge\":\n\t\treturn TsGeq\n\tcase \"-lt\":\n\t\treturn TsLss\n\tcase \"-gt\":\n\t\treturn TsGtr\n\tdefault:\n\t\treturn 0\n\t}\n}\n"
  },
  {
    "path": "syntax/nodes.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Node represents a syntax tree node.\ntype Node interface {\n\t// Pos returns the position of the first character of the node. Comments\n\t// are ignored, except if the node is a [*File].\n\tPos() Pos\n\t// End returns the position of the character immediately after the node.\n\t// If the character is a newline, the line number won't cross into the\n\t// next line. Comments are ignored, except if the node is a [*File].\n\tEnd() Pos\n}\n\n// File represents a shell source file.\ntype File struct {\n\tName string\n\n\tStmts []*Stmt\n\tLast  []Comment\n}\n\nfunc (f *File) Pos() Pos { return stmtsPos(f.Stmts, f.Last) }\nfunc (f *File) End() Pos { return stmtsEnd(f.Stmts, f.Last) }\n\nfunc stmtsPos(stmts []*Stmt, last []Comment) Pos {\n\tif len(stmts) > 0 {\n\t\ts := stmts[0]\n\t\tsPos := s.Pos()\n\t\tif len(s.Comments) > 0 {\n\t\t\tif cPos := s.Comments[0].Pos(); sPos.After(cPos) {\n\t\t\t\treturn cPos\n\t\t\t}\n\t\t}\n\t\treturn sPos\n\t}\n\tif len(last) > 0 {\n\t\treturn last[0].Pos()\n\t}\n\treturn Pos{}\n}\n\nfunc stmtsEnd(stmts []*Stmt, last []Comment) Pos {\n\tif len(last) > 0 {\n\t\treturn last[len(last)-1].End()\n\t}\n\tif len(stmts) > 0 {\n\t\ts := stmts[len(stmts)-1]\n\t\tsEnd := s.End()\n\t\tif len(s.Comments) > 0 {\n\t\t\tif cEnd := s.Comments[0].End(); cEnd.After(sEnd) {\n\t\t\t\treturn cEnd\n\t\t\t}\n\t\t}\n\t\treturn sEnd\n\t}\n\treturn Pos{}\n}\n\n// Pos is a position within a shell source file.\ntype Pos struct {\n\toffs, lineCol uint32\n}\n\nconst (\n\t// Offsets use 32 bits for a reasonable amount of precision.\n\t// We reserve a few of the highest values to represent types of invalid positions.\n\t// We leave some space before the real uint32 maximum so that we can easily detect\n\t// when arithmetic on invalid positions is done by mistake.\n\toffsetRecovered = math.MaxUint32 - 10\n\toffsetMax       = math.MaxUint32 - 11\n\n\t// We used to split line and column numbers evenly in 16 bits, but line numbers\n\t// are significantly more important in practice. Use more bits for them.\n\n\tlineBitSize = 18\n\tlineMax     = (1 << lineBitSize) - 1\n\n\tcolBitSize = 32 - lineBitSize\n\tcolMax     = (1 << colBitSize) - 1\n\tcolBitMask = colMax\n)\n\n// TODO(v4): consider using uint32 for Offset/Line/Col to better represent bit sizes.\n// Or go with int64, which more closely resembles portable \"sizes\" elsewhere.\n// The latter is probably nicest, as then we can change the number of internal\n// bits later, and we can also do overflow checks for the user in NewPos.\n\n// NewPos creates a position with the given offset, line, and column.\n//\n// Note that [Pos] uses a limited number of bits to store these numbers.\n// If line or column overflow their allocated space, they are replaced with 0.\nfunc NewPos(offset, line, column uint) Pos {\n\t// Basic protection against offset overflow;\n\t// note that an offset of 0 is valid, so we leave the maximum.\n\toffset = min(offset, offsetMax)\n\tif line > lineMax {\n\t\tline = 0 // protect against overflows; rendered as \"?\"\n\t}\n\tif column > colMax {\n\t\tcolumn = 0 // protect against overflows; rendered as \"?\"\n\t}\n\treturn Pos{\n\t\toffs:    uint32(offset),\n\t\tlineCol: (uint32(line) << colBitSize) | uint32(column),\n\t}\n}\n\n// Offset returns the byte offset of the position in the original source file.\n// Byte offsets start at 0. Invalid positions always report the offset 0.\n//\n// Offset has basic protection against overflows; if an input is too large,\n// offset numbers will stop increasing past a very large number.\nfunc (p Pos) Offset() uint {\n\tif p.offs > offsetMax {\n\t\treturn 0 // invalid\n\t}\n\treturn uint(p.offs)\n}\n\n// Line returns the line number of the position, starting at 1.\n// Invalid positions always report the line number 0.\n//\n// Line is protected against overflows; if an input has too many lines, extra\n// lines will have a line number of 0, rendered as \"?\" by [Pos.String].\nfunc (p Pos) Line() uint { return uint(p.lineCol >> colBitSize) }\n\n// Col returns the column number of the position, starting at 1. It counts in\n// bytes. Invalid positions always report the column number 0.\n//\n// Col is protected against overflows; if an input line has too many columns,\n// extra columns will have a column number of 0, rendered as \"?\" by [Pos.String].\nfunc (p Pos) Col() uint { return uint(p.lineCol & colBitMask) }\n\nfunc (p Pos) String() string {\n\tvar b strings.Builder\n\tif line := p.Line(); line > 0 {\n\t\tb.WriteString(strconv.FormatUint(uint64(line), 10))\n\t} else {\n\t\tb.WriteByte('?')\n\t}\n\tb.WriteByte(':')\n\tif col := p.Col(); col > 0 {\n\t\tb.WriteString(strconv.FormatUint(uint64(col), 10))\n\t} else {\n\t\tb.WriteByte('?')\n\t}\n\treturn b.String()\n}\n\n// IsValid reports whether the position contains useful position information.\n// Some positions returned via [Parse] may be invalid: for example, [Stmt.Semicolon]\n// will only be valid if a statement contained a closing token such as ';'.\n//\n// Recovered positions, as reported by [Pos.IsRecovered], are not considered valid\n// given that they don't contain position information.\nfunc (p Pos) IsValid() bool {\n\treturn p.offs <= offsetMax && p.lineCol != 0\n}\n\nvar recoveredPos = Pos{offs: offsetRecovered}\n\n// IsRecovered reports whether the position that the token or node belongs to\n// was missing in the original input and recovered via [RecoverErrors].\nfunc (p Pos) IsRecovered() bool { return p == recoveredPos }\n\n// After reports whether the position p is after p2. It is a more expressive\n// version of p.Offset() > p2.Offset().\n// It always returns false if p is an invalid position.\nfunc (p Pos) After(p2 Pos) bool {\n\tif !p.IsValid() {\n\t\treturn false\n\t}\n\treturn p.offs > p2.offs\n}\n\nfunc posAddCol(p Pos, n int) Pos {\n\tif !p.IsValid() {\n\t\treturn p\n\t}\n\t// TODO: guard against overflows\n\tp.lineCol += uint32(n)\n\tp.offs += uint32(n)\n\treturn p\n}\n\nfunc posMax(p1, p2 Pos) Pos {\n\tif p2.After(p1) {\n\t\treturn p2\n\t}\n\treturn p1\n}\n\n// Comment represents a single comment on a single line.\ntype Comment struct {\n\tHash Pos\n\tText string\n}\n\nfunc (c *Comment) Pos() Pos { return c.Hash }\nfunc (c *Comment) End() Pos { return posAddCol(c.Hash, 1+len(c.Text)) }\n\n// Stmt represents a statement, also known as a \"complete command\". It is\n// compromised of a command and other components that may come before or after\n// it.\ntype Stmt struct {\n\tComments   []Comment\n\tCmd        Command\n\tPosition   Pos\n\tSemicolon  Pos  // position of ';', '&', or '|&', if any\n\tNegated    bool // ! stmt\n\tBackground bool // stmt &\n\tCoprocess  bool // mksh's |&\n\tDisown     bool // zsh's &| or &!\n\n\tRedirs []*Redirect // stmt >a <b\n}\n\nfunc (s *Stmt) Pos() Pos { return s.Position }\nfunc (s *Stmt) End() Pos {\n\tif s.Semicolon.IsValid() {\n\t\tend := posAddCol(s.Semicolon, 1) // ';' or '&'\n\t\tif s.Coprocess || s.Disown {\n\t\t\tend = posAddCol(end, 1) // '|&' or '&|' or '&!'\n\t\t}\n\t\treturn end\n\t}\n\tend := s.Position\n\tif s.Negated {\n\t\tend = posAddCol(end, 1)\n\t}\n\tif s.Cmd != nil {\n\t\tend = s.Cmd.End()\n\t}\n\tif len(s.Redirs) > 0 {\n\t\tend = posMax(end, s.Redirs[len(s.Redirs)-1].End())\n\t}\n\treturn end\n}\n\n// Command represents all nodes that are simple or compound commands, including\n// function declarations.\n//\n// These are [*CallExpr], [*IfClause], [*WhileClause], [*ForClause], [*CaseClause],\n// [*Block], [*Subshell], [*BinaryCmd], [*FuncDecl], [*ArithmCmd], [*TestClause],\n// [*DeclClause], [*LetClause], [*TimeClause], and [*CoprocClause].\ntype Command interface {\n\tNode\n\tcommandNode()\n}\n\nfunc (*CallExpr) commandNode()     {}\nfunc (*IfClause) commandNode()     {}\nfunc (*WhileClause) commandNode()  {}\nfunc (*ForClause) commandNode()    {}\nfunc (*CaseClause) commandNode()   {}\nfunc (*Block) commandNode()        {}\nfunc (*Subshell) commandNode()     {}\nfunc (*BinaryCmd) commandNode()    {}\nfunc (*FuncDecl) commandNode()     {}\nfunc (*ArithmCmd) commandNode()    {}\nfunc (*TestClause) commandNode()   {}\nfunc (*DeclClause) commandNode()   {}\nfunc (*LetClause) commandNode()    {}\nfunc (*TimeClause) commandNode()   {}\nfunc (*CoprocClause) commandNode() {}\nfunc (*TestDecl) commandNode()     {}\n\n// Assign represents an assignment to a variable.\n//\n// Here and elsewhere, Index can mean either an index expression into an indexed\n// array, or a string key into an associative array.\n//\n// If Index is non-nil, the value will be a word and not an array as nested\n// arrays are not allowed.\n//\n// If Naked is true and Name is nil, the assignment is part of a [DeclClause] and\n// the argument (in the Value field) will be evaluated at run-time. This\n// includes parameter expansions, which may expand to assignments or options.\ntype Assign struct {\n\tAppend bool       // +=\n\tNaked  bool       // without '='\n\tName   *Lit       // must be a valid name\n\tIndex  ArithmExpr // [i], [\"k\"]\n\tValue  *Word      // =val\n\tArray  *ArrayExpr // =(arr)\n}\n\nfunc (a *Assign) Pos() Pos {\n\tif a.Name == nil {\n\t\treturn a.Value.Pos()\n\t}\n\treturn a.Name.Pos()\n}\n\nfunc (a *Assign) End() Pos {\n\tif a.Value != nil {\n\t\treturn a.Value.End()\n\t}\n\tif a.Array != nil {\n\t\treturn a.Array.End()\n\t}\n\tif a.Index != nil {\n\t\treturn posAddCol(a.Index.End(), 2)\n\t}\n\tif a.Naked {\n\t\treturn a.Name.End()\n\t}\n\treturn posAddCol(a.Name.End(), 1)\n}\n\n// Redirect represents an input/output redirection.\ntype Redirect struct {\n\tOpPos Pos\n\tOp    RedirOperator\n\tN     *Lit  // fd>, or {varname}> in Bash\n\tWord  *Word // >word\n\tHdoc  *Word // here-document body\n}\n\nfunc (r *Redirect) Pos() Pos {\n\tif r.N != nil {\n\t\treturn r.N.Pos()\n\t}\n\treturn r.OpPos\n}\n\nfunc (r *Redirect) End() Pos {\n\tif r.Hdoc != nil {\n\t\treturn r.Hdoc.End()\n\t}\n\treturn r.Word.End()\n}\n\n// CallExpr represents a command execution or function call, otherwise known as\n// a \"simple command\".\n//\n// If Args is empty, Assigns apply to the shell environment. Otherwise, they are\n// variables that cannot be arrays and which only apply to the call.\ntype CallExpr struct {\n\tAssigns []*Assign // a=x b=y args\n\tArgs    []*Word\n}\n\nfunc (c *CallExpr) Pos() Pos {\n\tif len(c.Assigns) > 0 {\n\t\treturn c.Assigns[0].Pos()\n\t}\n\treturn c.Args[0].Pos()\n}\n\nfunc (c *CallExpr) End() Pos {\n\tif len(c.Args) == 0 {\n\t\treturn c.Assigns[len(c.Assigns)-1].End()\n\t}\n\treturn c.Args[len(c.Args)-1].End()\n}\n\n// Subshell represents a series of commands that should be executed in a nested\n// shell environment.\ntype Subshell struct {\n\tLparen, Rparen Pos\n\n\tStmts []*Stmt\n\tLast  []Comment\n}\n\nfunc (s *Subshell) Pos() Pos { return s.Lparen }\nfunc (s *Subshell) End() Pos { return posAddCol(s.Rparen, 1) }\n\n// Block represents a series of commands that should be executed in a nested\n// scope. It is essentially a list of statements within curly braces.\ntype Block struct {\n\tLbrace, Rbrace Pos\n\n\tStmts []*Stmt\n\tLast  []Comment\n}\n\nfunc (b *Block) Pos() Pos { return b.Lbrace }\nfunc (b *Block) End() Pos { return posAddCol(b.Rbrace, 1) }\n\n// IfClause represents an if statement.\ntype IfClause struct {\n\tPosition Pos // position of the starting \"if\", \"elif\", or \"else\" token\n\tThenPos  Pos // position of \"then\", empty if this is an \"else\"\n\tFiPos    Pos // position of \"fi\", shared with .Else if non-nil\n\n\tCond     []*Stmt\n\tCondLast []Comment\n\tThen     []*Stmt\n\tThenLast []Comment\n\n\tElse *IfClause // if non-nil, an \"elif\" or an \"else\"\n\n\tLast []Comment // comments on the first \"elif\", \"else\", or \"fi\"\n}\n\nfunc (c *IfClause) Pos() Pos { return c.Position }\nfunc (c *IfClause) End() Pos { return posAddCol(c.FiPos, 2) }\n\n// WhileClause represents a while or an until clause.\ntype WhileClause struct {\n\tWhilePos, DoPos, DonePos Pos\n\tUntil                    bool\n\n\tCond     []*Stmt\n\tCondLast []Comment\n\tDo       []*Stmt\n\tDoLast   []Comment\n}\n\nfunc (w *WhileClause) Pos() Pos { return w.WhilePos }\nfunc (w *WhileClause) End() Pos { return posAddCol(w.DonePos, 4) }\n\n// ForClause represents a for or a select clause. The latter is only present in\n// Bash.\ntype ForClause struct {\n\tForPos, DoPos, DonePos Pos\n\tSelect                 bool\n\tBraces                 bool // deprecated form with { } instead of do/done\n\tLoop                   Loop\n\n\tDo     []*Stmt\n\tDoLast []Comment\n}\n\nfunc (f *ForClause) Pos() Pos { return f.ForPos }\nfunc (f *ForClause) End() Pos { return posAddCol(f.DonePos, 4) }\n\n// Loop holds either [*WordIter] or [*CStyleLoop].\ntype Loop interface {\n\tNode\n\tloopNode()\n}\n\nfunc (*WordIter) loopNode()   {}\nfunc (*CStyleLoop) loopNode() {}\n\n// WordIter represents the iteration of a variable over a series of words in a\n// for clause. If InPos is an invalid position, the \"in\" token was missing, so\n// the iteration is over the shell's positional parameters.\ntype WordIter struct {\n\tName  *Lit\n\tInPos Pos // position of \"in\"\n\tItems []*Word\n}\n\nfunc (w *WordIter) Pos() Pos { return w.Name.Pos() }\nfunc (w *WordIter) End() Pos {\n\tif len(w.Items) > 0 {\n\t\treturn wordLastEnd(w.Items)\n\t}\n\treturn posMax(w.Name.End(), posAddCol(w.InPos, 2))\n}\n\n// CStyleLoop represents the behavior of a for clause similar to the C\n// language.\n//\n// This node will only appear with [LangBash].\ntype CStyleLoop struct {\n\tLparen, Rparen Pos\n\t// Init, Cond, Post can each be nil, if the for loop construct omits it.\n\tInit, Cond, Post ArithmExpr\n}\n\nfunc (c *CStyleLoop) Pos() Pos { return c.Lparen }\nfunc (c *CStyleLoop) End() Pos { return posAddCol(c.Rparen, 2) }\n\n// BinaryCmd represents a binary expression between two statements.\ntype BinaryCmd struct {\n\tOpPos Pos\n\tOp    BinCmdOperator\n\tX, Y  *Stmt\n}\n\nfunc (b *BinaryCmd) Pos() Pos { return b.X.Pos() }\nfunc (b *BinaryCmd) End() Pos { return b.Y.End() }\n\n// FuncDecl represents the declaration of a function.\ntype FuncDecl struct {\n\tPosition Pos\n\tRsrvWord bool // non-posix \"function f\" style\n\tParens   bool // with () parentheses, can only be false when RsrvWord==true\n\n\t// Only one of these is set at a time.\n\t// Neither is set when declaring an anonymous func with [LangZsh].\n\t// TODO(v4): join these, even if it's mildly annoying to non-Zsh users.\n\tName  *Lit\n\tNames []*Lit // When declaring many func names with [LangZsh].\n\n\tBody *Stmt\n}\n\nfunc (f *FuncDecl) Pos() Pos { return f.Position }\nfunc (f *FuncDecl) End() Pos { return f.Body.End() }\n\n// Word represents a shell word, containing one or more word parts contiguous to\n// each other. The word is delimited by word boundaries, such as spaces,\n// newlines, semicolons, or parentheses.\ntype Word struct {\n\tParts []WordPart\n}\n\nfunc (w *Word) Pos() Pos { return w.Parts[0].Pos() }\nfunc (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() }\n\n// Lit returns the word as a string when it is a simple literal,\n// made up of [*Lit] word parts only.\n// An empty string is returned otherwise.\n//\n// For example, the word \"foo\" will return \"foo\",\n// but the word \"foo${bar}\" will return \"\".\nfunc (w *Word) Lit() string {\n\t// In the usual case, we'll have either a single part that's a literal,\n\t// or one of the parts being a non-literal. Using strings.Join instead\n\t// of a strings.Builder avoids extra work in these cases, since a single\n\t// part is a shortcut, and many parts don't incur string copies.\n\tlits := make([]string, 0, 1)\n\tfor _, part := range w.Parts {\n\t\tlit, ok := part.(*Lit)\n\t\tif !ok {\n\t\t\treturn \"\"\n\t\t}\n\t\tlits = append(lits, lit.Value)\n\t}\n\treturn strings.Join(lits, \"\")\n}\n\n// WordPart represents all nodes that can form part of a word.\n//\n// These are [*Lit], [*SglQuoted], [*DblQuoted], [*ParamExp], [*CmdSubst], [*ArithmExp],\n// [*ProcSubst], and [*ExtGlob].\ntype WordPart interface {\n\tNode\n\twordPartNode()\n}\n\nfunc (*Lit) wordPartNode()       {}\nfunc (*SglQuoted) wordPartNode() {}\nfunc (*DblQuoted) wordPartNode() {}\nfunc (*ParamExp) wordPartNode()  {}\nfunc (*CmdSubst) wordPartNode()  {}\nfunc (*ArithmExp) wordPartNode() {}\nfunc (*ProcSubst) wordPartNode() {}\nfunc (*ExtGlob) wordPartNode()   {}\nfunc (*BraceExp) wordPartNode()  {}\n\n// Lit represents a string literal.\n//\n// Note that a parsed string literal may not appear as-is in the original source\n// code, as it is possible to split literals by escaping newlines. The splitting\n// is lost, but the end position is not.\ntype Lit struct {\n\tValuePos, ValueEnd Pos\n\tValue              string\n}\n\nfunc (l *Lit) Pos() Pos { return l.ValuePos }\nfunc (l *Lit) End() Pos { return l.ValueEnd }\n\n// SglQuoted represents a string within single quotes.\ntype SglQuoted struct {\n\tLeft, Right Pos\n\tDollar      bool // $''\n\tValue       string\n}\n\nfunc (q *SglQuoted) Pos() Pos { return q.Left }\nfunc (q *SglQuoted) End() Pos { return posAddCol(q.Right, 1) }\n\n// DblQuoted represents a list of nodes within double quotes.\ntype DblQuoted struct {\n\tLeft, Right Pos\n\tDollar      bool // $\"\"\n\tParts       []WordPart\n}\n\nfunc (q *DblQuoted) Pos() Pos { return q.Left }\nfunc (q *DblQuoted) End() Pos { return posAddCol(q.Right, 1) }\n\n// CmdSubst represents a command substitution.\ntype CmdSubst struct {\n\tLeft, Right Pos\n\n\tStmts []*Stmt\n\tLast  []Comment\n\n\tBackquotes bool // deprecated `foo`\n\tTempFile   bool // mksh's ${ foo;}\n\tReplyVar   bool // mksh's ${|foo;}\n}\n\nfunc (c *CmdSubst) Pos() Pos { return c.Left }\nfunc (c *CmdSubst) End() Pos { return posAddCol(c.Right, 1) }\n\n// ParamExp represents a parameter expansion.\ntype ParamExp struct {\n\tDollar, Rbrace Pos\n\n\t// TODO(v4): replace Short for !Rbrace.IsValid()\n\n\tShort bool // $a instead of ${a}\n\n\tFlags *Lit // ${(flags)a} with [LangZsh]\n\n\t// Only one of these is set at a time.\n\t// TODO(v4): perhaps use an Operator token here,\n\t// given how we've grown the number of booleans\n\t// TODO(v4): rename Excl to reflect its purpose\n\tExcl   bool // ${!a}\n\tLength bool // ${#a}\n\tWidth  bool // mksh's ${%a}\n\tIsSet  bool // ${+a} with [LangZsh]\n\n\t// Only one of these is set at a time.\n\t// TODO(v4): consider joining Param and NestedParam into a single field,\n\t// even if that would be mildly annoying to non-Zsh users.\n\tParam *Lit\n\t// A nested parameter expression in the form of [*ParamExp] or [*CmdSubst],\n\t// or either of those in a [*DblQuoted]. Only possible with [LangZsh].\n\tNestedParam WordPart\n\n\t// TODO(v4): rename Index to Subscript, which better matches bash and zsh terminology\n\n\tIndex ArithmExpr // ${a[i]}, ${a[\"k\"]}, or a ${a[i,j]} slice with [LangZsh]\n\n\t// Only one of these is set at a time.\n\t// TODO(v4): consider joining these in a single \"expansion\" field/type,\n\t// because it should be impossible for multiple to be set at once,\n\t// and a flat structure like this takes up more space.\n\tModifiers []*Lit           // ${a:h2} with [LangZsh]\n\tSlice     *Slice           // ${a:x:y}\n\tRepl      *Replace         // ${a/x/y}\n\tNames     ParNamesOperator // ${!prefix*} or ${!prefix@}\n\tExp       *Expansion       // ${a:-b}, ${a#b}, etc\n}\n\n// simple returns true if the parameter expansion is of the form $name or ${name},\n// only expanding a name without any further logic.\nfunc (p *ParamExp) simple() bool {\n\treturn p.Flags == nil &&\n\t\t!p.Excl && !p.Length && !p.Width && !p.IsSet &&\n\t\tp.NestedParam == nil && p.Index == nil &&\n\t\tlen(p.Modifiers) == 0 && p.Slice == nil &&\n\t\tp.Repl == nil && p.Names == 0 && p.Exp == nil\n}\n\nfunc (p *ParamExp) Pos() Pos {\n\tif p.Dollar.IsValid() {\n\t\treturn p.Dollar\n\t}\n\treturn p.Param.Pos()\n}\nfunc (p *ParamExp) End() Pos {\n\tif !p.Short {\n\t\treturn posAddCol(p.Rbrace, 1)\n\t}\n\t// In short mode, we can only end in either an index or a simple name.\n\tif p.Index != nil {\n\t\treturn posAddCol(p.Index.End(), 1)\n\t}\n\treturn p.Param.End()\n}\n\nfunc (p *ParamExp) nakedIndex() bool {\n\t// A naked index is arr[x] inside arithmetic, without a leading '$'.\n\t// In that case Dollar is unset, unlike $arr[x] where it holds the '$' position.\n\treturn p.Short && p.Index != nil && !p.Dollar.IsValid()\n}\n\n// Slice represents a character slicing expression inside a [ParamExp].\n//\n// This node will only appear with [LangBash] and [LangMirBSDKorn].\n// [LangZsh] uses a [BinaryArithm] with [Comma] in [ParamExp.Index] instead.\ntype Slice struct {\n\tOffset, Length ArithmExpr\n}\n\n// Replace represents a search and replace expression inside a [ParamExp].\ntype Replace struct {\n\tAll        bool\n\tOrig, With *Word\n}\n\n// Expansion represents string manipulation in a [ParamExp] other than those\n// covered by [Replace].\ntype Expansion struct {\n\tOp   ParExpOperator\n\tWord *Word\n}\n\n// ArithmExp represents an arithmetic expansion.\ntype ArithmExp struct {\n\tLeft, Right Pos\n\tBracket     bool // deprecated $[expr] form\n\tUnsigned    bool // mksh's $((# expr))\n\n\tX ArithmExpr\n}\n\nfunc (a *ArithmExp) Pos() Pos { return a.Left }\nfunc (a *ArithmExp) End() Pos {\n\tif a.Bracket {\n\t\treturn posAddCol(a.Right, 1)\n\t}\n\treturn posAddCol(a.Right, 2)\n}\n\n// ArithmCmd represents an arithmetic command.\n//\n// This node will only appear with [LangBash] and [LangMirBSDKorn].\ntype ArithmCmd struct {\n\tLeft, Right Pos\n\tUnsigned    bool // mksh's ((# expr))\n\n\tX ArithmExpr\n}\n\nfunc (a *ArithmCmd) Pos() Pos { return a.Left }\nfunc (a *ArithmCmd) End() Pos { return posAddCol(a.Right, 2) }\n\n// ArithmExpr represents all nodes that form arithmetic expressions.\n//\n// These are [*BinaryArithm], [*UnaryArithm], [*ParenArithm], [*FlagsArithm], and [*Word].\ntype ArithmExpr interface {\n\tNode\n\tarithmExprNode()\n}\n\nfunc (*BinaryArithm) arithmExprNode() {}\nfunc (*UnaryArithm) arithmExprNode()  {}\nfunc (*ParenArithm) arithmExprNode()  {}\nfunc (*FlagsArithm) arithmExprNode()  {}\nfunc (*Word) arithmExprNode()         {}\n\n// BinaryArithm represents a binary arithmetic expression.\n//\n// If Op is any assign operator, X will be a word with a single [*Lit] whose value\n// is a valid name.\n//\n// Ternary operators like \"a ? b : c\" are fit into this structure. Thus, if\n// Op==[TernQuest], Y will be a [*BinaryArithm] with Op==[TernColon].\n// [TernColon] does not appear in any other scenario.\ntype BinaryArithm struct {\n\tOpPos Pos\n\tOp    BinAritOperator\n\tX, Y  ArithmExpr\n}\n\nfunc (b *BinaryArithm) Pos() Pos { return b.X.Pos() }\nfunc (b *BinaryArithm) End() Pos { return b.Y.End() }\n\n// UnaryArithm represents an unary arithmetic expression. The unary operator\n// may come before or after the sub-expression.\n//\n// If Op is [Inc] or [Dec], X will be a word with a single [*Lit] whose value is a\n// valid name.\ntype UnaryArithm struct {\n\tOpPos Pos\n\tOp    UnAritOperator\n\tPost  bool\n\tX     ArithmExpr\n}\n\nfunc (u *UnaryArithm) Pos() Pos {\n\tif u.Post {\n\t\treturn u.X.Pos()\n\t}\n\treturn u.OpPos\n}\n\nfunc (u *UnaryArithm) End() Pos {\n\tif u.Post {\n\t\treturn posAddCol(u.OpPos, 2)\n\t}\n\treturn u.X.End()\n}\n\n// ParenArithm represents an arithmetic expression within parentheses.\ntype ParenArithm struct {\n\tLparen, Rparen Pos\n\n\tX ArithmExpr\n}\n\nfunc (p *ParenArithm) Pos() Pos { return p.Lparen }\nfunc (p *ParenArithm) End() Pos { return posAddCol(p.Rparen, 1) }\n\n// FlagsArithm represents zsh subscript flags attached to an arithmetic expression,\n// such as ${array[(flags)expr]}.\n//\n// This node will only appear with [LangZsh].\ntype FlagsArithm struct {\n\tFlags *Lit\n\tX     ArithmExpr\n}\n\nfunc (z *FlagsArithm) Pos() Pos { return posAddCol(z.Flags.Pos(), -1) }\nfunc (z *FlagsArithm) End() Pos {\n\tif z.X != nil {\n\t\treturn z.X.End()\n\t}\n\treturn posAddCol(z.Flags.End(), 1) // closing paren\n}\n\n// CaseClause represents a case (switch) clause.\ntype CaseClause struct {\n\tCase, In, Esac Pos\n\tBraces         bool // deprecated mksh form with braces instead of in/esac\n\n\tWord  *Word\n\tItems []*CaseItem\n\tLast  []Comment\n}\n\nfunc (c *CaseClause) Pos() Pos { return c.Case }\nfunc (c *CaseClause) End() Pos { return posAddCol(c.Esac, 4) }\n\n// CaseItem represents a pattern list (case) within a [CaseClause].\ntype CaseItem struct {\n\tOp       CaseOperator\n\tOpPos    Pos // unset if it was finished by \"esac\"\n\tComments []Comment\n\tPatterns []*Word\n\n\tStmts []*Stmt\n\tLast  []Comment\n}\n\nfunc (c *CaseItem) Pos() Pos { return c.Patterns[0].Pos() }\nfunc (c *CaseItem) End() Pos {\n\tif c.OpPos.IsValid() {\n\t\treturn posAddCol(c.OpPos, len(c.Op.String()))\n\t}\n\treturn stmtsEnd(c.Stmts, c.Last)\n}\n\n// TestClause represents a Bash extended test clause.\n//\n// This node will only appear with [LangBash] and [LangMirBSDKorn].\ntype TestClause struct {\n\tLeft, Right Pos\n\n\tX TestExpr\n}\n\nfunc (t *TestClause) Pos() Pos { return t.Left }\nfunc (t *TestClause) End() Pos { return posAddCol(t.Right, 2) }\n\n// TestExpr represents all nodes that form test expressions.\n//\n// These are [*BinaryTest], [*UnaryTest], [*ParenTest], and [*Word].\ntype TestExpr interface {\n\tNode\n\ttestExprNode()\n}\n\nfunc (*BinaryTest) testExprNode() {}\nfunc (*UnaryTest) testExprNode()  {}\nfunc (*ParenTest) testExprNode()  {}\nfunc (*Word) testExprNode()       {}\n\n// BinaryTest represents a binary test expression.\ntype BinaryTest struct {\n\tOpPos Pos\n\tOp    BinTestOperator\n\tX, Y  TestExpr\n}\n\nfunc (b *BinaryTest) Pos() Pos { return b.X.Pos() }\nfunc (b *BinaryTest) End() Pos { return b.Y.End() }\n\n// UnaryTest represents a unary test expression. The unary operator may come\n// before or after the sub-expression.\ntype UnaryTest struct {\n\tOpPos Pos\n\tOp    UnTestOperator\n\tX     TestExpr\n}\n\nfunc (u *UnaryTest) Pos() Pos { return u.OpPos }\nfunc (u *UnaryTest) End() Pos { return u.X.End() }\n\n// ParenTest represents a test expression within parentheses.\ntype ParenTest struct {\n\tLparen, Rparen Pos\n\n\tX TestExpr\n}\n\nfunc (p *ParenTest) Pos() Pos { return p.Lparen }\nfunc (p *ParenTest) End() Pos { return posAddCol(p.Rparen, 1) }\n\n// DeclClause represents a Bash declare clause.\n//\n// Args can contain a mix of regular and naked assignments. The naked\n// assignments can represent either options or variable names.\n//\n// This node will only appear with [LangBash].\ntype DeclClause struct {\n\t// Variant is one of \"declare\", \"local\", \"export\", \"readonly\",\n\t// \"typeset\", or \"nameref\".\n\tVariant *Lit\n\tArgs    []*Assign\n}\n\nfunc (d *DeclClause) Pos() Pos { return d.Variant.Pos() }\nfunc (d *DeclClause) End() Pos {\n\tif len(d.Args) > 0 {\n\t\treturn d.Args[len(d.Args)-1].End()\n\t}\n\treturn d.Variant.End()\n}\n\n// ArrayExpr represents a Bash array expression.\n//\n// This node will only appear with [LangBash].\ntype ArrayExpr struct {\n\tLparen, Rparen Pos\n\n\tElems []*ArrayElem\n\tLast  []Comment\n}\n\nfunc (a *ArrayExpr) Pos() Pos { return a.Lparen }\nfunc (a *ArrayExpr) End() Pos { return posAddCol(a.Rparen, 1) }\n\n// ArrayElem represents a Bash array element.\n//\n// Index can be nil; for example, declare -a x=(value).\n// Value can be nil; for example, declare -A x=([index]=).\n// Finally, neither can be nil; for example, declare -A x=([index]=value)\ntype ArrayElem struct {\n\tIndex    ArithmExpr\n\tValue    *Word\n\tComments []Comment\n}\n\nfunc (a *ArrayElem) Pos() Pos {\n\tif a.Index != nil {\n\t\treturn a.Index.Pos()\n\t}\n\treturn a.Value.Pos()\n}\n\nfunc (a *ArrayElem) End() Pos {\n\tif a.Value != nil {\n\t\treturn a.Value.End()\n\t}\n\treturn posAddCol(a.Index.Pos(), 1)\n}\n\n// TODO(v4): the expand package has to stringify ExtGlob again,\n// and we don't gain much from a WordPart node anyway;\n// make these opaque literals like we did for zsh glob qualifiers.\n\n// ExtGlob represents a Bash extended globbing expression. Note that these are\n// parsed independently of whether or not `shopt -s extglob` has been used,\n// as the parser runs statically and independently of any interpreter.\n//\n// This node will only appear with [LangBash] and [LangMirBSDKorn].\ntype ExtGlob struct {\n\tOpPos   Pos\n\tOp      GlobOperator\n\tPattern *Lit\n}\n\nfunc (e *ExtGlob) Pos() Pos { return e.OpPos }\nfunc (e *ExtGlob) End() Pos { return posAddCol(e.Pattern.End(), 1) }\n\n// ProcSubst represents a Bash process substitution.\n//\n// This node will only appear with [LangBash].\ntype ProcSubst struct {\n\tOpPos, Rparen Pos\n\tOp            ProcOperator\n\n\tStmts []*Stmt\n\tLast  []Comment\n}\n\nfunc (s *ProcSubst) Pos() Pos { return s.OpPos }\nfunc (s *ProcSubst) End() Pos { return posAddCol(s.Rparen, 1) }\n\n// TimeClause represents a Bash time clause. PosixFormat corresponds to the -p\n// flag.\n//\n// This node will only appear with [LangBash] and [LangMirBSDKorn].\ntype TimeClause struct {\n\tTime        Pos\n\tPosixFormat bool\n\tStmt        *Stmt\n}\n\nfunc (c *TimeClause) Pos() Pos { return c.Time }\nfunc (c *TimeClause) End() Pos {\n\tif c.Stmt == nil {\n\t\treturn posAddCol(c.Time, 4)\n\t}\n\treturn c.Stmt.End()\n}\n\n// CoprocClause represents a Bash coproc clause.\n//\n// This node will only appear with [LangBash].\ntype CoprocClause struct {\n\tCoproc Pos\n\tName   *Word\n\tStmt   *Stmt\n}\n\nfunc (c *CoprocClause) Pos() Pos { return c.Coproc }\nfunc (c *CoprocClause) End() Pos { return c.Stmt.End() }\n\n// LetClause represents a Bash let clause.\n//\n// This node will only appear with [LangBash] and [LangMirBSDKorn].\ntype LetClause struct {\n\tLet   Pos\n\tExprs []ArithmExpr\n}\n\nfunc (l *LetClause) Pos() Pos { return l.Let }\nfunc (l *LetClause) End() Pos { return l.Exprs[len(l.Exprs)-1].End() }\n\n// BraceExp represents a Bash brace expression, such as \"{a,f}\" or \"{1..10}\".\n//\n// This node will only appear as a result of [SplitBraces].\ntype BraceExp struct {\n\tSequence bool // {x..y[..incr]} instead of {x,y[,...]}\n\tElems    []*Word\n}\n\nfunc (b *BraceExp) Pos() Pos {\n\treturn posAddCol(b.Elems[0].Pos(), -1)\n}\n\nfunc (b *BraceExp) End() Pos {\n\treturn posAddCol(wordLastEnd(b.Elems), 1)\n}\n\n// TestDecl represents the declaration of a Bats test function.\ntype TestDecl struct {\n\tPosition    Pos\n\tDescription *Word\n\tBody        *Stmt\n}\n\nfunc (f *TestDecl) Pos() Pos { return f.Position }\nfunc (f *TestDecl) End() Pos { return f.Body.End() }\n\nfunc wordLastEnd(ws []*Word) Pos {\n\tif len(ws) == 0 {\n\t\treturn Pos{}\n\t}\n\treturn ws[len(ws)-1].End()\n}\n"
  },
  {
    "path": "syntax/parser.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"iter\"\n\t\"math/bits\"\n\t\"slices\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\n// ParserOption is a function which can be passed to NewParser\n// to alter its behavior. To apply option to existing Parser\n// call it directly, for example KeepComments(true)(parser).\ntype ParserOption func(*Parser)\n\n// KeepComments makes the parser parse comments and attach them to\n// nodes, as opposed to discarding them.\nfunc KeepComments(enabled bool) ParserOption {\n\treturn func(p *Parser) { p.keepComments = enabled }\n}\n\n// LangVariant describes a shell language variant to use when tokenizing and\n// parsing shell code. The zero value is [LangBash].\n//\n// This type implements [flag.Value] so that it can be used as a CLI flag.\ntype LangVariant int\n\n// TODO(v4): the zero value should be left as an unset and invalid value.\n// TODO(v4): the type should be uint32 now that we use this as a bitset;\n// an unsigned integer is clearer, and being agnostic to uint size avoids issues.\n\nconst (\n\t// LangBash corresponds to the GNU Bash language, as described in its\n\t// manual at https://www.gnu.org/software/bash/manual/bash.html.\n\t//\n\t// We currently follow Bash version 5.2.\n\t//\n\t// Its string representation is \"bash\".\n\tLangBash LangVariant = 1 << iota\n\n\t// LangPOSIX corresponds to the POSIX Shell language, as described at\n\t// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html.\n\t//\n\t// Its string representation is \"posix\" or \"sh\".\n\tLangPOSIX\n\n\t// LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as\n\t// mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm.\n\t// Note that it shares some features with Bash, due to the shared\n\t// ancestry that is ksh.\n\t//\n\t// We currently follow mksh version 59.\n\t//\n\t// Its string representation is \"mksh\".\n\tLangMirBSDKorn\n\n\t// LangBats corresponds to the Bash Automated Testing System language,\n\t// as described at https://github.com/bats-core/bats-core. Note that\n\t// it's just a small extension of the Bash language.\n\t//\n\t// Its string representation is \"bats\".\n\tLangBats\n\n\t// LangZsh corresponds to the Z shell, as described at https://www.zsh.org/.\n\t//\n\t// Note that its support in the syntax package is experimental and\n\t// incomplete for now. See https://github.com/mvdan/sh/issues/120.\n\t//\n\t// We currently follow Zsh version 5.9.\n\t//\n\t// Its string representation is \"zsh\".\n\tLangZsh\n\n\t// LangAuto corresponds to automatic language detection,\n\t// commonly used by end-user applications like shfmt,\n\t// which can guess a file's language variant given its filename or shebang.\n\t//\n\t// At this time, [Variant] does not support LangAuto.\n\tLangAuto\n\n\t// langBashLegacy is what [LangBash] used to be, when it was zero.\n\t// We still support it for the sake of backwards compatibility.\n\tlangBashLegacy LangVariant = 0\n\n\t// langResolvedVariants contains all known variants except [LangAuto],\n\t// which is meant to resolve to another variant.\n\tlangResolvedVariants = LangBash | LangPOSIX | LangMirBSDKorn | LangBats | LangZsh\n\n\t// langResolvedVariantsCount is langResolvedVariants.count() as a constant.\n\t// TODO: Can we compute this as a constant expression somehow?\n\t// For example, if we had log2, we could do log2(LangAuto).\n\tlangResolvedVariantsCount = 5\n\n\t// langBashLike contains Bash plus all variants which are extensions of it.\n\tlangBashLike = LangBash | LangBats\n)\n\n// Variant changes the shell language variant that the parser will\n// accept.\n//\n// The passed language variant must be one of the constant values defined in\n// this package.\nfunc Variant(l LangVariant) ParserOption {\n\tswitch l {\n\tcase langBashLegacy:\n\t\tl = LangBash\n\tcase LangBash, LangPOSIX, LangMirBSDKorn, LangBats, LangZsh:\n\tcase LangAuto:\n\t\tpanic(\"LangAuto is not supported by the parser at this time\")\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unknown shell language variant: %#b\", l))\n\t}\n\treturn func(p *Parser) { p.lang = l }\n}\n\nfunc (l LangVariant) String() string {\n\tswitch l {\n\tcase langBashLegacy, LangBash:\n\t\treturn \"bash\"\n\tcase LangPOSIX:\n\t\treturn \"posix\"\n\tcase LangMirBSDKorn:\n\t\treturn \"mksh\"\n\tcase LangBats:\n\t\treturn \"bats\"\n\tcase LangZsh:\n\t\treturn \"zsh\"\n\tcase LangAuto:\n\t\treturn \"auto\"\n\t}\n\treturn \"unknown shell language variant\"\n}\n\nfunc (l *LangVariant) Set(s string) error {\n\tswitch s {\n\tcase \"bash\":\n\t\t*l = LangBash\n\tcase \"posix\", \"sh\":\n\t\t*l = LangPOSIX\n\tcase \"mksh\":\n\t\t*l = LangMirBSDKorn\n\tcase \"bats\":\n\t\t*l = LangBats\n\tcase \"zsh\":\n\t\t*l = LangZsh\n\tcase \"auto\":\n\t\t*l = LangAuto\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown shell language variant: %q\", s)\n\t}\n\treturn nil\n}\n\nfunc (l LangVariant) in(l2 LangVariant) bool {\n\treturn l&l2 == l\n}\n\nfunc (l LangVariant) count() int {\n\treturn bits.OnesCount32(uint32(l))\n}\n\nfunc (l LangVariant) index() int {\n\treturn bits.TrailingZeros32(uint32(l))\n}\n\nfunc (l LangVariant) bits() iter.Seq[LangVariant] {\n\treturn func(yield func(LangVariant) bool) {\n\t\tfor n := LangVariant(1); n < langResolvedVariants; n <<= 1 {\n\t\t\tif l&n == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !yield(n) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// StopAt configures the lexer to stop at an arbitrary word, treating it\n// as if it were the end of the input. It can contain any characters\n// except whitespace, and cannot be over four bytes in size.\n//\n// This can be useful to embed shell code within another language, as\n// one can use a special word to mark the delimiters between the two.\n//\n// As a word, it will only apply when following whitespace or a\n// separating token. For example, StopAt(\"$$\") will act on the inputs\n// \"foo $$\" and \"foo;$$\", but not on \"foo '$$'\".\n//\n// The match is done by prefix, so the example above will also act on\n// \"foo $$bar\".\nfunc StopAt(word string) ParserOption {\n\tif len(word) > 4 {\n\t\tpanic(\"stop word can't be over four bytes in size\")\n\t}\n\tif strings.ContainsAny(word, \" \\t\\n\\r\") {\n\t\tpanic(\"stop word can't contain whitespace characters\")\n\t}\n\treturn func(p *Parser) { p.stopAt = []byte(word) }\n}\n\n// RecoverErrors allows the parser to skip up to a maximum number of\n// errors in the given input on a best-effort basis.\n// This can be useful to tab-complete an interactive shell prompt,\n// or when providing diagnostics on slightly incomplete shell source.\n//\n// Currently, this only helps with mandatory tokens from the shell grammar\n// which are not present in the input. They result in position fields\n// or nodes whose position report [Pos.IsRecovered] as true.\n//\n// For example, given the input\n//\n//\t(foo |\n//\n// the result will contain two recovered positions; first, the pipe requires\n// a statement to follow, and as [Stmt.Pos] reports, the entire node is recovered.\n// Second, the subshell needs to be closed, so [Subshell.Rparen] is recovered.\nfunc RecoverErrors(maximum int) ParserOption {\n\treturn func(p *Parser) { p.recoverErrorsMax = maximum }\n}\n\n// NewParser allocates a new [Parser] and applies any number of options.\nfunc NewParser(options ...ParserOption) *Parser {\n\tp := &Parser{\n\t\tlang: LangBash,\n\t}\n\tfor _, opt := range options {\n\t\topt(p)\n\t}\n\treturn p\n}\n\n// Parse reads and parses a shell program with an optional name. It\n// returns the parsed program if no issues were encountered. Otherwise,\n// an error is returned. Reads from r are buffered.\n//\n// Parse can be called more than once, but not concurrently. That is, a\n// Parser can be reused once it is done working.\nfunc (p *Parser) Parse(r io.Reader, name string) (*File, error) {\n\tp.reset()\n\tp.f = &File{Name: name}\n\tp.src = r\n\tp.rune()\n\tp.next()\n\tp.f.Stmts, p.f.Last = p.stmtList()\n\tif p.err == nil {\n\t\t// EOF immediately after heredoc word so no newline to\n\t\t// trigger the parsing error.\n\t\tp.doHeredocs()\n\t}\n\treturn p.f, p.err\n}\n\n// Stmts is a pre-iterators API which now wraps [Parser.StmtsSeq].\n//\n// Deprecated: use [Parser.StmtsSeq].\nfunc (p *Parser) Stmts(r io.Reader, fn func(*Stmt) bool) error {\n\tfor stmt, err := range p.StmtsSeq(r) {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !fn(stmt) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// StmtsSeq reads and parses statements one at a time via an iterator.\nfunc (p *Parser) StmtsSeq(r io.Reader) iter.Seq2[*Stmt, error] {\n\tp.reset()\n\tp.f = &File{}\n\tp.src = r\n\treturn func(yield func(*Stmt, error) bool) {\n\t\tp.rune()\n\t\tp.next()\n\t\tp.stmts(yield)\n\t\tif p.err == nil {\n\t\t\t// EOF immediately after heredoc word so no newline to\n\t\t\t// trigger the parsing error.\n\t\t\tp.doHeredocs()\n\t\t}\n\t\tif p.err != nil {\n\t\t\t// Yield any final error from the parser.\n\t\t\tyield(nil, p.err)\n\t\t}\n\t}\n}\n\ntype wrappedReader struct {\n\tp  *Parser\n\trd io.Reader\n\n\tlastLine    int64\n\taccumulated []*Stmt\n\tyield       func([]*Stmt, error) bool\n}\n\nfunc (w *wrappedReader) Read(p []byte) (n int, err error) {\n\t// If we lexed a newline for the first time, we just finished a line, so\n\t// we may need to give a callback for the edge cases below not covered\n\t// by [Parser.Stmts].\n\tif (w.p.r == '\\n' || w.p.r == escNewl) && w.p.line > w.lastLine {\n\t\tif w.p.Incomplete() {\n\t\t\t// Incomplete statement; call back to print \"> \".\n\t\t\tif !w.yield(w.accumulated, w.p.err) {\n\t\t\t\treturn 0, io.EOF\n\t\t\t}\n\t\t} else if len(w.accumulated) == 0 {\n\t\t\t// Nothing was parsed; call back to print another \"$ \".\n\t\t\tif !w.yield(nil, w.p.err) {\n\t\t\t\treturn 0, io.EOF\n\t\t\t}\n\t\t}\n\t\tw.lastLine = w.p.line\n\t}\n\treturn w.rd.Read(p)\n}\n\n// Interactive is a pre-iterators API which now wraps [Parser.InteractiveSeq].\n//\n// Deprecated: use [Parser.InteractiveSeq].\nfunc (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error {\n\tfor stmts, err := range p.InteractiveSeq(r) {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !fn(stmts) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// InteractiveSeq implements what is necessary to parse statements in an\n// interactive shell. The parser will call the given function under two\n// circumstances outlined below.\n//\n// If a line containing any number of statements is parsed, the function will be\n// called with said statements.\n//\n// If a line ending in an incomplete statement is parsed, the function will be\n// called with any fully parsed statements, and [Parser.Incomplete] will return true.\n//\n// One can imagine a simple interactive shell implementation as follows:\n//\n//\tfmt.Fprintf(os.Stdout, \"$ \")\n//\tparser.Interactive(os.Stdin, func(stmts []*syntax.Stmt) bool {\n//\t\tif parser.Incomplete() {\n//\t\t\tfmt.Fprintf(os.Stdout, \"> \")\n//\t\t\treturn true\n//\t\t}\n//\t\trun(stmts)\n//\t\tfmt.Fprintf(os.Stdout, \"$ \")\n//\t\treturn true\n//\t}\n//\n// If the callback function returns false, parsing is stopped and the function\n// is not called again.\nfunc (p *Parser) InteractiveSeq(r io.Reader) iter.Seq2[[]*Stmt, error] {\n\treturn func(yield func([]*Stmt, error) bool) {\n\t\tw := wrappedReader{p: p, rd: r, yield: yield}\n\t\tfor stmts, err := range p.StmtsSeq(&w) {\n\t\t\tw.accumulated = append(w.accumulated, stmts)\n\t\t\tif err != nil {\n\t\t\t\tif !yield(w.accumulated, err) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\t// If the caller wishes, they can continue in the presence of parse errors.\n\t\t\t\t// TODO: does this even work? Write tests for it. This only came up\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// We finished parsing a statement and we're at a newline token,\n\t\t\t// so we finished fully parsing a number of statements. Call\n\t\t\t// back to run the statements and print \"$ \".\n\t\t\tif p.tok == _Newl {\n\t\t\t\tif !yield(w.accumulated, nil) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tw.accumulated = w.accumulated[:0]\n\t\t\t\t// The callback above would already print \"$ \", so we\n\t\t\t\t// don't want the subsequent wrappedReader.Read to cause\n\t\t\t\t// another \"$ \" print thinking that nothing was parsed.\n\t\t\t\tw.lastLine = w.p.line + 1\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Words is a pre-iterators API which now wraps [Parser.WordsSeq].\n//\n// Deprecated: use [Parser.WordsSeq].\nfunc (p *Parser) Words(r io.Reader, fn func(*Word) bool) error {\n\tfor w, err := range p.WordsSeq(r) {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !fn(w) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// WordsSeq reads and parses a sequence of words alongside any error encountered.\n//\n// Newlines are skipped, meaning that multi-line input will work fine. If the\n// parser encounters a token that isn't a word, such as a semicolon, an error\n// will be returned.\n//\n// Note that the lexer doesn't currently tokenize spaces, so it may need to read\n// a non-space byte such as a newline or a letter before finishing the parsing\n// of a word. This will be fixed in the future.\nfunc (p *Parser) WordsSeq(r io.Reader) iter.Seq2[*Word, error] {\n\tp.reset()\n\tp.f = &File{}\n\tp.src = r\n\treturn func(yield func(*Word, error) bool) {\n\t\tp.rune()\n\t\tp.next()\n\t\tfor {\n\t\t\tp.got(_Newl)\n\t\t\tw := p.getWord()\n\t\t\tif w == nil {\n\t\t\t\tif p.tok != _EOF {\n\t\t\t\t\tp.curErr(\"%#q is not a valid word\", p.tok)\n\t\t\t\t}\n\t\t\t\tif p.err != nil {\n\t\t\t\t\tyield(nil, p.err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !yield(w, nil) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Document parses a single here-document word. That is, it parses the input as\n// if they were lines following a <<EOF redirection.\n//\n// In practice, this is the same as parsing the input as if it were within\n// double quotes, but without having to escape all double quote characters.\n// Similarly, the here-document word parsed here cannot be ended by any\n// delimiter other than reaching the end of the input.\nfunc (p *Parser) Document(r io.Reader) (*Word, error) {\n\tp.reset()\n\tp.f = &File{}\n\tp.src = r\n\tp.rune()\n\tp.quote = hdocBody\n\tp.hdocStops = [][]byte{[]byte(\"MVDAN_CC_SH_SYNTAX_EOF\")}\n\tp.parsingDoc = true\n\tp.next()\n\tw := p.getWord()\n\treturn w, p.err\n}\n\n// Arithmetic parses a single arithmetic expression. That is, as if the input\n// were within the $(( and )) tokens.\nfunc (p *Parser) Arithmetic(r io.Reader) (ArithmExpr, error) {\n\tp.reset()\n\tp.f = &File{}\n\tp.src = r\n\tp.rune()\n\tp.quote = arithmExpr\n\tp.next()\n\texpr := p.arithmExpr(false)\n\treturn expr, p.err\n}\n\n// Parser holds the internal state of the parsing mechanism of a\n// program.\ntype Parser struct {\n\tsrc io.Reader\n\tbs  []byte // current chunk of read bytes\n\tbsp uint   // offset within [Parser.bs] for the rune after [Parser.r]\n\tr   rune   // next rune; [utf8.RuneSelf] when it went past EOF, or we stopped\n\tw   int    // width of [Parser.r]\n\n\tf *File\n\n\tspaced bool // whether [Parser.tok] has whitespace on its left\n\n\terr     error // lexer/parser error\n\treadErr error // got a read error, but bytes left\n\treadEOF bool  // [Parser.src] already gave us an [io.EOF] error\n\n\ttok token  // current token\n\tval string // current value (valid if tok is _Lit*)\n\n\t// position of [Parser.r], to be converted to [Parser.pos] later\n\toffs, line, col int64\n\n\tpos Pos // position of tok\n\n\tquote   quoteState // current lexer state\n\teqlOffs int        // position of '=' in [Parser.val] when [Parser.tok].isLit is true\n\n\tkeepComments bool\n\tlang         LangVariant\n\n\tstopAt []byte\n\n\trecoveredErrors  int\n\trecoverErrorsMax int\n\n\tforbidNested bool\n\n\t// list of pending heredoc bodies\n\tburiedHdocs int\n\theredocs    []*Redirect\n\n\thdocStops [][]byte // stack of end words for open heredocs\n\n\tparsingDoc bool // true if using [Parser.Document]\n\n\t// openNodes tracks how many entire statements or words we're currently parsing.\n\t// A non-zero number means that we require certain tokens or words before\n\t// reaching EOF, used for [Parser.Incomplete].\n\topenNodes int\n\t// openBquotes is how many levels of backquotes are open at the moment.\n\topenBquotes int\n\n\t// lastBquoteEsc is how many times the last backquote token was escaped\n\tlastBquoteEsc int\n\n\trxOpenParens int\n\trxFirstPart  bool\n\n\taccComs []Comment\n\tcurComs *[]Comment\n\n\tlitBatch  []Lit\n\twordBatch []wordAlloc\n\n\treadBuf [bufSize]byte\n\tlitBuf  [bufSize]byte\n\tlitBs   []byte\n}\n\n// Incomplete reports whether the parser needs more input bytes\n// to finish properly parsing a statement or word.\n//\n// It is only safe to call while the parser is blocked on a read. For an example\n// use case, see [Parser.Interactive].\nfunc (p *Parser) Incomplete() bool {\n\t// If there are any open nodes, we need to finish them.\n\t// If we're constructing a literal, we need to finish it.\n\treturn p.openNodes > 0 || len(p.litBs) > 0\n}\n\nconst bufSize = 1 << 10\n\nfunc (p *Parser) reset() {\n\tp.tok, p.val = illegalTok, \"\"\n\tp.eqlOffs = 0\n\tp.bs, p.bsp = nil, 0\n\tp.offs, p.line, p.col = 0, 1, 1\n\tp.r, p.w = 0, 0\n\tp.err, p.readErr, p.readEOF = nil, nil, false\n\tp.quote, p.forbidNested = noState, false\n\tp.openNodes = 0\n\tp.recoveredErrors = 0\n\tp.heredocs, p.buriedHdocs = p.heredocs[:0], 0\n\tp.hdocStops = nil\n\tp.parsingDoc = false\n\tp.openBquotes = 0\n\tp.accComs = nil\n\tp.accComs, p.curComs = nil, &p.accComs\n\tp.litBatch = nil\n\tp.wordBatch = nil\n\tp.litBs = nil\n}\n\n// nextPos returns the position of the next rune, [Parser.r].\nfunc (p *Parser) nextPos() Pos {\n\t// Basic protection against offset overflow;\n\t// note that an offset of 0 is valid, so we leave the maximum.\n\toffset := min(p.offs+int64(p.bsp)-int64(p.w), offsetMax)\n\tvar line, col uint\n\tif p.line <= lineMax {\n\t\tline = uint(p.line)\n\t}\n\tif p.col <= colMax {\n\t\tcol = uint(p.col)\n\t}\n\treturn NewPos(uint(offset), line, col)\n}\n\nfunc (p *Parser) lit(pos Pos, val string) *Lit {\n\tif len(p.litBatch) == 0 {\n\t\tp.litBatch = make([]Lit, 32)\n\t}\n\tl := &p.litBatch[0]\n\tp.litBatch = p.litBatch[1:]\n\tl.ValuePos = pos\n\tl.ValueEnd = p.nextPos()\n\tl.Value = val\n\treturn l\n}\n\ntype wordAlloc struct {\n\tword  Word\n\tparts [1]WordPart\n}\n\nfunc (p *Parser) wordAnyNumber() *Word {\n\tif len(p.wordBatch) == 0 {\n\t\tp.wordBatch = make([]wordAlloc, 32)\n\t}\n\talloc := &p.wordBatch[0]\n\tp.wordBatch = p.wordBatch[1:]\n\tw := &alloc.word\n\tw.Parts = p.wordParts(alloc.parts[:0])\n\treturn w\n}\n\nfunc (p *Parser) wordOne(part WordPart) *Word {\n\tif len(p.wordBatch) == 0 {\n\t\tp.wordBatch = make([]wordAlloc, 32)\n\t}\n\talloc := &p.wordBatch[0]\n\tp.wordBatch = p.wordBatch[1:]\n\tw := &alloc.word\n\tw.Parts = alloc.parts[:1]\n\tw.Parts[0] = part\n\treturn w\n}\n\nfunc (p *Parser) call(w *Word) *CallExpr {\n\tvar alloc struct {\n\t\tce CallExpr\n\t\tws [4]*Word\n\t}\n\tce := &alloc.ce\n\tce.Args = alloc.ws[:1]\n\tce.Args[0] = w\n\treturn ce\n}\n\ntype quoteState uint32\n\nconst (\n\t// The initial state of the parser.\n\tnoState quoteState = 1 << iota\n\n\t// Used when parsing parameter expansions; use with [Parser.rune],\n\t// [Parser.next] always returns [illegalTok].\n\truneByRune\n\n\t// unquotedWordCont exists purely so that the '#' in $foo#bar does not\n\t// get parsed as a comment; it's a tiny variation on [noState].\n\tunquotedWordCont\n\n\tsubCmd\n\tsubCmdBckquo\n\tdblQuotes\n\thdocWord\n\thdocBody\n\thdocBodyTabs\n\tarithmExpr\n\tarithmExprLet\n\tarithmExprCmd\n\ttestExpr\n\ttestExprRegexp\n\tswitchCase\n\tparamExpArithm\n\tparamExpRepl\n\tparamExpExp\n\tarrayElems\n\n\tallKeepSpaces = runeByRune | paramExpRepl | dblQuotes | hdocBody |\n\t\thdocBodyTabs | paramExpRepl | paramExpExp\n\tallRegTokens = noState | unquotedWordCont | subCmd | subCmdBckquo | hdocWord |\n\t\tswitchCase | arrayElems | testExpr\n\tallArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd | paramExpArithm\n\tallParamExp   = paramExpArithm | paramExpRepl | paramExpExp\n)\n\ntype saveState struct {\n\tquote       quoteState\n\tburiedHdocs int\n}\n\nfunc (p *Parser) preNested(quote quoteState) (s saveState) {\n\ts.quote, s.buriedHdocs = p.quote, p.buriedHdocs\n\tp.buriedHdocs, p.quote = len(p.heredocs), quote\n\treturn s\n}\n\nfunc (p *Parser) postNested(s saveState) {\n\tp.quote, p.buriedHdocs = s.quote, s.buriedHdocs\n}\n\nfunc (p *Parser) unquotedWordBytes(w *Word) ([]byte, bool) {\n\tbuf := make([]byte, 0, 4)\n\tdidUnquote := false\n\tfor _, wp := range w.Parts {\n\t\tbuf, didUnquote = p.unquotedWordPart(buf, wp, false)\n\t}\n\treturn buf, didUnquote\n}\n\nfunc (p *Parser) unquotedWordPart(buf []byte, wp WordPart, quotes bool) (_ []byte, quoted bool) {\n\tswitch wp := wp.(type) {\n\tcase *Lit:\n\t\tfor i := 0; i < len(wp.Value); i++ {\n\t\t\tif b := wp.Value[i]; b == '\\\\' && !quotes {\n\t\t\t\tif i++; i < len(wp.Value) {\n\t\t\t\t\tbuf = append(buf, wp.Value[i])\n\t\t\t\t}\n\t\t\t\tquoted = true\n\t\t\t} else {\n\t\t\t\tbuf = append(buf, b)\n\t\t\t}\n\t\t}\n\tcase *SglQuoted:\n\t\tbuf = append(buf, []byte(wp.Value)...)\n\t\tquoted = true\n\tcase *DblQuoted:\n\t\tfor _, wp2 := range wp.Parts {\n\t\t\tbuf, _ = p.unquotedWordPart(buf, wp2, true)\n\t\t}\n\t\tquoted = true\n\t}\n\treturn buf, quoted\n}\n\nfunc (p *Parser) doHeredocs() {\n\thdocs := p.heredocs[p.buriedHdocs:]\n\tif len(hdocs) == 0 {\n\t\t// Nothing do do; don't even issue a read.\n\t\treturn\n\t}\n\tp.rune() // consume '\\n', since we know p.tok == _Newl\n\told := p.quote\n\tp.heredocs = p.heredocs[:p.buriedHdocs]\n\tfor i, r := range hdocs {\n\t\tif p.err != nil {\n\t\t\tbreak\n\t\t}\n\t\tp.quote = hdocBody\n\t\tif r.Op == DashHdoc {\n\t\t\tp.quote = hdocBodyTabs\n\t\t}\n\t\tstop, quoted := p.unquotedWordBytes(r.Word)\n\t\tp.hdocStops = append(p.hdocStops, stop)\n\t\tif i > 0 && p.r == '\\n' {\n\t\t\tp.rune()\n\t\t}\n\t\tif quoted {\n\t\t\tr.Hdoc = p.quotedHdocWord()\n\t\t} else {\n\t\t\tp.next()\n\t\t\tr.Hdoc = p.getWord()\n\t\t}\n\t\tif stop := p.hdocStops[len(p.hdocStops)-1]; stop != nil {\n\t\t\tp.posErr(r.Pos(), \"unclosed here-document %#q\", stop)\n\t\t}\n\t\tp.hdocStops = p.hdocStops[:len(p.hdocStops)-1]\n\t}\n\tp.quote = old\n}\n\nfunc (p *Parser) got(tok token) bool {\n\tif p.tok == tok {\n\t\tp.next()\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (p *Parser) gotRsrv(val string) (Pos, bool) {\n\tpos := p.pos\n\tif p.tok == _LitWord && p.val == val {\n\t\tp.next()\n\t\treturn pos, true\n\t}\n\treturn pos, false\n}\n\nfunc (p *Parser) recoverError() bool {\n\tif p.recoveredErrors < p.recoverErrorsMax {\n\t\tp.recoveredErrors++\n\t\treturn true\n\t}\n\treturn false\n}\n\ntype noQuote string\n\nfunc (s noQuote) Format(f fmt.State, verb rune) {\n\tf.Write([]byte(s))\n}\n\nfunc (t token) Format(f fmt.State, verb rune) {\n\tif t < _realTokenBoundary && verb == 'q' {\n\t\t// EOF, Lit and the others should not be quoted in error messages\n\t\t// as they are not real shell syntax like `if` or `{`.\n\t\tf.Write([]byte(t.String()))\n\t} else {\n\t\tfmt.Fprintf(f, fmt.FormatString(f, verb), t.String())\n\t}\n}\n\nfunc (p *Parser) followErr(pos Pos, left, right any) {\n\tp.posErr(pos, \"%#q must be followed by %#q\", left, right)\n}\n\nfunc (p *Parser) followErrExp(pos Pos, left any) {\n\tp.followErr(pos, left, noQuote(\"an expression\"))\n}\n\nfunc (p *Parser) follow(lpos Pos, left string, tok token) {\n\tif !p.got(tok) {\n\t\tp.followErr(lpos, left, tok)\n\t}\n}\n\nfunc (p *Parser) followRsrv(lpos Pos, left, val string) Pos {\n\tpos, ok := p.gotRsrv(val)\n\tif !ok {\n\t\tif p.recoverError() {\n\t\t\treturn recoveredPos\n\t\t}\n\t\tp.followErr(lpos, left, val)\n\t}\n\treturn pos\n}\n\nfunc (p *Parser) followStmts(left string, lpos Pos, stops ...string) ([]*Stmt, []Comment) {\n\t// Language variants disallowing empty command lists:\n\t// * [LangPOSIX]: \"A list is a sequence of one or more AND-OR lists...\".\n\t// * [LangBash]: \"A list is a sequence of one or more pipelines...\"\n\t//\n\t// Language variants allowing empty command lists:\n\t// * [LangZsh]: \"A list is a sequence of zero or more sublists...\".\n\t// * [LangMirBSDKorn]: \"Lists of commands can be created by separating pipelines...\";\n\t//   note that the man page is not explicit, but the shell clearly allows e.g. `{ }`.\n\tif p.got(semicolon) {\n\t\tif p.lang.in(LangZsh | LangMirBSDKorn) {\n\t\t\treturn nil, nil // allow an empty list\n\t\t}\n\t\tp.followErr(lpos, left, noQuote(\"a statement list\"))\n\t\treturn nil, nil\n\t}\n\tstmts, last := p.stmtList(stops...)\n\tif len(stmts) < 1 {\n\t\tif p.lang.in(LangZsh | LangMirBSDKorn) {\n\t\t\treturn nil, nil // allow an empty list\n\t\t}\n\t\tif p.recoverError() {\n\t\t\treturn []*Stmt{{Position: recoveredPos}}, nil\n\t\t}\n\t\tp.followErr(lpos, left, noQuote(\"a statement list\"))\n\t}\n\treturn stmts, last\n}\n\nfunc (p *Parser) followWordTok(tok token, pos Pos) *Word {\n\tw := p.getWord()\n\tif w == nil {\n\t\tif p.recoverError() {\n\t\t\treturn p.wordOne(&Lit{ValuePos: recoveredPos})\n\t\t}\n\t\tp.followErr(pos, tok, noQuote(\"a word\"))\n\t}\n\treturn w\n}\n\nfunc (p *Parser) stmtEnd(n Node, start, end string) Pos {\n\tpos, ok := p.gotRsrv(end)\n\tif !ok {\n\t\tif p.recoverError() {\n\t\t\treturn recoveredPos\n\t\t}\n\t\tp.posErr(n.Pos(), \"%#q statement must end with %#q\", start, end)\n\t}\n\treturn pos\n}\n\nfunc (p *Parser) quoteErr(lpos Pos, quote token) {\n\tp.posErr(lpos, \"reached %#q without closing quote %#q\", p.tok, quote)\n}\n\nfunc (p *Parser) matchingErr(lpos Pos, left, right token) {\n\tp.posErr(lpos, \"reached %#q without matching %#q with %#q\", p.tok, left, right)\n}\n\nfunc (p *Parser) matched(lpos Pos, left, right token) Pos {\n\tpos := p.pos\n\tif !p.got(right) {\n\t\tif p.recoverError() {\n\t\t\treturn recoveredPos\n\t\t}\n\t\tp.matchingErr(lpos, left, right)\n\t}\n\treturn pos\n}\n\nfunc (p *Parser) errPass(err error) {\n\tif p.err == nil {\n\t\tp.err = err\n\t\tp.bsp = uint(len(p.bs)) + 1\n\t\tp.r = utf8.RuneSelf\n\t\tp.w = 1\n\t\tp.tok = _EOF\n\t}\n}\n\n// IsIncomplete reports whether a Parser error could have been avoided with\n// extra input bytes. For example, if an [io.EOF] was encountered while there was\n// an unclosed quote or parenthesis.\nfunc IsIncomplete(err error) bool {\n\tperr, ok := err.(ParseError)\n\treturn ok && perr.Incomplete\n}\n\n// TODO: probably redo with a [LangVariant] argument.\n// Perhaps offer an iterator version as well.\n\n// IsKeyword returns true if the given word is a language keyword\n// in POSIX Shell or Bash.\nfunc IsKeyword(word string) bool {\n\t// This list has been copied from the bash 5.1 source code, file y.tab.c +4460\n\t// TODO: should we include entries for zsh here? e.g. \"{}\", \"repeat\", \"always\", ...\n\tswitch word {\n\tcase\n\t\t\"!\",\n\t\t\"[[\", // only if COND_COMMAND is defined\n\t\t\"]]\", // only if COND_COMMAND is defined\n\t\t\"case\",\n\t\t\"coproc\", // only if COPROCESS_SUPPORT is defined\n\t\t\"do\",\n\t\t\"done\",\n\t\t\"else\",\n\t\t\"esac\",\n\t\t\"fi\",\n\t\t\"for\",\n\t\t\"function\",\n\t\t\"if\",\n\t\t\"in\",\n\t\t\"select\", // only if SELECT_COMMAND is defined\n\t\t\"then\",\n\t\t\"time\", // only if COMMAND_TIMING is defined\n\t\t\"until\",\n\t\t\"while\",\n\t\t\"{\",\n\t\t\"}\":\n\t\treturn true\n\t}\n\treturn false\n}\n\n// ParseError represents an error found when parsing a source file, from which\n// the parser cannot recover.\ntype ParseError struct {\n\tFilename string\n\tPos      Pos\n\tText     string\n\n\tIncomplete bool\n}\n\nfunc (e ParseError) Error() string {\n\tif e.Filename == \"\" {\n\t\treturn fmt.Sprintf(\"%s: %s\", e.Pos, e.Text)\n\t}\n\treturn fmt.Sprintf(\"%s:%s: %s\", e.Filename, e.Pos, e.Text)\n}\n\n// LangError is returned when the parser encounters code that is only valid in\n// other shell language variants. The error includes what feature is not present\n// in the current language variant, and what languages support it.\ntype LangError struct {\n\tFilename string\n\tPos      Pos\n\n\t// TODO: consider replacing the Langs slice with a bitset.\n\n\t// Feature briefly describes which language feature caused the error.\n\tFeature string\n\t// Langs lists some of the language variants which support the feature.\n\tLangs []LangVariant\n\t// LangUsed is the language variant used which led to the error.\n\tLangUsed LangVariant\n}\n\nfunc (e LangError) Error() string {\n\tvar sb strings.Builder\n\tif e.Filename != \"\" {\n\t\tsb.WriteString(e.Filename)\n\t\tsb.WriteString(\":\")\n\t}\n\tsb.WriteString(e.Pos.String())\n\tsb.WriteString(\": \")\n\tsb.WriteString(e.Feature)\n\tif strings.HasSuffix(e.Feature, \"s\") {\n\t\tsb.WriteString(\" are a \")\n\t} else {\n\t\tsb.WriteString(\" is a \")\n\t}\n\tfor i, lang := range e.Langs {\n\t\tif i > 0 {\n\t\t\tsb.WriteString(\"/\")\n\t\t}\n\t\tsb.WriteString(lang.String())\n\t}\n\tsb.WriteString(\" feature; tried parsing as \")\n\tsb.WriteString(e.LangUsed.String())\n\treturn sb.String()\n}\n\nfunc (p *Parser) posErr(pos Pos, format string, args ...any) {\n\t// for i, arg := range args {\n\t// \tif arg, ok := arg.(fmt.Stringer); ok && arg != _EOF {\n\t// \t\targs[i] = quotedToken(arg)\n\t// \t}\n\t// }\n\tp.errPass(ParseError{\n\t\tFilename:   p.f.Name,\n\t\tPos:        pos,\n\t\tText:       fmt.Sprintf(format, args...),\n\t\tIncomplete: p.tok == _EOF && p.Incomplete(),\n\t})\n}\n\nfunc (p *Parser) curErr(format string, args ...any) {\n\tp.posErr(p.pos, format, args...)\n}\n\nfunc (p *Parser) checkLang(pos Pos, langSet LangVariant, format string, a ...any) {\n\tif p.lang.in(langSet) {\n\t\treturn\n\t}\n\tif langBashLike.in(langSet) {\n\t\t// If we're reporting an error because a feature is for bash-like funcs,\n\t\t// just mention \"bash\" rather than \"bash/bats\" for the sake of clarity.\n\t\tlangSet &^= LangBats\n\t}\n\tp.errPass(LangError{\n\t\tFilename: p.f.Name,\n\t\tPos:      pos,\n\t\tFeature:  fmt.Sprintf(format, a...),\n\t\tLangs:    slices.Collect(langSet.bits()),\n\t\tLangUsed: p.lang,\n\t})\n}\n\nfunc (p *Parser) stmts(yield func(*Stmt, error) bool, stops ...string) {\n\tgotEnd := true\nloop:\n\tfor p.tok != _EOF {\n\t\tnewLine := p.got(_Newl)\n\t\tswitch p.tok {\n\t\tcase _LitWord:\n\t\t\tfor _, stop := range stops {\n\t\t\t\tif p.val == stop {\n\t\t\t\t\tbreak loop\n\t\t\t\t}\n\t\t\t}\n\t\t\tif p.val == \"}\" {\n\t\t\t\tp.curErr(`%#q can only be used to close a block`, rightBrace)\n\t\t\t}\n\t\tcase rightParen:\n\t\t\tif p.quote == subCmd {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\tcase bckQuote:\n\t\t\tif p.backquoteEnd() {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\tcase dblSemicolon, semiAnd, dblSemiAnd, semiOr:\n\t\t\tif p.quote == switchCase {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t\tp.curErr(\"%#q can only be used in a case clause\", p.tok)\n\t\t}\n\t\tif !newLine && !gotEnd {\n\t\t\tp.curErr(\"statements must be separated by &, ; or a newline\")\n\t\t}\n\t\tif p.tok == _EOF {\n\t\t\tbreak\n\t\t}\n\t\tp.openNodes++\n\t\ts := p.getStmt(true, false, false)\n\t\tp.openNodes--\n\t\tif s == nil {\n\t\t\tp.invalidStmtStart()\n\t\t\tbreak\n\t\t}\n\t\tgotEnd = s.Semicolon.IsValid()\n\t\tif !yield(s, p.err) {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) {\n\tvar stmts []*Stmt\n\tvar last []Comment\n\tfn := func(s *Stmt, err error) bool {\n\t\tstmts = append(stmts, s)\n\t\treturn true\n\t}\n\tp.stmts(fn, stops...)\n\tsplit := len(p.accComs)\n\tif p.tok == _LitWord && (p.val == \"elif\" || p.val == \"else\" || p.val == \"fi\") {\n\t\t// Split the comments, so that any aligned with an opening token\n\t\t// get attached to it. For example:\n\t\t//\n\t\t//     if foo; then\n\t\t//         # inside the body\n\t\t//     # document the else\n\t\t//     else\n\t\t//     fi\n\t\t// TODO(mvdan): look into deduplicating this with similar logic\n\t\t// in caseItems.\n\t\tfor i, c := range slices.Backward(p.accComs) {\n\t\t\tif c.Pos().Col() != p.pos.Col() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tsplit = i\n\t\t}\n\t}\n\tif split > 0 { // keep last nil if empty\n\t\tlast = p.accComs[:split]\n\t}\n\tp.accComs = p.accComs[split:]\n\treturn stmts, last\n}\n\nfunc (p *Parser) invalidStmtStart() {\n\tswitch p.tok {\n\tcase semicolon, and, or, andAnd, orOr, andPipe, andBang:\n\t\tp.curErr(\"%#q can only immediately follow a statement\", p.tok)\n\tcase rightParen:\n\t\tp.curErr(\"%#q can only be used to close a subshell\", p.tok)\n\tdefault:\n\t\tp.curErr(\"%#q is not a valid start for a statement\", p.tok)\n\t}\n}\n\nfunc (p *Parser) getWord() *Word {\n\tif w := p.wordAnyNumber(); len(w.Parts) > 0 && p.err == nil {\n\t\treturn w\n\t}\n\treturn nil\n}\n\nfunc (p *Parser) getLit() *Lit {\n\tif p.tok.isLit() {\n\t\tl := p.lit(p.pos, p.val)\n\t\tp.next()\n\t\treturn l\n\t}\n\treturn nil\n}\n\nfunc (p *Parser) wordParts(wps []WordPart) []WordPart {\n\tif p.quote == noState {\n\t\tp.quote = unquotedWordCont\n\t\tdefer func() { p.quote = noState }()\n\t}\n\tfor {\n\t\tp.openNodes++\n\t\tn := p.wordPart()\n\t\tp.openNodes--\n\t\tif n == nil {\n\t\t\tif len(wps) == 0 {\n\t\t\t\treturn nil // normalize empty lists into nil\n\t\t\t}\n\t\t\treturn wps\n\t\t}\n\t\twps = append(wps, n)\n\t\tif p.spaced {\n\t\t\treturn wps\n\t\t}\n\t}\n}\n\nfunc (p *Parser) ensureNoNested(pos Pos) {\n\tif p.forbidNested {\n\t\tp.posErr(pos, \"expansions not allowed in heredoc words\")\n\t}\n}\n\nfunc (p *Parser) wordPart() WordPart {\n\tswitch p.tok {\n\tcase _Lit, _LitWord, _LitRedir:\n\t\tl := p.lit(p.pos, p.val)\n\t\tp.next()\n\t\treturn l\n\tcase dollBrace:\n\t\tp.ensureNoNested(p.pos)\n\t\tswitch p.r {\n\t\tcase '|':\n\t\t\tp.checkLang(p.pos, langBashLike|LangMirBSDKorn, \"`${|stmts;}`\")\n\t\t\tfallthrough\n\t\tcase ' ', '\\t', '\\n':\n\t\t\tp.checkLang(p.pos, langBashLike|LangMirBSDKorn, \"`${ stmts;}`\")\n\t\t\tcs := &CmdSubst{\n\t\t\t\tLeft:     p.pos,\n\t\t\t\tTempFile: p.r != '|',\n\t\t\t\tReplyVar: p.r == '|',\n\t\t\t}\n\t\t\told := p.preNested(subCmd)\n\t\t\tp.rune() // don't tokenize '|'\n\t\t\tp.next()\n\t\t\tcs.Stmts, cs.Last = p.stmtList(\"}\")\n\t\t\tp.postNested(old)\n\t\t\tpos, ok := p.gotRsrv(\"}\")\n\t\t\tif !ok {\n\t\t\t\tp.matchingErr(cs.Left, dollBrace, rightBrace)\n\t\t\t}\n\t\t\tcs.Right = pos\n\t\t\treturn cs\n\t\tdefault:\n\t\t\treturn p.paramExp()\n\t\t}\n\tcase dollDblParen, dollBrack:\n\t\tp.ensureNoNested(p.pos)\n\t\tleft := p.tok\n\t\tar := &ArithmExp{Left: p.pos, Bracket: left == dollBrack}\n\t\told := p.preNested(arithmExpr)\n\t\tp.next()\n\t\tif p.got(hash) {\n\t\t\tp.checkLang(ar.Pos(), LangMirBSDKorn, \"unsigned expressions\")\n\t\t\tar.Unsigned = true\n\t\t}\n\t\tar.X = p.followArithm(left, ar.Left)\n\t\tif ar.Bracket {\n\t\t\tif p.tok != rightBrack {\n\t\t\t\tp.arithmMatchingErr(ar.Left, dollBrack, rightBrack)\n\t\t\t}\n\t\t\tp.postNested(old)\n\t\t\tar.Right = p.pos\n\t\t\tp.next()\n\t\t} else {\n\t\t\tar.Right = p.arithmEnd(dollDblParen, ar.Left, old)\n\t\t}\n\t\treturn ar\n\tcase dollParen:\n\t\tp.ensureNoNested(p.pos)\n\t\treturn p.cmdSubst()\n\tcase dollar:\n\t\tpe := p.paramExp()\n\t\tif pe == nil { // was not actually a parameter expansion, like: \"foo$\"\n\t\t\tl := p.lit(p.pos, \"$\")\n\t\t\tp.next()\n\t\t\treturn l\n\t\t}\n\t\tp.ensureNoNested(pe.Dollar)\n\t\treturn pe\n\tcase assgnParen:\n\t\tp.checkLang(p.pos, LangZsh, `%#q process substitutions`, p.tok)\n\t\tfallthrough\n\tcase cmdIn, cmdOut:\n\t\tp.ensureNoNested(p.pos)\n\t\tps := &ProcSubst{Op: ProcOperator(p.tok), OpPos: p.pos}\n\t\told := p.preNested(subCmd)\n\t\tp.next()\n\t\tps.Stmts, ps.Last = p.stmtList()\n\t\tp.postNested(old)\n\t\tps.Rparen = p.matched(ps.OpPos, token(ps.Op), rightParen)\n\t\treturn ps\n\tcase sglQuote, dollSglQuote:\n\t\tsq := &SglQuoted{Left: p.pos, Dollar: p.tok == dollSglQuote}\n\t\tr := p.r\n\t\tfor p.newLit(r); ; r = p.rune() {\n\t\t\tswitch r {\n\t\t\tcase '\\\\':\n\t\t\t\tif sq.Dollar {\n\t\t\t\t\tp.rune()\n\t\t\t\t}\n\t\t\tcase '\\'':\n\t\t\t\tsq.Right = p.nextPos()\n\t\t\t\tsq.Value = p.endLit()\n\n\t\t\t\tp.rune()\n\t\t\t\tp.next()\n\t\t\t\treturn sq\n\t\t\tcase escNewl:\n\t\t\t\tp.litBs = append(p.litBs, '\\\\', '\\n')\n\t\t\tcase utf8.RuneSelf:\n\t\t\t\tp.tok = _EOF\n\t\t\t\tif p.recoverError() {\n\t\t\t\t\tsq.Right = recoveredPos\n\t\t\t\t\treturn sq\n\t\t\t\t}\n\t\t\t\tp.quoteErr(sq.Pos(), sglQuote)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\tcase dblQuote, dollDblQuote:\n\t\tif p.quote == dblQuotes {\n\t\t\t// p.tok == dblQuote, as \"foo$\" puts $ in the lit\n\t\t\treturn nil\n\t\t}\n\t\treturn p.dblQuoted()\n\tcase bckQuote:\n\t\tif p.backquoteEnd() {\n\t\t\treturn nil\n\t\t}\n\t\tp.ensureNoNested(p.pos)\n\t\tcs := &CmdSubst{Left: p.pos, Backquotes: true}\n\t\told := p.preNested(subCmdBckquo)\n\t\tp.openBquotes++\n\n\t\t// The lexer didn't call p.rune for us, so that it could have\n\t\t// the right p.openBquotes to properly handle backslashes.\n\t\tp.rune()\n\n\t\tp.next()\n\t\tcs.Stmts, cs.Last = p.stmtList()\n\t\tif p.tok == bckQuote && p.lastBquoteEsc < p.openBquotes-1 {\n\t\t\t// e.g. found ` before the nested backquote \\` was closed.\n\t\t\tp.tok = _EOF\n\t\t\tp.quoteErr(cs.Pos(), bckQuote)\n\t\t}\n\t\tp.postNested(old)\n\t\tp.openBquotes--\n\t\tcs.Right = p.pos\n\n\t\t// Like above, the lexer didn't call p.rune for us.\n\t\tp.rune()\n\t\tif !p.got(bckQuote) {\n\t\t\tif p.recoverError() {\n\t\t\t\tcs.Right = recoveredPos\n\t\t\t} else {\n\t\t\t\tp.quoteErr(cs.Pos(), bckQuote)\n\t\t\t}\n\t\t}\n\t\treturn cs\n\tcase leftParen:\n\t\tif p.lang.in(LangZsh) && p.r != ')' {\n\t\t\t// Zsh glob qualifier like *(N) or .(:a); the only case where\n\t\t\t// ( immediately after a word is not a glob qualifier is ()\n\t\t\t// for a function declaration, which the parser handles earlier.\n\t\t\tpos := p.pos\n\t\t\tp.pos = p.nextPos()\n\t\t\tfor p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() {\n\t\t\t}\n\t\t\tif p.r != ')' {\n\t\t\t\tp.tok = _EOF // we can only get here due to EOF\n\t\t\t\tp.matchingErr(pos, leftParen, rightParen)\n\t\t\t}\n\t\t\tp.rune()\n\t\t\tp.val = p.endLit()\n\t\t\tl := p.lit(pos, \"(\"+p.val)\n\t\t\tp.next()\n\t\t\treturn l\n\t\t}\n\t\treturn nil\n\tcase globQuest, globStar, globPlus, globAt, globExcl:\n\t\tp.checkLang(p.pos, langBashLike|LangMirBSDKorn, \"extended globs\")\n\t\teg := &ExtGlob{Op: GlobOperator(p.tok), OpPos: p.pos}\n\t\tlparens := 1\n\t\tr := p.r\n\tglobLoop:\n\t\tfor p.newLit(r); ; r = p.rune() {\n\t\t\tswitch r {\n\t\t\tcase utf8.RuneSelf:\n\t\t\t\tbreak globLoop\n\t\t\tcase '(':\n\t\t\t\tlparens++\n\t\t\tcase ')':\n\t\t\t\tif lparens--; lparens == 0 {\n\t\t\t\t\tbreak globLoop\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\teg.Pattern = p.lit(posAddCol(eg.OpPos, 2), p.endLit())\n\t\tp.rune()\n\t\tp.next()\n\t\tif lparens != 0 {\n\t\t\tp.matchingErr(eg.OpPos, token(eg.Op), rightParen)\n\t\t}\n\t\treturn eg\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (p *Parser) cmdSubst() *CmdSubst {\n\tcs := &CmdSubst{Left: p.pos}\n\told := p.preNested(subCmd)\n\tp.next()\n\tcs.Stmts, cs.Last = p.stmtList()\n\tp.postNested(old)\n\tcs.Right = p.matched(cs.Left, dollParen, rightParen)\n\treturn cs\n}\n\nfunc (p *Parser) dblQuoted() *DblQuoted {\n\talloc := &struct {\n\t\tquoted DblQuoted\n\t\tparts  [1]WordPart\n\t}{\n\t\tquoted: DblQuoted{Left: p.pos, Dollar: p.tok == dollDblQuote},\n\t}\n\tq := &alloc.quoted\n\told := p.quote\n\tp.quote = dblQuotes\n\tp.next()\n\tq.Parts = p.wordParts(alloc.parts[:0])\n\tp.quote = old\n\tq.Right = p.pos\n\tif !p.got(dblQuote) {\n\t\tif p.recoverError() {\n\t\t\tq.Right = recoveredPos\n\t\t} else {\n\t\t\tp.quoteErr(q.Pos(), dblQuote)\n\t\t}\n\t}\n\treturn q\n}\n\n// paramExp parses a short or full parameter expansion, depending on whether\n// [Parser.tok] is [dollar] or [dollBrace]. It returns nil if a [dollar] token\n// does not form a valid parameter expansion, in which case it should be parsed\n// as a literal.\nfunc (p *Parser) paramExp() *ParamExp {\n\told := p.quote\n\tp.quote = runeByRune\n\t// [ParamExp.Short] means we are parsing $exp rather than ${exp}.\n\tpe := &ParamExp{\n\t\tDollar: p.pos,\n\t\tShort:  p.tok == dollar,\n\t}\n\tif !pe.Short && p.r == '(' {\n\t\tp.checkLang(pe.Pos(), LangZsh, `parameter expansion flags`)\n\t\t// For now, for simplicity, we parse flags as just a literal.\n\t\t// In the future, parsing as a word is better for cases like\n\t\t// `${(ps.$sep.)val}`.\n\t\tlparen := p.nextPos()\n\t\tp.rune()\n\t\tp.pos = p.nextPos()\n\t\tfor p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() {\n\t\t}\n\t\tp.val = p.endLit()\n\t\tif p.r != ')' {\n\t\t\tp.tok = _EOF // we can only get here due to EOF\n\t\t\tp.matchingErr(lparen, leftParen, rightParen)\n\t\t}\n\t\tpe.Flags = p.lit(p.pos, p.val)\n\t\tp.rune()\n\t}\n\tif !pe.Short || p.lang.in(LangZsh) {\n\t\t// Prefixes, like ${#name} to get the length of a variable.\n\t\t// Note that in Zsh, the short form like $#name is allowed too.\n\t\tswitch p.r {\n\t\tcase '#':\n\t\t\tif r := p.peek(); r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '\"' {\n\t\t\t\tpe.Length = true\n\t\t\t\tp.rune()\n\t\t\t}\n\t\tcase '%':\n\t\t\tif r := p.peek(); r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '\"' {\n\t\t\t\tp.checkLang(pe.Pos(), LangMirBSDKorn, \"`${%%foo}`\")\n\t\t\t\tpe.Width = true\n\t\t\t\tp.rune()\n\t\t\t}\n\t\tcase '!':\n\t\t\tif r := p.peek(); r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '\"' {\n\t\t\t\tp.checkLang(pe.Pos(), langBashLike|LangMirBSDKorn, \"`${!foo}`\")\n\t\t\t\tpe.Excl = true\n\t\t\t\tp.rune()\n\t\t\t}\n\t\tcase '+':\n\t\t\tif r := p.peek(); r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '\"' {\n\t\t\t\tp.checkLang(pe.Pos(), LangZsh, \"`${+foo}`\")\n\t\t\t\tpe.IsSet = true\n\t\t\t\tp.rune()\n\t\t\t}\n\t\t}\n\t}\n\tif pe = p.paramExpParameter(pe); pe == nil {\n\t\tp.quote = old\n\t\treturn nil // just \"$\"\n\t}\n\t// In short mode, any indexing or suffixes is not allowed, and we don't require '}'.\n\t// Zsh is an exception: $foo[1] and $foo[1,3] are valid. Note that $1[x] does not qualify.\n\tif pe.Short {\n\t\tif p.lang.in(LangZsh) && p.r == '[' && (len(p.val) != 1 || !positionalRuneParam(p.val[0])) {\n\t\t\tp.pos = p.nextPos()\n\t\t\tp.rune()\n\t\t\tpe.Index = p.eitherIndex()\n\t\t}\n\t\tp.quote = old\n\t\tp.next()\n\t\treturn pe\n\t}\n\t// Index expressions like ${foo[1]}. Note that expansion suffixes can be combined,\n\t// like ${foo[@]//replace/with}.\n\tif p.r == '[' {\n\t\tp.checkLang(p.nextPos(), langBashLike|LangMirBSDKorn|LangZsh, \"arrays\")\n\t\tif pe.Param != nil && !ValidName(pe.Param.Value) {\n\t\t\tp.posErr(p.nextPos(), \"cannot index a special parameter name\")\n\t\t}\n\t\tp.pos = p.nextPos()\n\t\tp.rune()\n\t\tpe.Index = p.eitherIndex()\n\t}\n\ttokRune := p.r\n\tp.pos = p.nextPos()\n\tp.tok = p.paramToken(p.r)\n\tif p.tok == rightBrace {\n\t\tpe.Rbrace = p.pos\n\t\tp.quote = old\n\t\tp.next()\n\t\treturn pe\n\t}\n\tif p.tok != _EOF && (pe.Length || pe.Width || pe.IsSet) {\n\t\tp.curErr(\"cannot combine multiple parameter expansion operators\")\n\t}\n\tswitch p.tok {\n\tcase slash, dblSlash: // pattern search and replace\n\t\tp.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, \"search and replace\")\n\t\tpe.Repl = &Replace{All: p.tok == dblSlash}\n\t\tp.quote = paramExpRepl\n\t\tp.next()\n\t\tpe.Repl.Orig = p.getWord()\n\t\tp.quote = paramExpExp\n\t\tif p.got(slash) {\n\t\t\tpe.Repl.With = p.getWord()\n\t\t}\n\tcase colon: // slicing\n\t\tif p.lang.in(LangZsh) && (p.r == '&' || asciiLetter(p.r)) {\n\t\t\tpos := p.pos\n\t\tloop:\n\t\t\tfor p.newLit(p.r); ; p.rune() {\n\t\t\t\tswitch p.r {\n\t\t\t\tcase utf8.RuneSelf:\n\t\t\t\t\tp.tok = _EOF\n\t\t\t\t\tp.matchingErr(pe.Dollar, dollBrace, rightBrace)\n\t\t\t\t\tbreak loop\n\t\t\t\tcase '}':\n\t\t\t\t\tpe.Modifiers = append(pe.Modifiers, p.lit(pos, p.endLit()))\n\t\t\t\t\tpe.Rbrace = p.nextPos()\n\t\t\t\t\tp.rune()\n\t\t\t\t\tbreak loop\n\t\t\t\tcase ':':\n\t\t\t\t\tpe.Modifiers = append(pe.Modifiers, p.lit(pos, p.endLit()))\n\t\t\t\t\tp.rune()\n\t\t\t\t\tpos = p.nextPos()\n\t\t\t\t\tp.newLit(p.r)\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.quote = old\n\t\t\tp.next()\n\t\t\treturn pe\n\t\t}\n\t\tp.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, \"slicing\")\n\t\tpe.Slice = &Slice{}\n\t\tcolonPos := p.pos\n\t\tp.quote = paramExpArithm\n\t\tif p.next(); p.tok != colon {\n\t\t\tpe.Slice.Offset = p.followArithm(colon, colonPos)\n\t\t}\n\t\tcolonPos = p.pos\n\t\tif p.got(colon) {\n\t\t\tpe.Slice.Length = p.followArithm(colon, colonPos)\n\t\t}\n\t\t// Need to use a different matched style so arithm errors\n\t\t// get reported correctly\n\t\tp.quote = old\n\t\tpe.Rbrace = p.pos\n\t\tp.matchedArithm(pe.Dollar, dollBrace, rightBrace)\n\t\treturn pe\n\tcase caret, dblCaret, comma, dblComma: // upper/lower case\n\t\tp.checkLang(p.pos, langBashLike, \"this expansion operator\")\n\t\tpe.Exp = p.paramExpExp()\n\tcase at, star:\n\t\tswitch {\n\t\tcase p.tok == star && !pe.Excl:\n\t\t\tp.curErr(\"not a valid parameter expansion operator: %#q\", p.tok)\n\t\tcase pe.Excl && p.r == '}':\n\t\t\tp.checkLang(pe.Pos(), langBashLike, \"`${!foo%s}`\", p.tok)\n\t\t\tpe.Names = ParNamesOperator(p.tok)\n\t\t\tp.next()\n\t\tcase p.tok == at:\n\t\t\tp.checkLang(p.pos, langBashLike|LangMirBSDKorn, \"this expansion operator\")\n\t\t\tfallthrough\n\t\tdefault:\n\t\t\tpe.Exp = p.paramExpExp()\n\t\t}\n\tcase plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn,\n\t\tperc, dblPerc, hash, dblHash, colHash, colPipe, colStar:\n\t\tpe.Exp = p.paramExpExp()\n\tcase _EOF:\n\tdefault:\n\t\tif paramNameRune(tokRune) {\n\t\t\tif pe.Param != nil {\n\t\t\t\tp.curErr(\"%#q cannot be followed by a word\", pe.Param.Value)\n\t\t\t} else {\n\t\t\t\tp.curErr(\"nested parameter expansion cannot be followed by a word\")\n\t\t\t}\n\t\t} else {\n\t\t\tp.curErr(\"not a valid parameter expansion operator: %#q\", string(tokRune))\n\t\t}\n\t}\n\tif p.tok != _EOF && p.tok != rightBrace {\n\t\tp.tok = p.paramToken(p.r)\n\t}\n\tp.quote = old\n\tpe.Rbrace = p.matched(pe.Dollar, dollBrace, rightBrace)\n\treturn pe\n}\n\nfunc (p *Parser) nestedParameterStart(pe *ParamExp) (left token, quotePos Pos) {\n\tif pe.Short {\n\t\treturn illegalTok, Pos{}\n\t}\n\tif p.r == '\"' {\n\t\tquotePos = p.nextPos()\n\t\tp.rune()\n\t}\n\tif p.r != '$' {\n\t\tif quotePos.IsValid() {\n\t\t\treturn dollar, quotePos\n\t\t}\n\t\treturn illegalTok, Pos{}\n\t}\n\tswitch p1 := p.peek(); p1 {\n\tcase '{', '(':\n\t\tp.pos = p.nextPos()\n\t\tp.checkLang(p.pos, LangZsh, \"nested parameter expansions\")\n\t\tif p.err != nil {\n\t\t\treturn illegalTok, Pos{} // xxx given that we overwrite p.tok below\n\t\t}\n\t\tp.rune()\n\t\tp.rune()\n\t\tif p1 == '{' {\n\t\t\tleft = dollBrace\n\t\t} else { // '('\n\t\t\tleft = dollParen\n\t\t}\n\t}\n\treturn left, quotePos\n}\n\nfunc (p *Parser) paramExpParameter(pe *ParamExp) *ParamExp {\n\t// Check for Zsh nested parameter expressions like ${(f)\"$(foo)\"}.\n\tif left, quotePos := p.nestedParameterStart(pe); left != illegalTok {\n\t\tvar wp WordPart\n\t\tswitch p.tok = left; p.tok {\n\t\tcase dollBrace: // ${#${nested parameter}}\n\t\t\tp.tok = dollBrace\n\t\t\twp = p.paramExp()\n\t\tcase dollParen: // ${#$(nested command)}\n\t\t\twp = p.cmdSubst()\n\t\tdefault: // dollar\n\t\t\tp.posErr(pe.Pos(), \"invalid nested parameter expansion\")\n\t\t}\n\t\tif quotePos.IsValid() {\n\t\t\tif p.r != '\"' {\n\t\t\t\tp.tok = p.paramToken(p.r)\n\t\t\t\tif p.tok == illegalTok {\n\t\t\t\t\tp.posErr(pe.Pos(), \"invalid nested parameter expansion\")\n\t\t\t\t} else {\n\t\t\t\t\tp.quoteErr(quotePos, dblQuote)\n\t\t\t\t}\n\t\t\t}\n\t\t\tpe.NestedParam = &DblQuoted{\n\t\t\t\tLeft:  quotePos,\n\t\t\t\tRight: p.nextPos(),\n\t\t\t\tParts: []WordPart{wp},\n\t\t\t}\n\t\t\tp.rune()\n\t\t} else {\n\t\t\tpe.NestedParam = wp\n\t\t}\n\t\treturn pe\n\t}\n\t// The parameter name itself, like $foo or $?.\n\tswitch p.r {\n\tcase '?', '-':\n\t\tif pe.Length && p.peek() != '}' {\n\t\t\t// actually ${#-default}, not ${#-}; fix the ambiguity\n\t\t\tpe.Length = false\n\t\t\tpos := p.nextPos()\n\t\t\tpe.Param = p.lit(posAddCol(pos, -1), \"#\")\n\t\t\tpe.Param.ValueEnd = pos\n\t\t\tbreak\n\t\t}\n\t\tfallthrough\n\tcase '@', '*', '#', '!', '$':\n\t\tr, pos := p.r, p.nextPos()\n\t\tp.rune()\n\t\tpe.Param = p.lit(pos, string(r))\n\tdefault:\n\t\t// Note that $1a is equivalent to ${1}a, but ${1a} is not.\n\t\t// POSIX Shell says the latter is unspecified behavior, so match Bash's behavior.\n\t\tpos := p.nextPos()\n\t\tif pe.Short && singleRuneParam(p.r) {\n\t\t\tp.val = string(p.r)\n\t\t\tp.rune()\n\t\t} else {\n\t\t\tfor p.newLit(p.r); p.r != utf8.RuneSelf; p.rune() {\n\t\t\t\tif !paramNameRune(p.r) && p.r != escNewl {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.val = p.endLit()\n\t\t\tif !numberLiteral(p.val) && !ValidName(p.val) {\n\t\t\t\tif pe.Short {\n\t\t\t\t\treturn nil // just \"$\"\n\t\t\t\t}\n\t\t\t\tp.posErr(pos, \"invalid parameter name\")\n\t\t\t}\n\t\t}\n\t\tpe.Param = p.lit(pos, p.val)\n\t}\n\treturn pe\n}\n\nfunc (p *Parser) paramExpExp() *Expansion {\n\top := ParExpOperator(p.tok)\n\tswitch op {\n\tcase MatchEmpty, ArrayExclude, ArrayIntersect:\n\t\tp.checkLang(p.pos, LangZsh, \"${name%sarg}\", op)\n\t}\n\tp.quote = paramExpExp\n\tp.next()\n\tif op == OtherParamOps {\n\t\tif !p.tok.isLit() {\n\t\t\tp.curErr(\"@ expansion operator requires a literal\")\n\t\t}\n\t\tswitch p.val {\n\t\tcase \"a\", \"k\", \"u\", \"A\", \"E\", \"K\", \"L\", \"P\", \"U\":\n\t\t\tp.checkLang(p.pos, langBashLike, \"this expansion operator\")\n\t\tcase \"#\":\n\t\t\tp.checkLang(p.pos, LangMirBSDKorn, \"this expansion operator\")\n\t\tcase \"Q\":\n\t\tdefault:\n\t\t\tp.curErr(\"invalid @ expansion operator %#q\", p.val)\n\t\t}\n\t}\n\treturn &Expansion{Op: op, Word: p.getWord()}\n}\n\nfunc (p *Parser) eitherIndex() ArithmExpr {\n\told := p.quote\n\tlpos := p.pos\n\tp.quote = paramExpArithm\n\tp.next()\n\tif p.tok == star || p.tok == at {\n\t\tp.tok, p.val = _LitWord, p.tok.String()\n\t}\n\texpr := p.followArithm(leftBrack, lpos)\n\tp.quote = old\n\tp.matchedArithm(lpos, leftBrack, rightBrack)\n\treturn expr\n}\n\nfunc (p *Parser) zshSubFlags() *FlagsArithm {\n\tzf := &FlagsArithm{}\n\t// Lex flags as raw text, like paramExp does for ${(flags)...}.\n\tlparen := p.pos\n\told := p.quote\n\tp.quote = runeByRune\n\tp.pos = p.nextPos()\n\tfor p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() {\n\t}\n\tp.val = p.endLit()\n\tif p.r != ')' {\n\t\tp.tok = _EOF\n\t\tp.matchingErr(lparen, leftParen, rightParen)\n\t}\n\tzf.Flags = p.lit(p.pos, p.val)\n\tp.rune()\n\tp.quote = old\n\t// Parse the expression; use arithmExprAssign so commas are left for ranges.\n\tp.next()\n\tif p.tok == star || p.tok == at {\n\t\tp.tok, p.val = _LitWord, p.tok.String()\n\t}\n\tzf.X = p.arithmExprAssign(false)\n\treturn zf\n}\n\nfunc (p *Parser) stopToken() bool {\n\tswitch p.tok {\n\tcase _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, andPipe, andBang,\n\t\tdblSemicolon, semiAnd, dblSemiAnd, semiOr, rightParen:\n\t\treturn true\n\tcase bckQuote:\n\t\treturn p.backquoteEnd()\n\t}\n\treturn false\n}\n\nfunc (p *Parser) backquoteEnd() bool {\n\treturn p.lastBquoteEsc < p.openBquotes\n}\n\n// ValidName returns whether val is a valid name as per the POSIX spec.\nfunc ValidName(val string) bool {\n\tif val == \"\" {\n\t\treturn false\n\t}\n\tfor i, r := range val {\n\t\tswitch {\n\t\tcase asciiLetter(r), r == '_':\n\t\tcase i > 0 && asciiDigit(r):\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc numberLiteral[T string | []byte](val T) bool {\n\tif len(val) == 0 {\n\t\treturn false\n\t}\n\tfor _, r := range string(val) {\n\t\tif !asciiDigit(r) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (p *Parser) hasValidIdent() bool {\n\tif !p.tok.isLit() {\n\t\treturn false\n\t}\n\tif end := p.eqlOffs; end > 0 {\n\t\tif p.val[end-1] == '+' && p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) {\n\t\t\tend-- // a+=x\n\t\t}\n\t\tif ValidName(p.val[:end]) {\n\t\t\treturn true\n\t\t}\n\t} else if !ValidName(p.val) {\n\t\treturn false // *[i]=x\n\t}\n\treturn p.r == '[' // a[i]=x\n}\n\nfunc (p *Parser) getAssign(needEqual bool) *Assign {\n\tas := &Assign{}\n\tif p.eqlOffs > 0 { // foo=bar\n\t\tnameEnd := p.eqlOffs\n\t\tif p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.val[p.eqlOffs-1] == '+' {\n\t\t\t// a+=b\n\t\t\tas.Append = true\n\t\t\tnameEnd--\n\t\t}\n\t\tas.Name = p.lit(p.pos, p.val[:nameEnd])\n\t\t// since we're not using the entire p.val\n\t\tas.Name.ValueEnd = posAddCol(as.Name.ValuePos, nameEnd)\n\t\tleft := p.lit(posAddCol(p.pos, 1), p.val[p.eqlOffs+1:])\n\t\tif left.Value != \"\" {\n\t\t\tleft.ValuePos = posAddCol(left.ValuePos, p.eqlOffs)\n\t\t\tas.Value = p.wordOne(left)\n\t\t}\n\t\tp.next()\n\t} else { // foo[x]=bar\n\t\tas.Name = p.lit(p.pos, p.val)\n\t\t// hasValidIdent already checks p.r is '['\n\t\tp.rune()\n\t\tp.pos = posAddCol(p.pos, 1)\n\t\tas.Index = p.eitherIndex()\n\t\tif p.spaced || p.stopToken() {\n\t\t\tif needEqual {\n\t\t\t\tp.followErr(as.Pos(), \"a[b]\", assgn)\n\t\t\t} else {\n\t\t\t\tas.Naked = true\n\t\t\t\treturn as\n\t\t\t}\n\t\t}\n\t\tif p.tok == assgnParen {\n\t\t\tif !p.lang.in(LangZsh) {\n\t\t\t\tp.curErr(\"arrays cannot be nested\")\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// zsh allows a[i]=(values...).\n\t\t\t// assgnParen consumed both '=' and '(',\n\t\t\t// so rewrite as leftParen for array parsing below.\n\t\t\tp.tok = leftParen\n\t\t\tp.pos = posAddCol(p.pos, 1)\n\t\t} else {\n\t\t\tif len(p.val) > 0 && p.val[0] == '+' {\n\t\t\t\tas.Append = true\n\t\t\t\tp.val = p.val[1:]\n\t\t\t\tp.pos = posAddCol(p.pos, 1)\n\t\t\t}\n\t\t\tif len(p.val) < 1 || p.val[0] != '=' {\n\t\t\t\tif as.Append {\n\t\t\t\t\tp.followErr(as.Pos(), \"a[b]+\", assgn)\n\t\t\t\t} else {\n\t\t\t\t\tp.followErr(as.Pos(), \"a[b]\", assgn)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tp.pos = posAddCol(p.pos, 1)\n\t\t\tp.val = p.val[1:]\n\t\t\tif p.val == \"\" {\n\t\t\t\tp.next()\n\t\t\t}\n\t\t}\n\t}\n\tif p.spaced || p.stopToken() {\n\t\treturn as\n\t}\n\tif as.Value == nil && p.tok == leftParen {\n\t\tp.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, \"arrays\")\n\t\tas.Array = &ArrayExpr{Lparen: p.pos}\n\t\tnewQuote := p.quote\n\t\tif p.lang.in(langBashLike | LangZsh) {\n\t\t\tnewQuote = arrayElems\n\t\t}\n\t\told := p.preNested(newQuote)\n\t\tp.next()\n\t\tp.got(_Newl)\n\t\tfor p.tok != _EOF && p.tok != rightParen {\n\t\t\tae := &ArrayElem{}\n\t\t\tae.Comments, p.accComs = p.accComs, nil\n\t\t\tif p.tok == leftBrack {\n\t\t\t\tleft := p.pos\n\t\t\t\tae.Index = p.eitherIndex()\n\t\t\t\tif p.tok == assgnParen {\n\t\t\t\t\tp.curErr(\"arrays cannot be nested\")\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tp.follow(left, `[x]`, assgn)\n\t\t\t}\n\t\t\tif ae.Value = p.getWord(); ae.Value == nil {\n\t\t\t\tswitch p.tok {\n\t\t\t\tcase _Newl, rightParen, leftBrack:\n\t\t\t\t\t// TODO: support [index]=[\n\t\t\t\tdefault:\n\t\t\t\t\tp.curErr(\"array element values must be words\")\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(p.accComs) > 0 {\n\t\t\t\tc := p.accComs[0]\n\t\t\t\tif c.Pos().Line() == ae.End().Line() {\n\t\t\t\t\tae.Comments = append(ae.Comments, c)\n\t\t\t\t\tp.accComs = p.accComs[1:]\n\t\t\t\t}\n\t\t\t}\n\t\t\tas.Array.Elems = append(as.Array.Elems, ae)\n\t\t\tp.got(_Newl)\n\t\t}\n\t\tas.Array.Last, p.accComs = p.accComs, nil\n\t\tp.postNested(old)\n\t\tas.Array.Rparen = p.matched(as.Array.Lparen, leftParen, rightParen)\n\t} else if w := p.getWord(); w != nil {\n\t\tif as.Value == nil {\n\t\t\tas.Value = w\n\t\t} else {\n\t\t\tas.Value.Parts = append(as.Value.Parts, w.Parts...)\n\t\t}\n\t}\n\treturn as\n}\n\nfunc (p *Parser) peekRedir() bool {\n\tswitch p.tok {\n\tcase _LitRedir, rdrOut, appOut, rdrIn, rdrInOut, dplIn, dplOut,\n\t\trdrClob, appClob, hdoc, dashHdoc, wordHdoc,\n\t\trdrAll, rdrAllClob, appAll, appAllClob:\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (p *Parser) doRedirect(s *Stmt) {\n\tvar r *Redirect\n\tif s.Redirs == nil {\n\t\tvar alloc struct {\n\t\t\tredirs [4]*Redirect\n\t\t\tredir  Redirect\n\t\t}\n\t\ts.Redirs = alloc.redirs[:0]\n\t\tr = &alloc.redir\n\t\ts.Redirs = append(s.Redirs, r)\n\t} else {\n\t\tr = &Redirect{}\n\t\ts.Redirs = append(s.Redirs, r)\n\t}\n\tr.N = p.getLit()\n\tif r.N != nil && r.N.Value[0] == '{' {\n\t\tp.checkLang(r.N.Pos(), langBashLike, \"`{varname}` redirects\")\n\t}\n\tr.Op, r.OpPos = RedirOperator(p.tok), p.pos\n\tswitch r.Op {\n\tcase RdrAll, AppAll:\n\t\tp.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, \"%#q redirects\", r.Op)\n\tcase AppClob, RdrAllClob, AppAllClob:\n\t\tp.checkLang(p.pos, LangZsh, \"%#q redirects\", r.Op)\n\t}\n\tp.next()\n\tswitch r.Op {\n\tcase Hdoc, DashHdoc:\n\t\told := p.quote\n\t\tp.quote, p.forbidNested = hdocWord, true\n\t\tp.heredocs = append(p.heredocs, r)\n\t\tr.Word = p.followWordTok(token(r.Op), r.OpPos)\n\t\tp.quote, p.forbidNested = old, false\n\t\tif p.tok == _Newl {\n\t\t\tif len(p.accComs) > 0 {\n\t\t\t\tc := p.accComs[0]\n\t\t\t\tif c.Pos().Line() == s.End().Line() {\n\t\t\t\t\ts.Comments = append(s.Comments, c)\n\t\t\t\t\tp.accComs = p.accComs[1:]\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.doHeredocs()\n\t\t}\n\tcase WordHdoc:\n\t\tp.checkLang(r.OpPos, langBashLike|LangMirBSDKorn|LangZsh, \"herestrings\")\n\t\tfallthrough\n\tdefault:\n\t\tr.Word = p.followWordTok(token(r.Op), r.OpPos)\n\t}\n}\n\nfunc (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt {\n\tpos, ok := p.gotRsrv(\"!\")\n\ts := &Stmt{Position: pos}\n\tif ok {\n\t\ts.Negated = true\n\t\tif p.stopToken() {\n\t\t\tp.posErr(s.Pos(), `%#q cannot form a statement alone`, exclMark)\n\t\t}\n\t\tif _, ok := p.gotRsrv(\"!\"); ok {\n\t\t\tp.posErr(s.Pos(), `cannot negate a command multiple times`)\n\t\t}\n\t}\n\tif s = p.gotStmtPipe(s, false); s == nil || p.err != nil {\n\t\treturn nil\n\t}\n\t// instead of using recursion, iterate manually\n\tfor p.tok == andAnd || p.tok == orOr {\n\t\tif binCmd {\n\t\t\t// left associativity: in a list of BinaryCmds, the\n\t\t\t// right recursion should only read a single element\n\t\t\treturn s\n\t\t}\n\t\tb := &BinaryCmd{\n\t\t\tOpPos: p.pos,\n\t\t\tOp:    BinCmdOperator(p.tok),\n\t\t\tX:     s,\n\t\t}\n\t\tp.next()\n\t\tp.got(_Newl)\n\t\tb.Y = p.getStmt(false, true, false)\n\t\tif b.Y == nil || p.err != nil {\n\t\t\tif p.recoverError() {\n\t\t\t\tb.Y = &Stmt{Position: recoveredPos}\n\t\t\t} else {\n\t\t\t\tp.followErr(b.OpPos, b.Op, noQuote(\"a statement\"))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\ts = &Stmt{Position: s.Position}\n\t\ts.Cmd = b\n\t\ts.Comments, b.X.Comments = b.X.Comments, nil\n\t}\n\tif readEnd {\n\t\tswitch p.tok {\n\t\tcase semicolon:\n\t\t\ts.Semicolon = p.pos\n\t\t\tp.next()\n\t\tcase and:\n\t\t\ts.Semicolon = p.pos\n\t\t\tp.next()\n\t\t\ts.Background = true\n\t\tcase orAnd:\n\t\t\ts.Semicolon = p.pos\n\t\t\tp.next()\n\t\t\ts.Coprocess = true\n\t\tcase andPipe, andBang:\n\t\t\ts.Semicolon = p.pos\n\t\t\tp.next()\n\t\t\ts.Disown = true\n\t\t}\n\t}\n\tif len(p.accComs) > 0 && !binCmd && !fnBody {\n\t\tc := p.accComs[0]\n\t\tif c.Pos().Line() == s.End().Line() {\n\t\t\ts.Comments = append(s.Comments, c)\n\t\t\tp.accComs = p.accComs[1:]\n\t\t}\n\t}\n\treturn s\n}\n\nfunc (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt {\n\ts.Comments, p.accComs = p.accComs, nil\n\tfor p.peekRedir() {\n\t\tp.doRedirect(s)\n\t}\n\tredirsStart := len(s.Redirs)\n\tswitch p.tok {\n\tcase _LitWord:\n\t\tswitch p.val {\n\t\tcase \"{\":\n\t\t\tp.block(s)\n\t\tcase \"{}\":\n\t\t\t// Zsh treats closing braces in a special way, allowing this.\n\t\t\tif p.lang.in(LangZsh) {\n\t\t\t\ts.Cmd = &Block{Lbrace: p.pos, Rbrace: posAddCol(p.pos, 1)}\n\t\t\t\tp.next()\n\t\t\t}\n\t\tcase \"if\":\n\t\t\tp.ifClause(s)\n\t\tcase \"while\", \"until\":\n\t\t\t// TODO(zsh): \"repeat\"\n\t\t\tp.whileClause(s, p.val == \"until\")\n\t\tcase \"for\":\n\t\t\tp.forClause(s)\n\t\tcase \"case\":\n\t\t\tp.caseClause(s)\n\t\t// TODO(zsh): { try-list } \"always\" { always-list }\n\t\tcase \"}\":\n\t\t\tp.curErr(`%#q can only be used to close a block`, rightBrace)\n\t\tcase \"then\", \"elif\":\n\t\t\tp.curErr(\"%#q can only be used in an `if`\", p.val)\n\t\tcase \"fi\":\n\t\t\tp.curErr(\"%#q can only be used to end an `if`\", p.val)\n\t\tcase \"do\":\n\t\t\tp.curErr(`%#q can only be used in a loop`, p.val)\n\t\tcase \"done\":\n\t\t\tp.curErr(`%#q can only be used to end a loop`, p.val)\n\t\tcase \"esac\":\n\t\t\tp.curErr(\"%#q can only be used to end a `case`\", p.val)\n\t\tcase \"!\":\n\t\t\tif !s.Negated {\n\t\t\t\tp.curErr(`%#q can only be used in full statements`, exclMark)\n\t\t\t\tbreak\n\t\t\t}\n\t\tcase \"[[\":\n\t\t\tif p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tp.testClause(s)\n\t\t\t}\n\t\tcase \"]]\":\n\t\t\tif p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tp.curErr(`%#q can only be used to close a test`, dblRightBrack)\n\t\t\t}\n\t\tcase \"let\":\n\t\t\tif p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tp.letClause(s)\n\t\t\t}\n\t\tcase \"function\":\n\t\t\tif p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tp.bashFuncDecl(s)\n\t\t\t}\n\t\tcase \"declare\":\n\t\t\tif p.lang.in(langBashLike | LangZsh) { // Note that mksh lacks this one.\n\t\t\t\tp.declClause(s)\n\t\t\t}\n\t\tcase \"local\", \"export\", \"readonly\", \"typeset\", \"nameref\":\n\t\t\tif p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tp.declClause(s)\n\t\t\t}\n\t\tcase \"time\":\n\t\t\tif p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tp.timeClause(s)\n\t\t\t}\n\t\tcase \"coproc\":\n\t\t\tif p.lang.in(langBashLike) { // Note that mksh lacks this one.\n\t\t\t\tp.coprocClause(s)\n\t\t\t}\n\t\tcase \"select\":\n\t\t\tif p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) {\n\t\t\t\tp.selectClause(s)\n\t\t\t}\n\t\tcase \"@test\":\n\t\t\tif p.lang.in(LangBats) {\n\t\t\t\tp.testDecl(s)\n\t\t\t}\n\t\t}\n\t\tif s.Cmd != nil {\n\t\t\tbreak\n\t\t}\n\t\tif p.hasValidIdent() {\n\t\t\tp.callExpr(s, nil, true)\n\t\t\tbreak\n\t\t}\n\t\tname := p.lit(p.pos, p.val)\n\t\tp.next()\n\t\t// In zsh, ( after a word is a glob qualifier unless followed\n\t\t// immediately by ), which is the func declaration syntax.\n\t\tif p.tok == leftParen && (!p.lang.in(LangZsh) || p.r == ')') {\n\t\t\tp.next()\n\t\t\tp.follow(name.ValuePos, \"foo(\", rightParen)\n\t\t\tif p.lang.in(LangPOSIX) && !ValidName(name.Value) {\n\t\t\t\tp.posErr(name.Pos(), \"invalid func name\")\n\t\t\t}\n\t\t\tp.funcDecl(s, name.ValuePos, false, true, name)\n\t\t} else {\n\t\t\tw := p.wordOne(name)\n\t\t\tif p.lang.in(LangZsh) && !p.spaced {\n\t\t\t\tw.Parts = append(w.Parts, p.wordParts(nil)...)\n\t\t\t}\n\t\t\tp.callExpr(s, w, false)\n\t\t}\n\tcase bckQuote:\n\t\tif p.backquoteEnd() {\n\t\t\tbreak\n\t\t}\n\t\tfallthrough\n\tcase _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, assgnParen, cmdOut,\n\t\tsglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack,\n\t\tglobQuest, globStar, globPlus, globAt, globExcl:\n\t\tif p.hasValidIdent() {\n\t\t\tp.callExpr(s, nil, true)\n\t\t\tbreak\n\t\t}\n\t\tw := p.wordAnyNumber()\n\t\tif p.got(leftParen) {\n\t\t\tp.posErr(w.Pos(), \"invalid func name\")\n\t\t}\n\t\tp.callExpr(s, w, false)\n\tcase leftParen:\n\t\tif p.r == ')' {\n\t\t\tp.rune()\n\t\t\tfpos := p.pos\n\t\t\tp.next()\n\t\t\tif p.tok == _LitWord && p.val == \"{\" {\n\t\t\t\tp.checkLang(fpos, LangZsh, \"anonymous functions\")\n\t\t\t}\n\t\t\tp.funcDecl(s, fpos, false, true)\n\t\t\tbreak\n\t\t}\n\t\tp.subshell(s)\n\tcase dblLeftParen:\n\t\tp.arithmExpCmd(s)\n\t}\n\tif s.Cmd == nil && len(s.Redirs) == 0 {\n\t\treturn nil // no statement found\n\t}\n\tif redirsStart > 0 && s.Cmd != nil {\n\t\tif _, ok := s.Cmd.(*CallExpr); !ok {\n\t\t\tp.checkLang(s.Pos(), LangZsh, \"redirects before compound commands\")\n\t\t}\n\t}\n\tfor p.peekRedir() {\n\t\tp.doRedirect(s)\n\t}\n\t// instead of using recursion, iterate manually\n\tfor p.tok == or || p.tok == orAnd {\n\t\tif binCmd {\n\t\t\t// left associativity: in a list of BinaryCmds, the\n\t\t\t// right recursion should only read a single element\n\t\t\treturn s\n\t\t}\n\t\tif p.tok == orAnd && p.lang.in(LangMirBSDKorn) {\n\t\t\t// No need to check for LangPOSIX, as on that language\n\t\t\t// we parse |& as two tokens.\n\t\t\tbreak\n\t\t}\n\t\tb := &BinaryCmd{OpPos: p.pos, Op: BinCmdOperator(p.tok), X: s}\n\t\tp.next()\n\t\tp.got(_Newl)\n\t\tif b.Y = p.gotStmtPipe(&Stmt{Position: p.pos}, true); b.Y == nil || p.err != nil {\n\t\t\tif p.recoverError() {\n\t\t\t\tb.Y = &Stmt{Position: recoveredPos}\n\t\t\t} else {\n\t\t\t\tp.followErr(b.OpPos, b.Op, noQuote(\"a statement\"))\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ts = &Stmt{Position: s.Position}\n\t\ts.Cmd = b\n\t\ts.Comments, b.X.Comments = b.X.Comments, nil\n\t\t// in \"! x | y\", the bang applies to the entire pipeline\n\t\ts.Negated = b.X.Negated\n\t\tb.X.Negated = false\n\t}\n\treturn s\n}\n\nfunc (p *Parser) subshell(s *Stmt) {\n\tsub := &Subshell{Lparen: p.pos}\n\told := p.preNested(subCmd)\n\tp.next()\n\tsub.Stmts, sub.Last = p.followStmts(\"(\", sub.Lparen)\n\tp.postNested(old)\n\tsub.Rparen = p.matched(sub.Lparen, leftParen, rightParen)\n\ts.Cmd = sub\n}\n\nfunc (p *Parser) arithmExpCmd(s *Stmt) {\n\tar := &ArithmCmd{Left: p.pos}\n\told := p.preNested(arithmExprCmd)\n\tp.next()\n\tif p.got(hash) {\n\t\tp.checkLang(ar.Pos(), LangMirBSDKorn, \"unsigned expressions\")\n\t\tar.Unsigned = true\n\t}\n\tar.X = p.followArithm(dblLeftParen, ar.Left)\n\tar.Right = p.arithmEnd(dblLeftParen, ar.Left, old)\n\ts.Cmd = ar\n}\n\nfunc (p *Parser) block(s *Stmt) {\n\tb := &Block{Lbrace: p.pos}\n\tp.next()\n\tb.Stmts, b.Last = p.followStmts(\"{\", b.Lbrace, \"}\")\n\tif pos, ok := p.gotRsrv(\"}\"); ok {\n\t\tb.Rbrace = pos\n\t} else if p.recoverError() {\n\t\tb.Rbrace = recoveredPos\n\t} else {\n\t\tp.matchingErr(b.Lbrace, leftBrace, rightBrace)\n\t}\n\ts.Cmd = b\n}\n\nfunc (p *Parser) ifClause(s *Stmt) {\n\trootIf := &IfClause{Position: p.pos}\n\tp.next()\n\trootIf.Cond, rootIf.CondLast = p.followStmts(\"if\", rootIf.Position, \"then\")\n\trootIf.ThenPos = p.followRsrv(rootIf.Position, \"if <cond>\", \"then\")\n\trootIf.Then, rootIf.ThenLast = p.followStmts(\"then\", rootIf.ThenPos, \"fi\", \"elif\", \"else\")\n\tcurIf := rootIf\n\tfor p.tok == _LitWord && p.val == \"elif\" {\n\t\telf := &IfClause{Position: p.pos}\n\t\tcurIf.Last = p.accComs\n\t\tp.accComs = nil\n\t\tp.next()\n\t\telf.Cond, elf.CondLast = p.followStmts(\"elif\", elf.Position, \"then\")\n\t\telf.ThenPos = p.followRsrv(elf.Position, \"elif <cond>\", \"then\")\n\t\telf.Then, elf.ThenLast = p.followStmts(\"then\", elf.ThenPos, \"fi\", \"elif\", \"else\")\n\t\tcurIf.Else = elf\n\t\tcurIf = elf\n\t}\n\tif elsePos, ok := p.gotRsrv(\"else\"); ok {\n\t\tcurIf.Last = p.accComs\n\t\tp.accComs = nil\n\t\tels := &IfClause{Position: elsePos}\n\t\tels.Then, els.ThenLast = p.followStmts(\"else\", els.Position, \"fi\")\n\t\tcurIf.Else = els\n\t\tcurIf = els\n\t}\n\tcurIf.Last = p.accComs\n\tp.accComs = nil\n\trootIf.FiPos = p.stmtEnd(rootIf, \"if\", \"fi\")\n\tfor els := rootIf.Else; els != nil; els = els.Else {\n\t\t// All the nested IfClauses share the same FiPos.\n\t\tels.FiPos = rootIf.FiPos\n\t}\n\ts.Cmd = rootIf\n}\n\nfunc (p *Parser) whileClause(s *Stmt, until bool) {\n\twc := &WhileClause{WhilePos: p.pos, Until: until}\n\trsrv := \"while\"\n\trsrvCond := \"while <cond>\"\n\tif wc.Until {\n\t\trsrv = \"until\"\n\t\trsrvCond = \"until <cond>\"\n\t}\n\tp.next()\n\twc.Cond, wc.CondLast = p.followStmts(rsrv, wc.WhilePos, \"do\")\n\twc.DoPos = p.followRsrv(wc.WhilePos, rsrvCond, \"do\")\n\twc.Do, wc.DoLast = p.followStmts(\"do\", wc.DoPos, \"done\")\n\twc.DonePos = p.stmtEnd(wc, rsrv, \"done\")\n\ts.Cmd = wc\n}\n\nfunc (p *Parser) forClause(s *Stmt) {\n\tfc := &ForClause{ForPos: p.pos}\n\tp.next()\n\tfc.Loop = p.loop(fc.ForPos)\n\n\tstart, end := \"do\", \"done\"\n\tif pos, ok := p.gotRsrv(\"{\"); ok {\n\t\tp.checkLang(pos, langBashLike|LangMirBSDKorn, \"for loops with braces\")\n\t\tfc.DoPos = pos\n\t\tfc.Braces = true\n\t\tstart, end = \"{\", \"}\"\n\t} else {\n\t\tfc.DoPos = p.followRsrv(fc.ForPos, \"for foo [in words]\", start)\n\t}\n\n\ts.Comments = append(s.Comments, p.accComs...)\n\tp.accComs = nil\n\tfc.Do, fc.DoLast = p.followStmts(start, fc.DoPos, end)\n\tfc.DonePos = p.stmtEnd(fc, \"for\", end)\n\ts.Cmd = fc\n}\n\nfunc (p *Parser) loop(fpos Pos) Loop {\n\tswitch p.tok {\n\tcase leftParen, dblLeftParen:\n\t\tp.checkLang(p.pos, langBashLike|LangZsh, \"c-style fors\")\n\t}\n\tif p.tok == dblLeftParen {\n\t\tcl := &CStyleLoop{Lparen: p.pos}\n\t\told := p.preNested(arithmExprCmd)\n\t\tp.next()\n\t\tcl.Init = p.arithmExpr(false)\n\t\tif !p.got(dblSemicolon) {\n\t\t\tp.follow(p.pos, \"expr\", semicolon)\n\t\t\tcl.Cond = p.arithmExpr(false)\n\t\t\tp.follow(p.pos, \"expr\", semicolon)\n\t\t}\n\t\tcl.Post = p.arithmExpr(false)\n\t\tcl.Rparen = p.arithmEnd(dblLeftParen, cl.Lparen, old)\n\t\tp.got(semicolon)\n\t\tp.got(_Newl)\n\t\treturn cl\n\t}\n\treturn p.wordIter(\"for\", fpos)\n}\n\nfunc (p *Parser) wordIter(ftok string, fpos Pos) *WordIter {\n\twi := &WordIter{}\n\tif wi.Name = p.getLit(); wi.Name == nil {\n\t\tp.followErr(fpos, ftok, noQuote(\"a literal\"))\n\t}\n\tif p.got(semicolon) {\n\t\tp.got(_Newl)\n\t\treturn wi\n\t}\n\tp.got(_Newl)\n\tif pos, ok := p.gotRsrv(\"in\"); ok {\n\t\twi.InPos = pos\n\t\tfor !p.stopToken() {\n\t\t\tif w := p.getWord(); w == nil {\n\t\t\t\tp.curErr(\"word list can only contain words\")\n\t\t\t} else {\n\t\t\t\twi.Items = append(wi.Items, w)\n\t\t\t}\n\t\t}\n\t\tp.got(semicolon)\n\t\tp.got(_Newl)\n\t} else if p.tok == _LitWord && p.val == \"do\" {\n\t} else {\n\t\tp.followErr(fpos, ftok+\" foo\", noQuote(\"`in`, `do`, `;`, or a newline\"))\n\t}\n\treturn wi\n}\n\nfunc (p *Parser) selectClause(s *Stmt) {\n\tfc := &ForClause{ForPos: p.pos, Select: true}\n\tp.next()\n\tfc.Loop = p.wordIter(\"select\", fc.ForPos)\n\tfc.DoPos = p.followRsrv(fc.ForPos, \"select foo [in words]\", \"do\")\n\tfc.Do, fc.DoLast = p.followStmts(\"do\", fc.DoPos, \"done\")\n\tfc.DonePos = p.stmtEnd(fc, \"select\", \"done\")\n\ts.Cmd = fc\n}\n\nfunc (p *Parser) caseClause(s *Stmt) {\n\tcc := &CaseClause{Case: p.pos}\n\tp.next()\n\tcc.Word = p.getWord()\n\tif cc.Word == nil {\n\t\tp.followErr(cc.Case, \"case\", noQuote(\"a word\"))\n\t}\n\tend := \"esac\"\n\tp.got(_Newl)\n\tif pos, ok := p.gotRsrv(\"{\"); ok {\n\t\tcc.In = pos\n\t\tcc.Braces = true\n\t\tp.checkLang(cc.Pos(), LangMirBSDKorn, \"`case i {`\")\n\t\tend = \"}\"\n\t} else {\n\t\tcc.In = p.followRsrv(cc.Case, \"case x\", \"in\")\n\t}\n\tcc.Items = p.caseItems(end)\n\tcc.Last, p.accComs = p.accComs, nil\n\tcc.Esac = p.stmtEnd(cc, \"case\", end)\n\ts.Cmd = cc\n}\n\nfunc (p *Parser) caseItems(stop string) (items []*CaseItem) {\n\tp.got(_Newl)\n\tfor p.tok != _EOF && (p.tok != _LitWord || p.val != stop) {\n\t\tci := &CaseItem{}\n\t\tci.Comments, p.accComs = p.accComs, nil\n\t\tp.got(leftParen)\n\t\tfor p.tok != _EOF {\n\t\t\tif w := p.getWord(); w == nil {\n\t\t\t\tp.curErr(\"case patterns must consist of words\")\n\t\t\t} else {\n\t\t\t\tci.Patterns = append(ci.Patterns, w)\n\t\t\t}\n\t\t\tif p.tok == rightParen {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !p.got(or) {\n\t\t\t\tp.curErr(\"case patterns must be separated with %#q\", or)\n\t\t\t}\n\t\t}\n\t\told := p.preNested(switchCase)\n\t\tp.next()\n\t\tci.Stmts, ci.Last = p.stmtList(stop)\n\t\tp.postNested(old)\n\t\tswitch p.tok {\n\t\tcase dblSemicolon, semiAnd, dblSemiAnd, semiOr:\n\t\tdefault:\n\t\t\tci.Op = Break\n\t\t\titems = append(items, ci)\n\t\t\treturn items\n\t\t}\n\t\tci.Last = append(ci.Last, p.accComs...)\n\t\tp.accComs = nil\n\t\tci.OpPos = p.pos\n\t\tci.Op = CaseOperator(p.tok)\n\t\tp.next()\n\t\tp.got(_Newl)\n\n\t\t// Split the comments:\n\t\t//\n\t\t// case x in\n\t\t// a)\n\t\t//   foo\n\t\t//   ;;\n\t\t//   # comment for a\n\t\t// # comment for b\n\t\t// b)\n\t\t//   [...]\n\t\tsplit := len(p.accComs)\n\t\tfor i, c := range slices.Backward(p.accComs) {\n\t\t\tif c.Pos().Col() != p.pos.Col() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tsplit = i\n\t\t}\n\t\tci.Comments = append(ci.Comments, p.accComs[:split]...)\n\t\tp.accComs = p.accComs[split:]\n\n\t\titems = append(items, ci)\n\t}\n\treturn items\n}\n\nfunc (p *Parser) testClause(s *Stmt) {\n\ttc := &TestClause{Left: p.pos}\n\told := p.preNested(testExpr)\n\tp.next()\n\tif tc.X = p.testExprBinary(false); tc.X == nil {\n\t\tp.followErrExp(tc.Left, dblLeftBrack)\n\t}\n\ttc.Right = p.pos\n\tif _, ok := p.gotRsrv(\"]]\"); !ok {\n\t\tp.matchingErr(tc.Left, dblLeftBrack, dblRightBrack)\n\t}\n\tp.postNested(old)\n\ts.Cmd = tc\n}\n\nfunc (p *Parser) testExprBinary(pastAndOr bool) TestExpr {\n\tp.got(_Newl)\n\tvar left TestExpr\n\tif pastAndOr {\n\t\tleft = p.testExprUnary()\n\t} else {\n\t\tleft = p.testExprBinary(true)\n\t}\n\tif left == nil {\n\t\treturn left\n\t}\n\tp.got(_Newl)\n\tswitch p.tok {\n\tcase andAnd, orOr:\n\tcase _LitWord:\n\t\tif p.val == \"]]\" {\n\t\t\treturn left\n\t\t}\n\t\tif p.tok = token(testBinaryOp(p.val)); p.tok == illegalTok {\n\t\t\tp.curErr(\"not a valid test operator: %#q\", p.val)\n\t\t}\n\tcase rdrIn, rdrOut:\n\tcase _EOF, rightParen:\n\t\treturn left\n\tcase _Lit:\n\t\tp.curErr(\"test operator words must consist of a single literal\")\n\tdefault:\n\t\tp.curErr(\"not a valid test operator: %#q\", p.tok)\n\t}\n\tb := &BinaryTest{\n\t\tOpPos: p.pos,\n\t\tOp:    BinTestOperator(p.tok),\n\t\tX:     left,\n\t}\n\tswitch b.Op {\n\tcase AndTest, OrTest:\n\t\tp.next()\n\t\tif b.Y = p.testExprBinary(false); b.Y == nil {\n\t\t\tp.followErrExp(b.OpPos, b.Op)\n\t\t}\n\tcase TsReMatch:\n\t\tp.checkLang(p.pos, langBashLike|LangZsh, \"regex tests\")\n\t\tp.rxOpenParens = 0\n\t\tp.rxFirstPart = true\n\t\t// TODO(mvdan): Using nested states within a regex will break in\n\t\t// all sorts of ways. The better fix is likely to use a stop\n\t\t// token, like we do with heredocs.\n\t\tp.quote = testExprRegexp\n\t\tfallthrough\n\tdefault:\n\t\tif _, ok := b.X.(*Word); !ok {\n\t\t\tp.posErr(b.OpPos, \"expected %#q, %#q or %#q after complex expr\",\n\t\t\t\tAndTest, OrTest, dblRightBrack)\n\t\t}\n\t\tp.next()\n\t\tb.Y = p.followWordTok(token(b.Op), b.OpPos)\n\t}\n\treturn b\n}\n\nfunc (p *Parser) testExprUnary() TestExpr {\n\tswitch p.tok {\n\tcase _EOF, rightParen:\n\t\treturn nil\n\tcase _LitWord:\n\t\top := token(testUnaryOp(p.val))\n\t\tswitch op {\n\t\tcase illegalTok:\n\t\tcase tsRefVar, tsModif: // not available in mksh\n\t\t\tif p.lang.in(langBashLike) {\n\t\t\t\tp.tok = op\n\t\t\t}\n\t\tdefault:\n\t\t\tp.tok = op\n\t\t}\n\t}\n\tswitch p.tok {\n\tcase exclMark:\n\t\tu := &UnaryTest{OpPos: p.pos, Op: TsNot}\n\t\tp.next()\n\t\tif u.X = p.testExprBinary(false); u.X == nil {\n\t\t\tp.followErrExp(u.OpPos, u.Op)\n\t\t}\n\t\treturn u\n\tcase tsExists, tsRegFile, tsDirect, tsCharSp, tsBlckSp, tsNmPipe,\n\t\ttsSocket, tsSmbLink, tsSticky, tsGIDSet, tsUIDSet, tsGrpOwn,\n\t\ttsUsrOwn, tsModif, tsRead, tsWrite, tsExec, tsNoEmpty,\n\t\ttsFdTerm, tsEmpStr, tsNempStr, tsOptSet, tsVarSet, tsRefVar:\n\t\tu := &UnaryTest{OpPos: p.pos, Op: UnTestOperator(p.tok)}\n\t\tp.next()\n\t\tu.X = p.followWordTok(token(u.Op), u.OpPos)\n\t\treturn u\n\tcase leftParen:\n\t\tpe := &ParenTest{Lparen: p.pos}\n\t\tp.next()\n\t\tif pe.X = p.testExprBinary(false); pe.X == nil {\n\t\t\tp.followErrExp(pe.Lparen, leftParen)\n\t\t}\n\t\tpe.Rparen = p.matched(pe.Lparen, leftParen, rightParen)\n\t\treturn pe\n\tcase _LitWord:\n\t\tif p.val == \"]]\" {\n\t\t\treturn nil\n\t\t}\n\t\tfallthrough\n\tdefault:\n\t\tif w := p.getWord(); w != nil {\n\t\t\treturn w\n\t\t}\n\t\t// otherwise we'd return a typed nil above\n\t\treturn nil\n\t}\n}\n\nfunc (p *Parser) declClause(s *Stmt) {\n\tds := &DeclClause{Variant: p.lit(p.pos, p.val)}\n\tp.next()\n\tfor !p.stopToken() && !p.peekRedir() {\n\t\tif p.hasValidIdent() {\n\t\t\tds.Args = append(ds.Args, p.getAssign(false))\n\t\t} else if p.tok.isLit() && p.eqlOffs > 0 && !strings.Contains(p.val[:p.eqlOffs], \"{\") {\n\t\t\tp.curErr(\"invalid var name\")\n\t\t} else if p.tok == _LitWord && ValidName(p.val) {\n\t\t\tds.Args = append(ds.Args, &Assign{\n\t\t\t\tNaked: true,\n\t\t\t\tName:  p.getLit(),\n\t\t\t})\n\t\t} else if w := p.getWord(); w != nil {\n\t\t\tds.Args = append(ds.Args, &Assign{\n\t\t\t\tNaked: true,\n\t\t\t\tValue: w,\n\t\t\t})\n\t\t} else {\n\t\t\tp.followErr(p.pos, ds.Variant.Value, noQuote(\"names or assignments\"))\n\t\t}\n\t}\n\ts.Cmd = ds\n}\n\nfunc isBashCompoundCommand(tok token, val string) bool {\n\tswitch tok {\n\tcase leftParen, dblLeftParen:\n\t\treturn true\n\tcase _LitWord:\n\t\tswitch val {\n\t\tcase \"{\", \"if\", \"while\", \"until\", \"for\", \"case\", \"[[\",\n\t\t\t\"coproc\", \"let\", \"function\", \"declare\", \"local\",\n\t\t\t\"export\", \"readonly\", \"typeset\", \"nameref\":\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (p *Parser) timeClause(s *Stmt) {\n\ttc := &TimeClause{Time: p.pos}\n\tp.next()\n\tif _, ok := p.gotRsrv(\"-p\"); ok {\n\t\ttc.PosixFormat = true\n\t}\n\ttc.Stmt = p.gotStmtPipe(&Stmt{Position: p.pos}, false)\n\ts.Cmd = tc\n}\n\nfunc (p *Parser) coprocClause(s *Stmt) {\n\tcc := &CoprocClause{Coproc: p.pos}\n\tif p.next(); isBashCompoundCommand(p.tok, p.val) {\n\t\t// has no name\n\t\tcc.Stmt = p.gotStmtPipe(&Stmt{Position: p.pos}, false)\n\t\ts.Cmd = cc\n\t\treturn\n\t}\n\tcc.Name = p.getWord()\n\tcc.Stmt = p.gotStmtPipe(&Stmt{Position: p.pos}, false)\n\tif cc.Stmt == nil {\n\t\tif cc.Name == nil {\n\t\t\tp.posErr(cc.Coproc, \"coproc clause requires a command\")\n\t\t\treturn\n\t\t}\n\t\t// name was in fact the stmt\n\t\tcc.Stmt = &Stmt{Position: cc.Name.Pos()}\n\t\tcc.Stmt.Cmd = p.call(cc.Name)\n\t\tcc.Name = nil\n\t} else if cc.Name != nil {\n\t\tif call, ok := cc.Stmt.Cmd.(*CallExpr); ok {\n\t\t\t// name was in fact the start of a call\n\t\t\tcall.Args = append([]*Word{cc.Name}, call.Args...)\n\t\t\tcc.Name = nil\n\t\t}\n\t}\n\ts.Cmd = cc\n}\n\nfunc (p *Parser) letClause(s *Stmt) {\n\tlc := &LetClause{Let: p.pos}\n\told := p.preNested(arithmExprLet)\n\tp.next()\n\tfor !p.stopToken() && !p.peekRedir() {\n\t\tx := p.arithmExpr(true)\n\t\tif x == nil {\n\t\t\tbreak\n\t\t}\n\t\tlc.Exprs = append(lc.Exprs, x)\n\t}\n\tif len(lc.Exprs) == 0 {\n\t\tp.followErrExp(lc.Let, \"let\")\n\t}\n\tp.postNested(old)\n\ts.Cmd = lc\n}\n\nfunc (p *Parser) bashFuncDecl(s *Stmt) {\n\tfpos := p.pos\n\tp.next()\n\tnames := make([]*Lit, 0, 1)\n\tfor p.tok == _LitWord && p.val != \"{\" {\n\t\tnames = append(names, p.lit(p.pos, p.val))\n\t\tp.next()\n\t}\n\thasParens := p.got(leftParen)\n\tswitch len(names) {\n\tcase 0:\n\t\tif hasParens || (p.tok == _LitWord && p.val == \"{\") {\n\t\t\tp.checkLang(fpos, LangZsh, \"anonymous functions\")\n\t\t} else if !p.lang.in(LangZsh) {\n\t\t\tp.followErr(fpos, \"function\", noQuote(\"a name\"))\n\t\t}\n\t\tnames = nil // avoid non-nil zero-length slices\n\tcase 1:\n\t\t// allowed in all variants\n\tdefault:\n\t\tp.checkLang(fpos, LangZsh, \"multi-name functions\")\n\t}\n\tif hasParens {\n\t\tp.follow(fpos, \"function foo(\", rightParen)\n\t}\n\tp.funcDecl(s, fpos, true, hasParens, names...)\n}\n\nfunc (p *Parser) testDecl(s *Stmt) {\n\ttd := &TestDecl{Position: p.pos}\n\tp.next()\n\tif td.Description = p.getWord(); td.Description == nil {\n\t\tp.followErr(td.Position, \"@test\", noQuote(\"a description word\"))\n\t}\n\tif td.Body = p.getStmt(false, false, true); td.Body == nil {\n\t\tp.followErr(td.Position, `@test \"desc\"`, noQuote(\"a statement\"))\n\t}\n\ts.Cmd = td\n}\n\nfunc (p *Parser) callExpr(s *Stmt, w *Word, assign bool) {\n\tce := p.call(w)\n\tif w == nil {\n\t\tce.Args = ce.Args[:0]\n\t}\n\tif assign {\n\t\tce.Assigns = append(ce.Assigns, p.getAssign(true))\n\t}\nloop:\n\tfor {\n\t\tswitch p.tok {\n\t\tcase _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, andPipe, andBang,\n\t\t\tdblSemicolon, semiAnd, dblSemiAnd, semiOr:\n\t\t\tbreak loop\n\t\tcase _LitWord:\n\t\t\tif len(ce.Args) == 0 && p.hasValidIdent() {\n\t\t\t\tce.Assigns = append(ce.Assigns, p.getAssign(true))\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Avoid failing later with the confusing \"} can only be used to close a block\".\n\t\t\tif p.val == \"{\" && w != nil && w.Lit() == \"function\" {\n\t\t\t\tp.checkLang(p.pos, langBashLike, `the \"function\" builtin`)\n\t\t\t}\n\t\t\t// Zsh does not require a semicolon to close a block.\n\t\t\tif p.lang.in(LangZsh) && p.val == \"}\" {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t\tw := p.wordOne(p.lit(p.pos, p.val))\n\t\t\tp.next()\n\t\t\tif p.lang.in(LangZsh) && !p.spaced {\n\t\t\t\tw.Parts = append(w.Parts, p.wordParts(nil)...)\n\t\t\t}\n\t\t\tce.Args = append(ce.Args, w)\n\t\tcase _Lit:\n\t\t\tif len(ce.Args) == 0 && p.hasValidIdent() {\n\t\t\t\tce.Assigns = append(ce.Assigns, p.getAssign(true))\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tce.Args = append(ce.Args, p.wordAnyNumber())\n\t\tcase bckQuote:\n\t\t\tif p.backquoteEnd() {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase dollBrace, dollDblParen, dollParen, dollar, cmdIn, assgnParen, cmdOut,\n\t\t\tsglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack,\n\t\t\tglobQuest, globStar, globPlus, globAt, globExcl:\n\t\t\tce.Args = append(ce.Args, p.wordAnyNumber())\n\t\tcase dblLeftParen:\n\t\t\tp.curErr(\"%#q can only be used to open an arithmetic cmd\", p.tok)\n\t\tcase rightParen:\n\t\t\tif p.quote == subCmd {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t\tfallthrough\n\t\tdefault:\n\t\t\tif p.peekRedir() {\n\t\t\t\tp.doRedirect(s)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Note that we'll only keep the first error that happens.\n\t\t\tif len(ce.Args) > 0 {\n\t\t\t\tif cmd := ce.Args[0].Lit(); isBashCompoundCommand(_LitWord, cmd) {\n\t\t\t\t\tp.checkLang(p.pos, langBashLike, \"the %#q builtin\", cmd)\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.curErr(\"a command can only contain words and redirects; encountered %#q\", p.tok)\n\t\t}\n\t}\n\tif len(ce.Args) == 0 {\n\t\tce.Args = nil\n\t} else {\n\t\tfor _, asgn := range ce.Assigns {\n\t\t\tif asgn.Index != nil || asgn.Array != nil {\n\t\t\t\tp.posErr(asgn.Pos(), \"inline variables cannot be arrays\")\n\t\t\t}\n\t\t}\n\t}\n\ts.Cmd = ce\n}\n\nfunc (p *Parser) funcDecl(s *Stmt, pos Pos, long, withParens bool, names ...*Lit) {\n\tfd := &FuncDecl{\n\t\tPosition: pos,\n\t\tRsrvWord: long,\n\t\tParens:   withParens,\n\t}\n\tif len(names) == 1 {\n\t\tfd.Name = names[0]\n\t} else {\n\t\tfd.Names = names\n\t}\n\tp.got(_Newl)\n\t// TODO: reject any body which isn't a compound command, like a quoted word\n\tif fd.Body = p.getStmt(false, false, true); fd.Body == nil {\n\t\tp.followErr(fd.Pos(), \"foo()\", noQuote(\"a statement\"))\n\t}\n\ts.Cmd = fd\n}\n"
  },
  {
    "path": "syntax/parser_arithm.go",
    "content": "package syntax\n\n// compact specifies whether we allow spaces between expressions.\n// This is true for let\nfunc (p *Parser) arithmExpr(compact bool) ArithmExpr {\n\treturn p.arithmExprComma(compact)\n}\n\n// These function names are inspired by Bash's expr.c\n\nfunc (p *Parser) arithmExprComma(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprAssign, Comma)\n}\n\nfunc (p *Parser) arithmExprAssign(compact bool) ArithmExpr {\n\t// Assign is different from the other binary operators because it's\n\t// right-associative and needs to check that it's placed after a name\n\tvalue := p.arithmExprTernary(compact)\n\tswitch BinAritOperator(p.tok) {\n\tcase AddAssgn, SubAssgn, MulAssgn, QuoAssgn, RemAssgn, AndAssgn,\n\t\tOrAssgn, XorAssgn, ShlAssgn, ShrAssgn, Assgn,\n\t\tAndBoolAssgn, OrBoolAssgn, XorBoolAssgn, PowAssgn:\n\t\tif compact && p.spaced {\n\t\t\treturn value\n\t\t}\n\t\tif !isArithName(value) {\n\t\t\tp.posErr(p.pos, \"%#q must follow a name\", p.tok)\n\t\t}\n\t\tpos := p.pos\n\t\ttok := p.tok\n\t\tp.nextArithOp(compact)\n\t\ty := p.arithmExprAssign(compact)\n\t\tif y == nil {\n\t\t\tp.followErrExp(pos, tok)\n\t\t}\n\t\treturn &BinaryArithm{\n\t\t\tOpPos: pos,\n\t\t\tOp:    BinAritOperator(tok),\n\t\t\tX:     value,\n\t\t\tY:     y,\n\t\t}\n\t}\n\treturn value\n}\n\nfunc (p *Parser) arithmExprTernary(compact bool) ArithmExpr {\n\tvalue := p.arithmExprLor(compact)\n\tif BinAritOperator(p.tok) != TernQuest || (compact && p.spaced) {\n\t\treturn value\n\t}\n\n\tif value == nil {\n\t\tp.curErr(\"%#q must follow an expression\", p.tok)\n\t}\n\tquestPos := p.pos\n\tp.nextArithOp(compact)\n\tif BinAritOperator(p.tok) == TernColon {\n\t\tp.followErrExp(questPos, TernQuest)\n\t}\n\ttrueExpr := p.arithmExpr(compact)\n\tif trueExpr == nil {\n\t\tp.followErrExp(questPos, TernQuest)\n\t}\n\tif BinAritOperator(p.tok) != TernColon {\n\t\tp.posErr(questPos, \"ternary operator missing %#q after %#q\", colon, quest)\n\t}\n\tcolonPos := p.pos\n\tp.nextArithOp(compact)\n\tfalseExpr := p.arithmExprTernary(compact)\n\tif falseExpr == nil {\n\t\tp.followErrExp(colonPos, TernColon)\n\t}\n\treturn &BinaryArithm{\n\t\tOpPos: questPos,\n\t\tOp:    TernQuest,\n\t\tX:     value,\n\t\tY: &BinaryArithm{\n\t\t\tOpPos: colonPos,\n\t\t\tOp:    TernColon,\n\t\t\tX:     trueExpr,\n\t\t\tY:     falseExpr,\n\t\t},\n\t}\n}\n\nfunc (p *Parser) arithmExprLor(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprLand, OrArit, XorBool)\n}\n\nfunc (p *Parser) arithmExprLand(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprBor, AndArit)\n}\n\nfunc (p *Parser) arithmExprBor(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprBxor, Or)\n}\n\nfunc (p *Parser) arithmExprBxor(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprBand, Xor)\n}\n\nfunc (p *Parser) arithmExprBand(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprEquality, And)\n}\n\nfunc (p *Parser) arithmExprEquality(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprComparison, Eql, Neq)\n}\n\nfunc (p *Parser) arithmExprComparison(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprShift, Lss, Gtr, Leq, Geq)\n}\n\nfunc (p *Parser) arithmExprShift(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprAddition, Shl, Shr)\n}\n\nfunc (p *Parser) arithmExprAddition(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprMultiplication, Add, Sub)\n}\n\nfunc (p *Parser) arithmExprMultiplication(compact bool) ArithmExpr {\n\treturn p.arithmExprBinary(compact, p.arithmExprPower, Mul, Quo, Rem)\n}\n\nfunc (p *Parser) arithmExprPower(compact bool) ArithmExpr {\n\t// Power is different from the other binary operators because it's right-associative\n\tvalue := p.arithmExprUnary(compact)\n\tif BinAritOperator(p.tok) != Pow || (compact && p.spaced) {\n\t\treturn value\n\t}\n\n\tif value == nil {\n\t\tp.curErr(\"%#q must follow an expression\", p.tok)\n\t}\n\n\top := p.tok\n\tpos := p.pos\n\tp.nextArithOp(compact)\n\ty := p.arithmExprPower(compact)\n\tif y == nil {\n\t\tp.followErrExp(pos, op)\n\t}\n\treturn &BinaryArithm{\n\t\tOpPos: pos,\n\t\tOp:    BinAritOperator(op),\n\t\tX:     value,\n\t\tY:     y,\n\t}\n}\n\nfunc (p *Parser) arithmExprUnary(compact bool) ArithmExpr {\n\tif !compact {\n\t\tp.got(_Newl)\n\t}\n\n\tswitch UnAritOperator(p.tok) {\n\tcase Not, BitNegation, Plus, Minus:\n\t\tue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)}\n\t\tp.nextArithOp(compact)\n\t\tif ue.X = p.arithmExprUnary(compact); ue.X == nil {\n\t\t\tp.followErrExp(ue.OpPos, ue.Op)\n\t\t}\n\t\treturn ue\n\t}\n\treturn p.arithmExprValue(compact)\n}\n\nfunc (p *Parser) arithmExprValue(compact bool) ArithmExpr {\n\tvar x ArithmExpr\n\tswitch p.tok {\n\tcase addAdd, subSub:\n\t\tue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)}\n\t\tp.nextArith(compact)\n\t\tif p.tok != _LitWord {\n\t\t\tp.followErr(ue.OpPos, ue.Op, noQuote(\"a literal\"))\n\t\t}\n\t\tue.X = p.arithmExprValue(compact)\n\t\treturn ue\n\tcase leftParen:\n\t\tif p.quote == paramExpArithm && p.lang.in(LangZsh) {\n\t\t\tx = p.zshSubFlags()\n\t\t\tbreak\n\t\t}\n\t\tpe := &ParenArithm{Lparen: p.pos}\n\t\tp.nextArithOp(compact)\n\t\tpe.X = p.followArithm(leftParen, pe.Lparen)\n\t\tpe.Rparen = p.matched(pe.Lparen, leftParen, rightParen)\n\t\tif p.quote == paramExpArithm && p.tok == _LitWord {\n\t\t\tp.checkLang(pe.Lparen, LangZsh, \"subscript flags\")\n\t\t}\n\t\tx = pe\n\tcase leftBrack:\n\t\tp.curErr(\"%#q must follow a name like a[i]\", p.tok)\n\tcase colon:\n\t\tp.curErr(\"ternary operator missing %#q before %#q\", quest, colon)\n\tcase _LitWord:\n\t\tl := p.getLit()\n\t\tif p.tok != leftBrack {\n\t\t\tx = p.wordOne(l)\n\t\t\tbreak\n\t\t}\n\t\tpe := &ParamExp{Short: true, Param: l}\n\t\tpe.Index = p.eitherIndex()\n\t\tx = p.wordOne(pe)\n\tcase bckQuote:\n\t\tif p.quote == arithmExprLet && p.openBquotes > 0 {\n\t\t\treturn nil\n\t\t}\n\t\tfallthrough\n\tdefault:\n\t\tif w := p.getWord(); w != nil {\n\t\t\tx = w\n\t\t} else {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif compact && p.spaced {\n\t\treturn x\n\t}\n\tif !compact {\n\t\tp.got(_Newl)\n\t}\n\n\t// we want real nil, not (*Word)(nil) as that\n\t// sets the type to non-nil and then x != nil\n\tif p.tok == addAdd || p.tok == subSub {\n\t\tif !isArithName(x) {\n\t\t\tp.curErr(\"%#q must follow a name\", p.tok)\n\t\t}\n\t\tu := &UnaryArithm{\n\t\t\tPost:  true,\n\t\t\tOpPos: p.pos,\n\t\t\tOp:    UnAritOperator(p.tok),\n\t\t\tX:     x,\n\t\t}\n\t\tp.nextArith(compact)\n\t\treturn u\n\t}\n\treturn x\n}\n\n// nextArith consumes a token.\n// It returns true if compact and the token was followed by spaces\nfunc (p *Parser) nextArith(compact bool) bool {\n\tp.next()\n\tif compact && p.spaced {\n\t\treturn true\n\t}\n\tif !compact {\n\t\tp.got(_Newl)\n\t}\n\treturn false\n}\n\nfunc (p *Parser) nextArithOp(compact bool) {\n\tpos := p.pos\n\ttok := p.tok\n\tif p.nextArith(compact) {\n\t\tp.followErrExp(pos, tok)\n\t}\n}\n\n// arithmExprBinary is used for all left-associative binary operators\nfunc (p *Parser) arithmExprBinary(compact bool, nextOp func(bool) ArithmExpr, operators ...BinAritOperator) ArithmExpr {\n\tvalue := nextOp(compact)\n\tfor {\n\t\tvar foundOp BinAritOperator\n\t\tfor _, op := range operators {\n\t\t\tif p.tok == token(op) {\n\t\t\t\tfoundOp = op\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif token(foundOp) == illegalTok || (compact && p.spaced) {\n\t\t\treturn value\n\t\t}\n\n\t\tif value == nil {\n\t\t\tp.curErr(\"%#q must follow an expression\", p.tok)\n\t\t}\n\n\t\tpos := p.pos\n\t\tp.nextArithOp(compact)\n\t\ty := nextOp(compact)\n\t\tif y == nil {\n\t\t\tp.followErrExp(pos, foundOp)\n\t\t}\n\n\t\tvalue = &BinaryArithm{\n\t\t\tOpPos: pos,\n\t\t\tOp:    foundOp,\n\t\t\tX:     value,\n\t\t\tY:     y,\n\t\t}\n\t}\n}\n\nfunc isArithName(left ArithmExpr) bool {\n\tw, ok := left.(*Word)\n\tif !ok || len(w.Parts) != 1 {\n\t\treturn false\n\t}\n\tswitch wp := w.Parts[0].(type) {\n\tcase *Lit:\n\t\treturn ValidName(wp.Value)\n\tcase *ParamExp:\n\t\treturn wp.nakedIndex()\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (p *Parser) followArithm(ftok token, fpos Pos) ArithmExpr {\n\tx := p.arithmExpr(false)\n\tif x == nil {\n\t\tp.followErrExp(fpos, ftok)\n\t}\n\treturn x\n}\n\nfunc (p *Parser) peekArithmEnd() bool {\n\treturn p.tok == rightParen && p.r == ')'\n}\n\nfunc (p *Parser) arithmMatchingErr(pos Pos, left, right token) {\n\tswitch p.tok {\n\tcase _Lit, _LitWord:\n\t\tp.curErr(\"not a valid arithmetic operator: %#q\", p.val)\n\tcase leftBrack:\n\t\tp.curErr(\"%#q must follow a name like a[i]\", leftBrack)\n\tcase colon:\n\t\tp.curErr(\"ternary operator missing %#q before %#q\", quest, colon)\n\tcase rightParen, _EOF:\n\t\tp.matchingErr(pos, left, right)\n\tcase period:\n\t\tp.checkLang(p.pos, LangZsh, `floating point arithmetic`)\n\tdefault:\n\t\tif p.quote&allArithmExpr != 0 {\n\t\t\tp.curErr(\"not a valid arithmetic operator: %#q\", p.tok)\n\t\t}\n\t\tp.matchingErr(pos, left, right)\n\t}\n}\n\nfunc (p *Parser) matchedArithm(lpos Pos, left, right token) {\n\tif !p.got(right) {\n\t\tp.arithmMatchingErr(lpos, left, right)\n\t}\n}\n\nfunc (p *Parser) arithmEnd(ltok token, lpos Pos, old saveState) Pos {\n\tif !p.peekArithmEnd() {\n\t\tif p.recoverError() {\n\t\t\treturn recoveredPos\n\t\t}\n\t\tp.arithmMatchingErr(lpos, ltok, dblRightParen)\n\t}\n\tp.rune()\n\tp.postNested(old)\n\tpos := p.pos\n\tp.next()\n\treturn pos\n}\n"
  },
  {
    "path": "syntax/parser_linux_test.go",
    "content": "// Copyright (c) 2025, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc killCommandOnTestExit(cmd *exec.Cmd) {\n\t// It's incredibly easy to let an external shell loop forever by accident.\n\t// In those cases, kill it as soon as the Go test process finishes.\n\t//\n\t// Similarly, when killing the shell process due to a timeout,\n\t// also kill any children processes that the shell spawned via a group.\n\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\tPdeathsig: syscall.SIGKILL,\n\t\tSetpgid:   true,\n\t}\n\tcmd.Cancel = func() error {\n\t\treturn syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)\n\t}\n}\n"
  },
  {
    "path": "syntax/parser_other_test.go",
    "content": "// Copyright (c) 2025, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n//go:build !linux\n\npackage syntax\n\nimport \"os/exec\"\n\nfunc killCommandOnTestExit(cmd *exec.Cmd) {\n\t// We don't develop outside of Linux at the moment.\n}\n"
  },
  {
    "path": "syntax/parser_test.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-quicktest/qt\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"mvdan.cc/sh/v3/internal\"\n)\n\nfunc TestParseFiles(t *testing.T) {\n\tt.Parallel()\n\tfor lang := range langResolvedVariants.bits() {\n\t\tt.Run(lang.String(), func(t *testing.T) {\n\t\t\tp := NewParser(Variant(lang))\n\t\t\tfor i, c := range append(fileTests, fileTestsNoPrint...) {\n\t\t\t\twant := c.byLangIndex[lang.index()]\n\t\t\t\tswitch want := want.(type) {\n\t\t\t\tcase nil:\n\t\t\t\t\tcontinue\n\t\t\t\tcase *File:\n\t\t\t\t\tfor j, in := range c.inputs {\n\t\t\t\t\t\tt.Run(fmt.Sprintf(\"OK/%03d-%d\", i, j), singleParse(p, in, want))\n\t\t\t\t\t}\n\t\t\t\tcase string:\n\t\t\t\t\twant = strings.Replace(want, \"LANG\", p.lang.String(), 1)\n\t\t\t\t\tfor j, in := range c.inputs {\n\t\t\t\t\t\tt.Run(fmt.Sprintf(\"Err/%03d-%d\", i, j), func(t *testing.T) {\n\t\t\t\t\t\t\tt.Logf(\"input: %s\", in)\n\t\t\t\t\t\t\t_, err := p.Parse(newStrictReader(in), \"\")\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tt.Fatalf(\"Expected error: %v\", want)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif got := err.Error(); got != want {\n\t\t\t\t\t\t\t\tt.Fatalf(\"Error mismatch\\nwant: %s\\ngot:  %s\",\n\t\t\t\t\t\t\t\t\twant, got)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseErr(t *testing.T) {\n\tt.Parallel()\n\tfor lang := range langResolvedVariants.bits() {\n\t\tt.Run(lang.String(), func(t *testing.T) {\n\t\t\tp := NewParser(Variant(lang), KeepComments(true))\n\t\t\tfor _, c := range errorCases {\n\t\t\t\twant := c.byLangIndex[lang.index()]\n\t\t\t\tif want == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tt.Run(\"\", func(t *testing.T) { // number them #001, #002, ...\n\t\t\t\t\twant = strings.Replace(want, \"LANG\", p.lang.String(), 1)\n\t\t\t\t\tt.Logf(\"input: %s\", c.in)\n\t\t\t\t\t_, err := p.Parse(newStrictReader(c.in), \"\")\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tt.Fatalf(\"Expected error: %v\", want)\n\t\t\t\t\t}\n\t\t\t\t\tif got := err.Error(); got != want {\n\t\t\t\t\t\tt.Fatalf(\"Error mismatch\\nwant: %s\\ngot:  %s\",\n\t\t\t\t\t\t\twant, got)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseConfirm(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"calling external shells is slow\")\n\t}\n\tfor lang := range langResolvedVariants.bits() {\n\t\tt.Run(lang.String(), func(t *testing.T) {\n\t\t\texternal, ok := externalShells[lang]\n\t\t\tif !ok {\n\t\t\t\tt.Skip(\"no external shell to check against\")\n\t\t\t}\n\t\t\tif external.require != nil {\n\t\t\t\texternal.require(t)\n\t\t\t}\n\t\t\tfor i, c := range append(fileTests, fileTestsNoPrint...) {\n\t\t\t\twant := c.byLangIndex[lang.index()]\n\t\t\t\tswitch want.(type) {\n\t\t\t\tcase nil:\n\t\t\t\t\tcontinue\n\t\t\t\tcase *File:\n\t\t\t\t\tfor j, in := range c.inputs {\n\t\t\t\t\t\twantErr := lang.in(c.flipConfirmSet)\n\t\t\t\t\t\tt.Run(fmt.Sprintf(\"OK/%03d-%d\", i, j), confirmParse(in, external.cmd, wantErr))\n\t\t\t\t\t}\n\t\t\t\tcase string:\n\t\t\t\t\tfor j, in := range c.inputs {\n\t\t\t\t\t\twantErr := !lang.in(c.flipConfirmSet)\n\t\t\t\t\t\tt.Run(fmt.Sprintf(\"Err/%03d-%d\", i, j), confirmParse(in, external.cmd, wantErr))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif lang == LangZsh {\n\t\t\t\treturn // TODO: we don't confirm errors with zsh yet\n\t\t\t}\n\t\t\tfor i, c := range errorCases {\n\t\t\t\twant := c.byLangIndex[lang.index()]\n\t\t\t\tif want == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\twantErr := !lang.in(c.flipConfirmSet)\n\t\t\t\tt.Run(fmt.Sprintf(\"ErrOld/%03d\", i), confirmParse(c.in, external.cmd, wantErr))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseBashKeepComments(t *testing.T) {\n\tt.Parallel()\n\tp := NewParser(KeepComments(true))\n\tfor i, c := range fileTestsKeepComments {\n\t\twant, _ := c.byLangIndex[LangBash.index()].(*File)\n\t\tif want == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor j, in := range c.inputs {\n\t\t\tt.Run(fmt.Sprintf(\"%03d-%d\", i, j), singleParse(p, in, want))\n\t\t}\n\t}\n}\n\nfunc TestParsePosOverflow(t *testing.T) {\n\tt.Parallel()\n\n\t// Consider using a custom reader to save memory.\n\ttests := []struct {\n\t\tname, in, want string\n\t}{\n\t\t{\n\t\t\t\"LineOverflowIsValid\",\n\t\t\tstrings.Repeat(\"\\n\", lineMax) + \"foo; bar\",\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"LineOverflowPosString\",\n\t\t\tstrings.Repeat(\"\\n\", lineMax) + \")\",\n\t\t\t\"?:1: `)` can only be used to close a subshell\",\n\t\t},\n\t\t{\n\t\t\t\"LineOverflowExtraPosString\",\n\t\t\tstrings.Repeat(\"\\n\", lineMax+5) + \")\",\n\t\t\t\"?:1: `)` can only be used to close a subshell\",\n\t\t},\n\t\t{\n\t\t\t\"ColOverflowPosString\",\n\t\t\tstrings.Repeat(\" \", colMax) + \")\",\n\t\t\t\"1:?: `)` can only be used to close a subshell\",\n\t\t},\n\t\t{\n\t\t\t\"ColOverflowExtraPosString\",\n\t\t\tstrings.Repeat(\" \", colMax) + \")\",\n\t\t\t\"1:?: `)` can only be used to close a subshell\",\n\t\t},\n\t\t{\n\t\t\t\"ColOverflowSkippedPosString\",\n\t\t\tstrings.Repeat(\" \", colMax+5) + \"\\n)\",\n\t\t\t\"2:1: `)` can only be used to close a subshell\",\n\t\t},\n\t\t{\n\t\t\t\"LargestLineNumber\",\n\t\t\tstrings.Repeat(\"\\n\", lineMax-1) + \")\",\n\t\t\t\"262143:1: `)` can only be used to close a subshell\",\n\t\t},\n\t\t{\n\t\t\t\"LargestColNumber\",\n\t\t\tstrings.Repeat(\" \", colMax-1) + \")\",\n\t\t\t\"1:16383: `)` can only be used to close a subshell\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tp := NewParser()\n\t\t\t_, err := p.Parse(strings.NewReader(test.in), \"\")\n\t\t\tgot := fmt.Sprint(err)\n\t\t\tif got != test.want {\n\t\t\t\tt.Fatalf(\"want error %q, got %q\", test.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMain(m *testing.M) {\n\tinternal.TestMainSetup()\n\tm.Run()\n}\n\nvar (\n\tonceHasBash52 = sync.OnceValue(func() bool {\n\t\treturn cmdContains(\"version 5.3\", \"bash\", \"--version\")\n\t})\n\n\tonceHasDash059 = sync.OnceValue(func() bool {\n\t\t// dash provides no way to check its version, so we have to\n\t\t// check if it's new enough as to not have the bug that breaks\n\t\t// our integration tests.\n\t\t// This also means our check does not require a specific version.\n\t\t//\n\t\t// We get odd failures on Windows on CI, and it's hard to debug\n\t\t// or even understand what version of dash it's using; skip on those.\n\t\treturn cmdContains(\"Bad subst\", \"dash\", \"-c\", \"echo ${#<}\") &&\n\t\t\truntime.GOOS != \"windows\"\n\t})\n\n\tonceHasMksh59 = sync.OnceValue(func() bool {\n\t\treturn cmdContains(\" R59 \", \"mksh\", \"-c\", \"echo $KSH_VERSION\")\n\t})\n\n\tonceHasZsh59 = sync.OnceValue(func() bool {\n\t\treturn cmdContains(\"zsh 5.9\", \"zsh\", \"--version\")\n\t})\n)\n\ntype externalShell struct {\n\tcmd     string\n\trequire func(testing.TB)\n}\n\n// requireShells can be set to make sure that no external shell tests\n// are being skipped due to a misalignment in installed versions.\nvar requireShells = os.Getenv(\"REQUIRE_SHELLS\") == \"1\"\n\nfunc skipExternal(tb testing.TB, message string) {\n\tif requireShells {\n\t\ttb.Fatal(message)\n\t} else {\n\t\ttb.Skip(message)\n\t}\n}\n\n// Note that externalShells is a map, and not an array,\n// because [LangVariant.index] is not a constant expression.\n// This seems fine; this table is only for the sake of testing.\nvar externalShells = map[LangVariant]externalShell{\n\tLangBash: {\"bash\", func(tb testing.TB) {\n\t\tif !onceHasBash52() {\n\t\t\tskipExternal(tb, \"bash 5.2 required to run\")\n\t\t}\n\t}},\n\tLangPOSIX: {\"dash\", func(tb testing.TB) {\n\t\tif !onceHasDash059() {\n\t\t\tskipExternal(tb, \"dash 0.5.9+ required to run\")\n\t\t}\n\t}},\n\tLangMirBSDKorn: {\"mksh\", func(tb testing.TB) {\n\t\tif !onceHasMksh59() {\n\t\t\tskipExternal(tb, \"mksh 59 required to run\")\n\t\t}\n\t}},\n\tLangZsh: {\"zsh\", func(tb testing.TB) {\n\t\tif !onceHasZsh59() {\n\t\t\tskipExternal(tb, \"zsh 5.9 required to run\")\n\t\t}\n\t}},\n}\n\nfunc cmdContains(substr, cmd string, args ...string) bool {\n\tout, err := exec.Command(cmd, args...).CombinedOutput()\n\tgot := string(out)\n\tif err != nil {\n\t\tgot += \"\\n\" + err.Error()\n\t}\n\treturn strings.Contains(got, substr)\n}\n\nvar extGlobRe = regexp.MustCompile(`[@?*+!]\\(`)\n\nfunc confirmParse(in, cmd string, wantErr bool) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tt.Helper()\n\t\tt.Parallel()\n\t\tt.Logf(\"input: %s\", in)\n\t\tvar opts []string\n\t\tif strings.Contains(in, \"\\\\\\r\\n\") {\n\t\t\tt.Skip(\"shells do not generally support CRLF line endings\")\n\t\t}\n\t\tif cmd == \"bash\" && extGlobRe.MatchString(in) {\n\t\t\t// otherwise bash refuses to parse these\n\t\t\t// properly. Also avoid -n since that too makes\n\t\t\t// bash bail.\n\t\t\tin = \"shopt -s extglob\\n\" + in\n\t\t} else if !wantErr {\n\t\t\t// -n makes bash accept invalid inputs like\n\t\t\t// \"let\" or \"`{`\", so only use it in\n\t\t\t// non-erroring tests. Should be safe to not use\n\t\t\t// -n anyway since these are supposed to just fail.\n\t\t\t// also, -n will break if we are using extglob\n\t\t\t// as extglob is not actually applied.\n\t\t\topts = append(opts, \"-n\")\n\t\t}\n\n\t\t// All the bits of shell we test should either finish or fail very quickly,\n\t\t// given that they are very small. If we make a mistake with an endless loop,\n\t\t// or we somehow trigger a bug that makes a shell hang, kill it.\n\t\tctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)\n\t\tdefer cancel()\n\n\t\tcmd := exec.CommandContext(ctx, cmd, opts...)\n\t\tkillCommandOnTestExit(cmd)\n\t\tcmd.Dir = t.TempDir() // to be safe\n\t\tcmd.Stdin = strings.NewReader(in)\n\t\tvar stderrBuf strings.Builder\n\t\tcmd.Stderr = &stderrBuf\n\t\terr := cmd.Run()\n\n\t\tif cmd.ProcessState.ExitCode() == -1 {\n\t\t\tt.Fatalf(\"shell terminated by signal: %v\", err)\n\t\t}\n\n\t\t// bash sometimes likes to error on an input via stderr\n\t\t// while forgetting to set the exit code to non-zero. Fun.\n\t\t// Note that we do not treat warnings as errors.\n\t\tstderrLines := strings.Split(stderrBuf.String(), \"\\n\")\n\t\tfor i, line := range stderrLines {\n\t\t\tstderrLines[i] = strings.TrimSpace(line)\n\t\t}\n\t\tstderrLines = slices.DeleteFunc(stderrLines, func(line string) bool {\n\t\t\treturn line == \"\" || strings.Contains(line, \"warning:\")\n\t\t})\n\t\tif stderr := strings.Join(stderrLines, \"\\n\"); stderr != \"\" {\n\t\t\tif err == nil {\n\t\t\t\terr = fmt.Errorf(\"non-fatal error: %s\", stderr)\n\t\t\t} else {\n\t\t\t\terr = fmt.Errorf(\"%v: %s\", err, stderr)\n\t\t\t}\n\t\t}\n\n\t\tif wantErr && err == nil {\n\t\t\tt.Fatalf(\"Expected error in %q\", strings.Join(cmd.Args, \" \"))\n\t\t} else if !wantErr && err != nil {\n\t\t\tt.Fatalf(\"Unexpected error in %q: %v\", strings.Join(cmd.Args, \" \"), err)\n\t\t}\n\t}\n}\n\nvar cmpOpt = cmp.FilterValues(func(p1, p2 Pos) bool { return true }, cmp.Ignore())\n\nfunc singleParse(p *Parser, in string, want *File) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tt.Helper()\n\t\tt.Logf(\"input: %s\", in)\n\t\tgot, err := p.Parse(newStrictReader(in), \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Unexpected error: %v\", err)\n\t\t}\n\t\tWalk(got, sanityChecker{tb: t, src: in}.visit)\n\t\tqt.Assert(t, qt.CmpEquals(got, want, cmpOpt))\n\t}\n}\n\ntype errorCase struct {\n\tin string\n\n\tbyLangIndex [langResolvedVariantsCount]string\n\n\t// The real shells where testing the input succeeds rather than failing as expected.\n\tflipConfirmSet LangVariant\n}\n\nfunc errCase(in string, opts ...func(*errorCase)) errorCase {\n\tc := errorCase{in: in}\n\tfor _, o := range opts {\n\t\to(&c)\n\t}\n\treturn c\n}\n\nfunc langErr(want string, langSets ...LangVariant) func(*errorCase) {\n\treturn func(c *errorCase) {\n\t\t// The parameter is a slice to allow omitting the argument.\n\t\tswitch len(langSets) {\n\t\tcase 0:\n\t\t\tfor i := range c.byLangIndex {\n\t\t\t\tc.byLangIndex[i] = want\n\t\t\t}\n\t\t\treturn\n\t\tcase 1:\n\t\t\t// continue below\n\t\tdefault:\n\t\t\tpanic(\"use a LangVariant bitset\")\n\t\t}\n\t\tfor lang := range langSets[0].bits() {\n\t\t\tc.byLangIndex[lang.index()] = want\n\t\t}\n\t}\n}\n\nfunc flipConfirm(langSet LangVariant) func(*errorCase) {\n\treturn func(c *errorCase) { c.flipConfirmSet = langSet }\n}\n\nvar flipConfirmAll = flipConfirm(langResolvedVariants)\n\n// The real shells which allow unclosed heredocs.\n// TODO: allow ending a heredoc at EOF in these language variant modes.\nvar flipConfirmUnclosedHeredoc = flipConfirm(LangBash | LangPOSIX | LangBats | LangZsh)\n\nfunc init() {\n\tseenInputs := make(map[string]bool)\n\tfor i, c := range errorCases {\n\t\tif seenInputs[c.in] {\n\t\t\tpanic(fmt.Sprintf(\"duplicate at %d: %q\", i, c.in))\n\t\t}\n\t\tseenInputs[c.in] = true\n\t}\n}\n\nvar errorCases = []errorCase{\n\terrCase(\n\t\t\"echo \\x80\",\n\t\tlangErr(\"1:6: invalid UTF-8 encoding\"),\n\t\tflipConfirmAll, // common shells use bytes\n\t),\n\terrCase(\n\t\t\"\\necho \\x80\",\n\t\tlangErr(\"2:6: invalid UTF-8 encoding\"),\n\t\tflipConfirmAll, // common shells use bytes\n\t),\n\terrCase(\n\t\t\"echo foo\\x80bar\",\n\t\tlangErr(\"1:9: invalid UTF-8 encoding\"),\n\t\tflipConfirmAll, // common shells use bytes\n\t),\n\terrCase(\n\t\t\"echo foo\\xc3\",\n\t\tlangErr(\"1:9: invalid UTF-8 encoding\"),\n\t\tflipConfirmAll, // common shells use bytes\n\t),\n\terrCase(\n\t\t\"#foo\\xc3\",\n\t\tlangErr(\"1:5: invalid UTF-8 encoding\"),\n\t\tflipConfirmAll, // common shells use bytes\n\t),\n\terrCase(\n\t\t\"echo a\\x80\",\n\t\tlangErr(\"1:7: invalid UTF-8 encoding\"),\n\t\tflipConfirmAll, // common shells use bytes\n\t),\n\terrCase(\n\t\t\"<<$\\xc8\\n$\\xc8\",\n\t\tlangErr(\"1:4: invalid UTF-8 encoding\"),\n\t\tflipConfirmAll, // common shells use bytes\n\t),\n\terrCase(\n\t\t\"echo $((foo\\x80bar\",\n\t\tlangErr(\"1:12: invalid UTF-8 encoding\"),\n\t),\n\terrCase(\n\t\t\"z=($\\\\\\n#\\\\\\n\\\\\\n$#\\x91\\\\\\n\",\n\t\tlangErr(\"4:3: invalid UTF-8 encoding\", LangBash),\n\t),\n\terrCase(\n\t\t`${ `,\n\t\tlangErr(\"1:1: reached EOF without matching `${` with `}`\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t`${ foo;`,\n\t\tlangErr(\"1:1: reached EOF without matching `${` with `}`\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t`${ foo }`,\n\t\tlangErr(\"1:1: reached EOF without matching `${` with `}`\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t`${|`,\n\t\tlangErr(\"1:1: reached EOF without matching `${` with `}`\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t`${|foo;`,\n\t\tlangErr(\"1:1: reached EOF without matching `${` with `}`\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t`${|foo }`,\n\t\tlangErr(\"1:1: reached EOF without matching `${` with `}`\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"((foo\\x80bar\",\n\t\tlangErr(\"1:6: invalid UTF-8 encoding\"),\n\t),\n\terrCase(\n\t\t\";\\x80\",\n\t\tlangErr(\"1:2: invalid UTF-8 encoding\"),\n\t),\n\terrCase(\n\t\t\"${a\\x80\",\n\t\tlangErr(\"1:4: invalid UTF-8 encoding\"),\n\t),\n\terrCase(\n\t\t\"${a#\\x80\",\n\t\tlangErr(\"1:5: invalid UTF-8 encoding\"),\n\t),\n\terrCase(\n\t\t\"${a-'\\x80\",\n\t\tlangErr(\"1:6: invalid UTF-8 encoding\"),\n\t),\n\terrCase(\n\t\t\"echo $((a |\\x80\",\n\t\tlangErr(\"1:12: invalid UTF-8 encoding\"),\n\t),\n\terrCase(\n\t\t\"!\",\n\t\tlangErr(\"1:1: `!` cannot form a statement alone\"),\n\t),\n\terrCase(\n\t\t\"! !\",\n\t\tlangErr(\"1:1: cannot negate a command multiple times\"),\n\t\tflipConfirm(LangBash), // bash allows lone `!`, unlike dash, mksh, and us.\n\t),\n\terrCase(\n\t\t\"! ! foo\",\n\t\tlangErr(\"1:1: cannot negate a command multiple times\"),\n\t\tflipConfirm(LangBash|LangMirBSDKorn), // bash allows lone `!`, unlike dash, mksh, and us.\n\t),\n\terrCase(\n\t\t\"}\",\n\t\tlangErr(\"1:1: `}` can only be used to close a block\"),\n\t),\n\terrCase(\n\t\t\"foo | }\",\n\t\tlangErr(\"1:7: `}` can only be used to close a block\"),\n\t),\n\terrCase(\n\t\t\"foo }\",\n\t\tlangErr(\"1:5: `}` can only be used to close a block\", LangZsh),\n\t),\n\terrCase(\n\t\t\"then\",\n\t\tlangErr(\"1:1: `then` can only be used in an `if`\"),\n\t),\n\terrCase(\n\t\t\"elif\",\n\t\tlangErr(\"1:1: `elif` can only be used in an `if`\"),\n\t),\n\terrCase(\n\t\t\"fi\",\n\t\tlangErr(\"1:1: `fi` can only be used to end an `if`\"),\n\t),\n\terrCase(\n\t\t\"do\",\n\t\tlangErr(\"1:1: `do` can only be used in a loop\"),\n\t),\n\terrCase(\n\t\t\"done\",\n\t\tlangErr(\"1:1: `done` can only be used to end a loop\"),\n\t),\n\terrCase(\n\t\t\"esac\",\n\t\tlangErr(\"1:1: `esac` can only be used to end a `case`\"),\n\t),\n\terrCase(\n\t\t\"a=b { foo; }\",\n\t\tlangErr(\"1:12: `}` can only be used to close a block\"),\n\t),\n\terrCase(\n\t\t\"a=b foo() { bar; }\",\n\t\tlangErr(\"1:8: a command can only contain words and redirects; encountered `(`\"),\n\t),\n\terrCase(\n\t\t\"a=b if foo; then bar; fi\",\n\t\tlangErr(\"1:13: `then` can only be used in an `if`\"),\n\t),\n\terrCase(\n\t\t\">f { foo; }\",\n\t\tlangErr(\"1:1: redirects before compound commands are a zsh feature; tried parsing as LANG\"),\n\t\tlangErr(\"\", LangZsh),\n\t),\n\terrCase(\n\t\t\">f foo() { bar; }\",\n\t\tlangErr(\"1:1: redirects before compound commands are a zsh feature; tried parsing as LANG\"),\n\t\tlangErr(\"\", LangZsh),\n\t),\n\terrCase(\n\t\t\">f if foo; then bar; fi\",\n\t\tlangErr(\"1:1: redirects before compound commands are a zsh feature; tried parsing as LANG\"),\n\t\tlangErr(\"\", LangZsh),\n\t),\n\terrCase(\n\t\t\"if done; then b; fi\",\n\t\tlangErr(\"1:4: `done` can only be used to end a loop\"),\n\t),\n\terrCase(\n\t\t\"'\",\n\t\tlangErr(\"1:1: reached EOF without closing quote `'`\"),\n\t),\n\terrCase(\n\t\t`\"`,\n\t\tlangErr(\"1:1: reached EOF without closing quote `\\\"`\"),\n\t),\n\terrCase(\n\t\t`'\\''`,\n\t\tlangErr(\"1:4: reached EOF without closing quote `'`\"),\n\t),\n\terrCase(\n\t\t\";\",\n\t\tlangErr(\"1:1: `;` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"{ ; }\",\n\t\tlangErr(\"1:1: `{` must be followed by a statement list\"),\n\t\tlangErr(\"\", LangZsh|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t`\"foo\"(){ :; }`,\n\t\tlangErr(\"1:1: invalid func name\"),\n\t\tflipConfirm(LangMirBSDKorn), // TODO: support non-literal func names\n\t),\n\terrCase(\n\t\t`foo$bar(){ :; }`,\n\t\tlangErr(\"1:1: invalid func name\"),\n\t),\n\terrCase(\n\t\t\"{\",\n\t\tlangErr(\"1:1: `{` must be followed by a statement list\"),\n\t\tlangErr(\"1:1: reached EOF without matching `{` with `}`\", LangZsh|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"{ foo;\",\n\t\tlangErr(\"1:1: reached EOF without matching `{` with `}`\"),\n\t),\n\terrCase(\n\t\t\"{ foo; #}\",\n\t\tlangErr(\"1:1: reached EOF without matching `{` with `}`\"),\n\t),\n\terrCase(\n\t\t\"(x\",\n\t\tlangErr(\"1:1: reached EOF without matching `(` with `)`\"),\n\t),\n\terrCase(\n\t\t\")\",\n\t\tlangErr(\"1:1: `)` can only be used to close a subshell\"),\n\t),\n\terrCase(\n\t\t\"`\",\n\t\tlangErr(\"1:1: reached EOF without closing quote \\\"`\\\"\"),\n\t),\n\terrCase(\n\t\t\";;\",\n\t\tlangErr(\"1:1: `;;` can only be used in a case clause\"),\n\t),\n\terrCase(\n\t\t\"( foo;\",\n\t\tlangErr(\"1:1: reached EOF without matching `(` with `)`\"),\n\t),\n\terrCase(\n\t\t\"&\",\n\t\tlangErr(\"1:1: `&` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"|\",\n\t\tlangErr(\"1:1: `|` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"&&\",\n\t\tlangErr(\"1:1: `&&` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"||\",\n\t\tlangErr(\"1:1: `||` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"foo; || bar\",\n\t\tlangErr(\"1:6: `||` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"echo & || bar\",\n\t\tlangErr(\"1:8: `||` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"echo & ; bar\",\n\t\tlangErr(\"1:8: `;` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"foo;;\",\n\t\tlangErr(\"1:4: `;;` can only be used in a case clause\"),\n\t),\n\terrCase(\n\t\t\"foo(\",\n\t\tlangErr(\"1:1: `foo(` must be followed by `)`\", LangPOSIX|LangBash|LangMirBSDKorn|LangBats),\n\t\tlangErr(\"1:4: reached EOF without matching `(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"foo(bar\",\n\t\tlangErr(\"1:1: `foo(` must be followed by `)`\", LangPOSIX|LangBash|LangMirBSDKorn|LangBats),\n\t\tlangErr(\"1:4: reached EOF without matching `(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"à(\",\n\t\tlangErr(\"1:1: `foo(` must be followed by `)`\", LangPOSIX|LangBash|LangMirBSDKorn|LangBats),\n\t\tlangErr(\"1:3: reached EOF without matching `(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"foo'\",\n\t\tlangErr(\"1:4: reached EOF without closing quote `'`\"),\n\t),\n\terrCase(\n\t\t`foo\"`,\n\t\tlangErr(\"1:4: reached EOF without closing quote `\\\"`\"),\n\t),\n\terrCase(\n\t\t`\"foo`,\n\t\tlangErr(\"1:1: reached EOF without closing quote `\\\"`\"),\n\t),\n\terrCase(\n\t\t`\"foobar\\`,\n\t\tlangErr(\"1:1: reached EOF without closing quote `\\\"`\"),\n\t),\n\terrCase(\n\t\t`\"foo\\a`,\n\t\tlangErr(\"1:1: reached EOF without closing quote `\\\"`\"),\n\t),\n\terrCase(\n\t\t\"foo()\",\n\t\tlangErr(\"1:1: `foo()` must be followed by a statement\"),\n\t\tflipConfirm(LangMirBSDKorn), // TODO: some variants allow a missing body\n\t),\n\terrCase(\n\t\t\"foo() {\",\n\t\tlangErr(\"1:7: `{` must be followed by a statement list\"),\n\t\tlangErr(\"1:7: reached EOF without matching `{` with `}`\", LangZsh|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"foo() { bar;\",\n\t\tlangErr(\"1:7: reached EOF without matching `{` with `}`\"),\n\t),\n\terrCase(\n\t\t\"foo-bar() { x; }\",\n\t\tlangErr(\"1:1: invalid func name\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"foò() { x; }\",\n\t\tlangErr(\"1:1: invalid func name\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"echo foo(\",\n\t\tlangErr(\"1:9: a command can only contain words and redirects; encountered `(`\", LangPOSIX|LangBash|LangMirBSDKorn|LangBats),\n\t\tlangErr(\"1:9: reached EOF without matching `(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo &&\",\n\t\tlangErr(\"1:6: `&&` must be followed by a statement\"),\n\t),\n\terrCase(\n\t\t\"echo |\",\n\t\tlangErr(\"1:6: `|` must be followed by a statement\"),\n\t),\n\terrCase(\n\t\t\"echo ||\",\n\t\tlangErr(\"1:6: `||` must be followed by a statement\"),\n\t),\n\terrCase(\n\t\t\"echo | #bar\",\n\t\tlangErr(\"1:6: `|` must be followed by a statement\"),\n\t),\n\terrCase(\n\t\t\"echo && #bar\",\n\t\tlangErr(\"1:6: `&&` must be followed by a statement\"),\n\t),\n\terrCase(\n\t\t\"`echo &&`\",\n\t\tlangErr(\"1:7: `&&` must be followed by a statement\"),\n\t),\n\terrCase(\n\t\t\"`echo |`\",\n\t\tlangErr(\"1:7: `|` must be followed by a statement\"),\n\t),\n\terrCase(\n\t\t\"echo | ! true\",\n\t\tlangErr(\"1:8: `!` can only be used in full statements\"),\n\t),\n\terrCase(\n\t\t\"echo >\",\n\t\tlangErr(\"1:6: `>` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo >>\",\n\t\tlangErr(\"1:6: `>>` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo <\",\n\t\tlangErr(\"1:6: `<` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo 2>\",\n\t\tlangErr(\"1:7: `>` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo <\\nbar\",\n\t\tlangErr(\"1:6: `<` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo | < #bar\",\n\t\tlangErr(\"1:8: `<` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo && > #\",\n\t\tlangErr(\"1:9: `>` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"<<\",\n\t\tlangErr(\"1:1: `<<` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"<<EOF\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<EOF\\n\\\\\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<EOF\\n\\\\\\n\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<EOF\\n\\\\\\nEOF\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmAll, // why does mksh allow this?\n\t),\n\terrCase(\n\t\t\"<<EOF\\nfoo\\\\\\nEOF\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<'EOF'\\n\\\\\\n\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<EOF <`\\n#\\n`\\n``\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t),\n\terrCase(\n\t\t\"<<'EOF'\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<\\\\EOF\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<\\\\\\\\EOF\",\n\t\tlangErr(\"1:1: unclosed here-document `\\\\EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<-EOF\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<-EOF\\n\\t\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<-'EOF'\\n\\t\",\n\t\tlangErr(\"1:1: unclosed here-document `EOF`\"),\n\t\tflipConfirmUnclosedHeredoc,\n\t),\n\terrCase(\n\t\t\"<<\\nEOF\\nbar\\nEOF\",\n\t\tlangErr(\"1:1: `<<` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"$(<<EOF\\nNOTEOF)\",\n\t\tlangErr(\"1:3: unclosed here-document `EOF`\", LangBash|LangMirBSDKorn),\n\t\t// Note that this fails on external shells as they treat \")\" as part of the heredoc.\n\t),\n\terrCase(\n\t\t\"`<<EOF\\nNOTEOF`\",\n\t\tlangErr(\"1:2: unclosed here-document `EOF`\", LangBash|LangMirBSDKorn),\n\t\tflipConfirmAll,\n\t\t// Note that this works on external shells as they treat \"`\" as outside the heredoc.\n\t),\n\terrCase(\n\t\t\"if\",\n\t\tlangErr(\"1:1: `if` must be followed by a statement list\"),\n\t\tlangErr(\"1:1: `if <cond>` must be followed by `then`\", LangZsh|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"if true;\",\n\t\tlangErr(\"1:1: `if <cond>` must be followed by `then`\"),\n\t),\n\terrCase(\n\t\t\"if true then\",\n\t\tlangErr(\"1:1: `if <cond>` must be followed by `then`\"),\n\t),\n\terrCase(\n\t\t\"if true; then bar;\",\n\t\tlangErr(\"1:1: `if` statement must end with `fi`\"),\n\t),\n\terrCase(\n\t\t\"if true; then bar; fi#etc\",\n\t\tlangErr(\"1:1: `if` statement must end with `fi`\"),\n\t),\n\terrCase(\n\t\t\"if a; then b; elif c;\",\n\t\tlangErr(\"1:15: `elif <cond>` must be followed by `then`\"),\n\t),\n\terrCase(\n\t\t\"'foo' '\",\n\t\tlangErr(\"1:7: reached EOF without closing quote `'`\"),\n\t),\n\terrCase(\n\t\t\"'foo\\n' '\",\n\t\tlangErr(\"2:3: reached EOF without closing quote `'`\"),\n\t),\n\terrCase(\n\t\t\"while\",\n\t\tlangErr(\"1:1: `while` must be followed by a statement list\"),\n\t\tlangErr(\"1:1: `while <cond>` must be followed by `do`\", LangZsh|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"while true;\",\n\t\tlangErr(\"1:1: `while <cond>` must be followed by `do`\"),\n\t),\n\terrCase(\n\t\t\"while true; do bar\",\n\t\tlangErr(\"1:1: `while` statement must end with `done`\"),\n\t),\n\terrCase(\n\t\t\"while true; do bar;\",\n\t\tlangErr(\"1:1: `while` statement must end with `done`\"),\n\t),\n\terrCase(\n\t\t\"until\",\n\t\tlangErr(\"1:1: `until` must be followed by a statement list\"),\n\t\tlangErr(\"1:1: `until <cond>` must be followed by `do`\", LangZsh|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"until true;\",\n\t\tlangErr(\"1:1: `until <cond>` must be followed by `do`\"),\n\t),\n\terrCase(\n\t\t\"until true; do bar\",\n\t\tlangErr(\"1:1: `until` statement must end with `done`\"),\n\t),\n\terrCase(\n\t\t\"until true; do bar;\",\n\t\tlangErr(\"1:1: `until` statement must end with `done`\"),\n\t),\n\terrCase(\n\t\t\"for\",\n\t\tlangErr(\"1:1: `for` must be followed by a literal\"),\n\t),\n\terrCase(\n\t\t\"for i\",\n\t\tlangErr(\"1:1: `for foo` must be followed by `in`, `do`, `;`, or a newline\"),\n\t),\n\terrCase(\n\t\t\"for i in;\",\n\t\tlangErr(\"1:1: `for foo [in words]` must be followed by `do`\"),\n\t),\n\terrCase(\n\t\t\"for i in 1 2 3;\",\n\t\tlangErr(\"1:1: `for foo [in words]` must be followed by `do`\"),\n\t),\n\terrCase(\n\t\t\"for i in 1 2 &\",\n\t\tlangErr(\"1:1: `for foo [in words]` must be followed by `do`\"),\n\t),\n\terrCase(\n\t\t\"for i in 1 2 (\",\n\t\tlangErr(\"1:14: word list can only contain words\"),\n\t\tlangErr(\"1:14: reached EOF without matching `(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"for i in 1 2 3; do echo $i;\",\n\t\tlangErr(\"1:1: `for` statement must end with `done`\"),\n\t),\n\terrCase(\n\t\t\"for i in 1 2 3; echo $i;\",\n\t\tlangErr(\"1:1: `for foo [in words]` must be followed by `do`\"),\n\t),\n\terrCase(\n\t\t\"for 'i' in 1 2 3; do echo $i; done\",\n\t\tlangErr(\"1:1: `for` must be followed by a literal\"),\n\t),\n\terrCase(\n\t\t\"for in 1 2 3; do echo $i; done\",\n\t\tlangErr(\"1:1: `for foo` must be followed by `in`, `do`, `;`, or a newline\"),\n\t),\n\terrCase(\n\t\t\"select\",\n\t\tlangErr(\"1:1: `select` must be followed by a literal\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"select i\",\n\t\tlangErr(\"1:1: `select foo` must be followed by `in`, `do`, `;`, or a newline\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"select i in;\",\n\t\tlangErr(\"1:1: `select foo [in words]` must be followed by `do`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"select i in 1 2 3;\",\n\t\tlangErr(\"1:1: `select foo [in words]` must be followed by `do`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"select i in 1 2 3; do echo $i;\",\n\t\tlangErr(\"1:1: `select` statement must end with `done`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"select i in 1 2 3; echo $i;\",\n\t\tlangErr(\"1:1: `select foo [in words]` must be followed by `do`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"select 'i' in 1 2 3; do echo $i; done\",\n\t\tlangErr(\"1:1: `select` must be followed by a literal\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"select in 1 2 3; do echo $i; done\",\n\t\tlangErr(\"1:1: `select foo` must be followed by `in`, `do`, `;`, or a newline\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo foo &\\n;\",\n\t\tlangErr(\"2:1: `;` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"echo $(foo\",\n\t\tlangErr(\"1:6: reached EOF without matching `$(` with `)`\"),\n\t),\n\terrCase(\n\t\t\"echo $((foo\",\n\t\tlangErr(\"1:6: reached EOF without matching `$((` with `))`\"),\n\t),\n\terrCase(\n\t\t`echo $((\\`,\n\t\tlangErr(\"1:6: reached EOF without matching `$((` with `))`\"),\n\t),\n\terrCase(\n\t\t`echo $((o\\`,\n\t\tlangErr(\"1:6: reached EOF without matching `$((` with `))`\"),\n\t),\n\terrCase(\n\t\t`echo $((foo\\a`,\n\t\tlangErr(\"1:6: reached EOF without matching `$((` with `))`\"),\n\t),\n\terrCase(\n\t\t`echo $(($(a\"`,\n\t\tlangErr(\"1:12: reached EOF without closing quote `\\\"`\"),\n\t),\n\terrCase(\n\t\t\"echo $((`echo 0`\",\n\t\tlangErr(\"1:6: reached EOF without matching `$((` with `))`\"),\n\t),\n\terrCase(\n\t\t`echo $((& $(`,\n\t\tlangErr(\"1:9: `&` must follow an expression\"),\n\t),\n\terrCase(\n\t\t`echo $((a'`,\n\t\tlangErr(\"1:10: reached EOF without closing quote `'`\"),\n\t),\n\terrCase(\n\t\t`echo $((a b\"`,\n\t\tlangErr(\"1:11: not a valid arithmetic operator: `b`\"),\n\t),\n\terrCase(\n\t\t\"echo $(())\",\n\t\tlangErr(\"1:6: `$((` must be followed by an expression\"),\n\t\tflipConfirmAll, // TODO: empty arithmetic expressions seem to be OK?\n\t),\n\terrCase(\n\t\t\"echo $((()))\",\n\t\tlangErr(\"1:9: `(` must be followed by an expression\"),\n\t),\n\terrCase(\n\t\t\"echo $(((3))\",\n\t\tlangErr(\"1:6: reached `)` without matching `$((` with `))`\"),\n\t),\n\terrCase(\n\t\t\"echo $((+))\",\n\t\tlangErr(\"1:9: `+` must be followed by an expression\"),\n\t),\n\terrCase(\n\t\t\"echo $((a b c))\",\n\t\tlangErr(\"1:11: not a valid arithmetic operator: `b`\"),\n\t),\n\terrCase(\n\t\t\"echo $((a ; c))\",\n\t\tlangErr(\"1:11: not a valid arithmetic operator: `;`\"),\n\t),\n\terrCase(\n\t\t\"echo $((foo) )\",\n\t\tlangErr(\"1:6: reached `)` without matching `$((` with `))`\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirmAll, // note that we don't backtrack\n\t),\n\terrCase(\n\t\t\"echo $((a *))\",\n\t\tlangErr(\"1:11: `*` must be followed by an expression\"),\n\t),\n\terrCase(\n\t\t\"echo $((++))\",\n\t\tlangErr(\"1:9: `++` must be followed by a literal\"),\n\t),\n\terrCase(\n\t\t\"echo $((a ? b))\",\n\t\tlangErr(\"1:11: ternary operator missing `:` after `?`\"),\n\t),\n\terrCase(\n\t\t\"echo $((a : b))\",\n\t\tlangErr(\"1:11: ternary operator missing `?` before `:`\"),\n\t),\n\terrCase(\n\t\t\"echo $((/\",\n\t\tlangErr(\"1:9: `/` must follow an expression\"),\n\t),\n\terrCase(\n\t\t\"echo $((:\",\n\t\tlangErr(\"1:9: ternary operator missing `?` before `:`\"),\n\t),\n\terrCase(\n\t\t\"echo $(((a)+=b))\",\n\t\tlangErr(\"1:12: `+=` must follow a name\"),\n\t\tflipConfirm(LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo $((1=2))\",\n\t\tlangErr(\"1:10: `=` must follow a name\"),\n\t),\n\terrCase(\n\t\t\"echo $(($0=2))\",\n\t\tlangErr(\"1:11: `=` must follow a name\"),\n\t\tflipConfirmAll,\n\t),\n\terrCase(\n\t\t\"echo $(($(a)=2))\",\n\t\tlangErr(\"1:13: `=` must follow a name\"),\n\t\tflipConfirmAll,\n\t),\n\t// errCase(\n\t// \t\"echo $((1'2`))\",\n\t// \t// TODO: Take a look at this again, since this no longer fails\n\t// \t// after fixing https://github.com/mvdan/sh/issues/587.\n\t// \t// Note that Bash seems to treat code inside $(()) as if it were\n\t// \t// within double quotes, yet still requires single quotes to be\n\t// \t// matched.\n\t// \t//  `1:10: not a valid arithmetic operator: ``,\n\t// ),\n\terrCase(\n\t\t\"<<EOF\\n$(()a\",\n\t\tlangErr(\"2:1: `$((` must be followed by an expression\"),\n\t),\n\terrCase(\n\t\t\"<<EOF\\n`))\",\n\t\tlangErr(\"2:2: `)` can only be used to close a subshell\"),\n\t),\n\terrCase(\n\t\t\"echo ${foo\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\"),\n\t),\n\terrCase(\n\t\t\"echo $foo ${}\",\n\t\tlangErr(\"1:13: invalid parameter name\"),\n\t),\n\terrCase(\n\t\t\"echo ${à}\",\n\t\tlangErr(\"1:8: invalid parameter name\"),\n\t),\n\terrCase(\n\t\t\"echo ${1a}\",\n\t\tlangErr(\"1:8: invalid parameter name\"),\n\t),\n\terrCase(\n\t\t\"echo ${foo-bar\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\"),\n\t),\n\terrCase(\n\t\t\"#foo\\n{ bar;\",\n\t\tlangErr(\"2:1: reached EOF without matching `{` with `}`\"),\n\t),\n\terrCase(\n\t\t`echo \"foo${bar\"`,\n\t\tlangErr(\"1:15: not a valid parameter expansion operator: `\\\"`\"),\n\t),\n\terrCase(\n\t\t\"echo ${%\",\n\t\tlangErr(\"1:6: `${%foo}` is a mksh feature; tried parsing as LANG\"),\n\t\tlangErr(\"1:9: invalid parameter name\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${+\",\n\t\tlangErr(\"1:6: `${+foo}` is a zsh feature; tried parsing as LANG\"),\n\t\tlangErr(\"1:9: invalid parameter name\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${#${\",\n\t\tlangErr(\"1:9: nested parameter expansions are a zsh feature; tried parsing as LANG\"),\n\t\tlangErr(\"1:11: invalid parameter name\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${#$(\",\n\t\tlangErr(\"1:9: nested parameter expansions are a zsh feature; tried parsing as LANG\"),\n\t\tlangErr(\"1:9: reached EOF without matching `$(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${(\",\n\t\tlangErr(\"1:6: parameter expansion flags are a zsh feature; tried parsing as LANG\"),\n\t\tlangErr(\"1:8: reached EOF without matching `(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo $$(foo)\",\n\t\tlangErr(\"1:8: a command can only contain words and redirects; encountered `(`\", LangPOSIX|LangBash|LangMirBSDKorn|LangBats),\n\t),\n\terrCase(\n\t\t\"echo ${##\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\"),\n\t),\n\terrCase(\n\t\t\"echo ${#<}\",\n\t\tlangErr(\"1:9: not a valid parameter expansion operator: `<`\"),\n\t),\n\terrCase(\n\t\t\"echo ${%<}\",\n\t\tlangErr(\"1:8: invalid parameter name\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${!<}\",\n\t\tlangErr(\"1:9: not a valid parameter expansion operator: `<`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${@foo}\",\n\t\tlangErr(\"1:9: `@` cannot be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo ${$needbraces}\",\n\t\tlangErr(\"1:9: `$` cannot be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo ${?foo}\",\n\t\tlangErr(\"1:9: `?` cannot be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"echo ${-foo}\",\n\t\tlangErr(\"1:9: `-` cannot be followed by a word\"),\n\t),\n\terrCase(\n\t\t`echo ${\"bad\"}`,\n\t\tlangErr(\"1:6: invalid nested parameter expansion\", LangZsh),\n\t),\n\terrCase(\n\t\t`echo ${\"$needbraces\"}`,\n\t\tlangErr(\"1:10: `$` cannot be followed by a word\", LangZsh),\n\t),\n\terrCase(\n\t\t`echo ${\"${foo}}`,\n\t\tlangErr(\"1:8: reached `}` without closing quote `\\\"`\", LangZsh),\n\t),\n\terrCase(\n\t\t`echo ${\"${foo}bad\"}`,\n\t\tlangErr(\"1:6: invalid nested parameter expansion\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${${nested}foo}\",\n\t\tlangErr(\"1:17: nested parameter expansion cannot be followed by a word\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${@[@]} ${@[*]}\",\n\t\tlangErr(\"1:9: cannot index a special parameter name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${${nested}[@]\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${*[@]} ${*[*]}\",\n\t\tlangErr(\"1:9: cannot index a special parameter name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${#[x]}\",\n\t\tlangErr(\"1:9: cannot index a special parameter name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${$[0]}\",\n\t\tlangErr(\"1:9: cannot index a special parameter name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${?[@]}\",\n\t\tlangErr(\"1:9: cannot index a special parameter name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${2[@]}\",\n\t\tlangErr(\"1:9: cannot index a special parameter name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${foo*}\",\n\t\tlangErr(\"1:11: not a valid parameter expansion operator: `*`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo;}\",\n\t\tlangErr(\"1:11: not a valid parameter expansion operator: `;`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo!}\",\n\t\tlangErr(\"1:11: not a valid parameter expansion operator: `!`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${#foo:-bar}\",\n\t\tlangErr(\"1:12: cannot combine multiple parameter expansion operators\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${%foo:1:3}\",\n\t\tlangErr(\"1:12: cannot combine multiple parameter expansion operators\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${#foo%x}\",\n\t\tlangErr(\"1:12: cannot combine multiple parameter expansion operators\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo foo\\n;\",\n\t\tlangErr(\"2:1: `;` can only immediately follow a statement\"),\n\t),\n\terrCase(\n\t\t\"<<$ <<0\\n$(<<$<<\",\n\t\tlangErr(\"2:6: `<<` must be followed by a word\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"(foo) bar\",\n\t\tlangErr(\"1:7: statements must be separated by &, ; or a newline\"),\n\t),\n\terrCase(\n\t\t\"{ foo; } bar\",\n\t\tlangErr(\"1:10: statements must be separated by &, ; or a newline\"),\n\t),\n\terrCase(\n\t\t\"if foo; then bar; fi bar\",\n\t\tlangErr(\"1:22: statements must be separated by &, ; or a newline\"),\n\t),\n\terrCase(\n\t\t\"case\",\n\t\tlangErr(\"1:1: `case` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t\"case i\",\n\t\tlangErr(\"1:1: `case x` must be followed by `in`\"),\n\t),\n\terrCase(\n\t\t\"case i in 3) foo;\",\n\t\tlangErr(\"1:1: `case` statement must end with `esac`\"),\n\t),\n\terrCase(\n\t\t\"case i in 3) foo; 4) bar; esac\",\n\t\tlangErr(\"1:20: a command can only contain words and redirects; encountered `)`\"),\n\t),\n\terrCase(\n\t\t\"case i in 3&) foo;\",\n\t\tlangErr(\"1:12: case patterns must be separated with `|`\"),\n\t),\n\terrCase(\n\t\t\"case $i in &) foo;\",\n\t\tlangErr(\"1:12: case patterns must consist of words\"),\n\t),\n\terrCase(\n\t\t\"case i {\",\n\t\tlangErr(\"1:1: `case i {` is a mksh feature; tried parsing as LANG\"),\n\t\tlangErr(\"1:1: `case` statement must end with `}`\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"case i { x) y ;;\",\n\t\tlangErr(\"1:1: `case` statement must end with `}`\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"\\\"`\\\"\",\n\t\tlangErr(\"1:3: reached EOF without closing quote `\\\"`\"),\n\t),\n\terrCase(\n\t\t\"`\\\"`\",\n\t\tlangErr(\"1:2: reached \\\"`\\\" without closing quote `\\\"`\"),\n\t),\n\terrCase(\n\t\t\"`\\\\```\",\n\t\tlangErr(\"1:3: reached EOF without closing quote \\\"`\\\"\"),\n\t),\n\terrCase(\n\t\t\"`{\\nfoo`\",\n\t\tlangErr(\"1:2: reached \\\"`\\\" without matching `{` with `}`\"),\n\t),\n\terrCase(\n\t\t\"echo \\\"`)`\\\"\",\n\t\tlangErr(\"1:8: `)` can only be used to close a subshell\"),\n\t\tflipConfirm(LangPOSIX), // dash bug?\n\t),\n\terrCase(\n\t\t\"<<$bar\\n$bar\",\n\t\tlangErr(\"1:3: expansions not allowed in heredoc words\"),\n\t\tflipConfirmAll, // we are stricter\n\t),\n\terrCase(\n\t\t\"<<${bar}\\n${bar}\",\n\t\tlangErr(\"1:3: expansions not allowed in heredoc words\"),\n\t\tflipConfirmAll, // we are stricter\n\t),\n\terrCase(\n\t\t\"<<$(bar)\\n$(bar)\",\n\t\tlangErr(\"1:3: expansions not allowed in heredoc words\", LangBash),\n\t\tflipConfirmAll, // we are stricter\n\t),\n\n\terrCase(\n\t\t\"<<$-\\n$-\",\n\t\tlangErr(\"1:3: expansions not allowed in heredoc words\"),\n\t\tflipConfirmAll, // we are stricter\n\t),\n\terrCase(\n\t\t\"<<`bar`\\n`bar`\",\n\t\tlangErr(\"1:3: expansions not allowed in heredoc words\"),\n\t\tflipConfirmAll, // we are stricter\n\t),\n\terrCase(\n\t\t\"<<\\\"$bar\\\"\\n$bar\",\n\t\tlangErr(\"1:4: expansions not allowed in heredoc words\"),\n\t\tflipConfirmAll, // we are stricter\n\t),\n\terrCase(\n\t\t\"<<a <<0\\n$(<<$<<\",\n\t\tlangErr(\"2:6: `<<` must be followed by a word\"),\n\t),\n\terrCase(\n\t\t`\"\"()`,\n\t\tlangErr(\"1:1: invalid func name\"),\n\t\tflipConfirm(LangMirBSDKorn), // TODO: support non-literal func names, even empty ones?\n\t),\n\terrCase(\n\t\t\"]] )\",\n\t\tlangErr(\"1:1: `]]` can only be used to close a test\"),\n\t\tlangErr(\"1:4: a command can only contain words and redirects; encountered `)`\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"((foo\",\n\t\tlangErr(\"1:1: reached EOF without matching `((` with `))`\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr(\"1:2: reached EOF without matching `(` with `)`\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"(())\",\n\t\tlangErr(\"1:1: `((` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ((foo\",\n\t\tlangErr(\"1:6: `((` can only be used to open an arithmetic cmd\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tlangErr(\"1:1: `foo(` must be followed by `)`\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"echo |&\",\n\t\tlangErr(\"1:6: `|&` must be followed by a statement\", LangBash|LangZsh),\n\t\tlangErr(\"1:6: `|` must be followed by a statement\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"|& a\",\n\t\tlangErr(\"1:1: `|&` is not a valid start for a statement\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"foo |& bar\",\n\t\tlangErr(\"1:5: `|` must be followed by a statement\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"let\",\n\t\tlangErr(\"1:1: `let` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let a+ b\",\n\t\tlangErr(\"1:6: `+` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let + a\",\n\t\tlangErr(\"1:5: `+` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let a ++\",\n\t\tlangErr(\"1:7: `++` must be followed by a literal\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let (a)++\",\n\t\tlangErr(\"1:8: `++` must follow a name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let 1++\",\n\t\tlangErr(\"1:6: `++` must follow a name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let $0++\",\n\t\tlangErr(\"1:7: `++` must follow a name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let --(a)\",\n\t\tlangErr(\"1:5: `--` must be followed by a literal\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let --$a\",\n\t\tlangErr(\"1:5: `--` must be followed by a literal\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let a+\\n\",\n\t\tlangErr(\"1:6: `+` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let ))\",\n\t\tlangErr(\"1:1: `let` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"`let !`\",\n\t\tlangErr(\"1:6: `!` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let a:b\",\n\t\tlangErr(\"1:6: ternary operator missing `?` before `:`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"let a+b=c\",\n\t\tlangErr(\"1:8: `=` must follow a name\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"`let` { foo; }\",\n\t\tlangErr(\"1:2: `let` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"$(let)\",\n\t\tlangErr(\"1:3: `let` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[\",\n\t\tlangErr(\"1:1: `[[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ ]]\",\n\t\tlangErr(\"1:1: `[[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a\",\n\t\tlangErr(\"1:1: reached EOF without matching `[[` with `]]`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a ||\",\n\t\tlangErr(\"1:6: `||` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a && &&\",\n\t\tlangErr(\"1:6: `&&` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a && ]]\",\n\t\tlangErr(\"1:6: `&&` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a ==\",\n\t\tlangErr(\"1:6: `==` must be followed by a word\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a =~\",\n\t\tlangErr(\"1:6: `=~` must be followed by a word\", LangBash|LangZsh),\n\t\tlangErr(\"1:6: regex tests are a bash/zsh feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"[[ -f a\",\n\t\tlangErr(\"1:1: reached EOF without matching `[[` with `]]`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ -n\\na ]]\",\n\t\tlangErr(\"1:4: `-n` must be followed by a word\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a -ef\\nb ]]\",\n\t\tlangErr(\"1:6: `-ef` must be followed by a word\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a ==\\nb ]]\",\n\t\tlangErr(\"1:6: `==` must be followed by a word\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a -nt b\",\n\t\tlangErr(\"1:1: reached EOF without matching `[[` with `]]`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a =~ b\",\n\t\tlangErr(\"1:1: reached EOF without matching `[[` with `]]`\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a b c ]]\",\n\t\tlangErr(\"1:6: not a valid test operator: `b`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a b$x c ]]\",\n\t\tlangErr(\"1:6: test operator words must consist of a single literal\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a & b ]]\",\n\t\tlangErr(\"1:6: not a valid test operator: `&`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ true && () ]]\",\n\t\tlangErr(\"1:12: `(` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ true && (&& ]]\",\n\t\tlangErr(\"1:12: `(` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a == ! b ]]\",\n\t\tlangErr(\"1:11: not a valid test operator: `b`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ (! ) ]]\",\n\t\tlangErr(\"1:5: `!` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ ! && ]]\",\n\t\tlangErr(\"1:4: `!` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ (-e ) ]]\",\n\t\tlangErr(\"1:5: `-e` must be followed by a word\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ (a) == b ]]\",\n\t\tlangErr(\"1:8: expected `&&`, `||` or `]]` after complex expr\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a =~ ; ]]\",\n\t\tlangErr(\"1:6: `=~` must be followed by a word\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a =~ )\",\n\t\tlangErr(\"1:6: `=~` must be followed by a word\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ a =~ ())\",\n\t\tlangErr(\"1:1: reached `)` without matching `[[` with `]]`\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"[[ >\",\n\t\tlangErr(\"1:1: `[[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"local (\",\n\t\tlangErr(\"1:7: `local` must be followed by names or assignments\", LangBash),\n\t\tlangErr(\"1:7: reached EOF without matching `(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"declare 0=${o}\",\n\t\tlangErr(\"1:9: invalid var name\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"declare ab=${o})\",\n\t\tlangErr(\"1:16: statements must be separated by &, ; or a newline\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"export ab=$0(\",\n\t\tlangErr(\"1:13: `export` must be followed by names or assignments\", LangBash),\n\t\tlangErr(\"1:13: reached EOF without matching `(` with `)`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"declare {x,y}=(1 2)\",\n\t\tlangErr(\"1:15: `declare` must be followed by names or assignments\", LangBash),\n\t),\n\terrCase(\n\t\t\"a=(<)\",\n\t\tlangErr(\"1:4: array element values must be words\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"a=([)\",\n\t\tlangErr(\"1:4: `[` must be followed by an expression\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"a=([i)\",\n\t\tlangErr(\"1:4: reached `)` without matching `[` with `]`\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"a=([i])\",\n\t\tlangErr(\"1:4: `[x]` must be followed by `=`\", LangBash|LangZsh),\n\t\tflipConfirmAll, // TODO: why is this valid?\n\t),\n\terrCase(\n\t\t\"a[i]=(y)\",\n\t\tlangErr(\"1:5: arrays cannot be nested\", LangBash),\n\t),\n\terrCase(\n\t\t\"a=([i]=(y))\",\n\t\tlangErr(\"1:7: arrays cannot be nested\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"o=([0]=#\",\n\t\tlangErr(\"1:8: array element values must be words\", LangBash|LangZsh),\n\t),\n\terrCase(\n\t\t\"a[b] ==[\",\n\t\tlangErr(\"1:1: `a[b]` must be followed by `=`\", LangBash|LangZsh),\n\t\tflipConfirmAll, // stringifies\n\t),\n\terrCase(\n\t\t\"a[b] +=c\",\n\t\tlangErr(\"1:1: `a[b]` must be followed by `=`\", LangBash|LangZsh),\n\t\tflipConfirmAll, // stringifies\n\t),\n\terrCase(\n\t\t\"a=(x y) foo\",\n\t\tlangErr(\"1:1: inline variables cannot be arrays\", LangBash|LangZsh),\n\t\tflipConfirmAll, // stringifies\n\t),\n\terrCase(\n\t\t\"a[2]=x foo\",\n\t\tlangErr(\"1:1: inline variables cannot be arrays\", LangBash|LangZsh),\n\t\tflipConfirmAll, // stringifies\n\t),\n\terrCase(\n\t\t\"function\",\n\t\tlangErr(\"1:1: `function` must be followed by a name\", LangBash|LangMirBSDKorn),\n\t\tlangErr(\"1:1: `foo()` must be followed by a statement\", LangZsh),\n\t),\n\terrCase(\n\t\t\"function foo(\",\n\t\tlangErr(\"1:1: `function foo(` must be followed by `)`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"function `function\",\n\t\tlangErr(\"1:1: `function` must be followed by a name\", LangBash|LangMirBSDKorn),\n\t\tlangErr(\"1:11: `foo()` must be followed by a statement\", LangZsh),\n\t),\n\terrCase(\n\t\t`function \"foo\"(){}`,\n\t\tlangErr(\"1:1: `function` must be followed by a name\", LangBash|LangMirBSDKorn),\n\t\tlangErr(\"1:10: invalid func name\", LangZsh),\n\t),\n\terrCase(\n\t\t\"function foo()\",\n\t\tlangErr(\"1:1: `foo()` must be followed by a statement\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"@test\",\n\t\tlangErr(\"1:1: `@test` must be followed by a description word\", LangBats),\n\t),\n\terrCase(\n\t\t\"@test 'desc'\",\n\t\tlangErr(\"1:1: `@test \\\"desc\\\"` must be followed by a statement\", LangBats),\n\t),\n\terrCase(\n\t\t\"echo <<<\",\n\t\tlangErr(\"1:6: `<<<` must be followed by a word\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"a[\",\n\t\tlangErr(\"1:2: `[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"a[b\",\n\t\tlangErr(\"1:2: reached EOF without matching `[` with `]`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"a[]\",\n\t\tlangErr(\"1:2: `[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirmAll, // is cmd\n\t),\n\terrCase(\n\t\t\"a[[\",\n\t\tlangErr(\"1:3: `[` must follow a name like a[i]\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo $((a[))\",\n\t\tlangErr(\"1:10: `[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo $((a[b))\",\n\t\tlangErr(\"1:10: reached `)` without matching `[` with `]`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo $((a[]))\",\n\t\tlangErr(\"1:10: `[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirm(LangMirBSDKorn), // wrong?\n\t),\n\terrCase(\n\t\t\"echo $((x$t[\",\n\t\tlangErr(\"1:12: `[` must follow a name like a[i]\", LangBash|LangMirBSDKorn),\n\t\tlangErr(\"1:12: `[` must be followed by an expression\", LangZsh),\n\t),\n\terrCase(\n\t\t\"a[1]\",\n\t\tlangErr(\"1:1: `a[b]` must be followed by `=`\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirmAll, // is cmd\n\t),\n\terrCase(\n\t\t\"a[i]+\",\n\t\tlangErr(\"1:1: `a[b]+` must be followed by `=`\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirmAll, // is cmd\n\t),\n\terrCase(\n\t\t\"a[1]#\",\n\t\tlangErr(\"1:1: `a[b]` must be followed by `=`\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirmAll, // is cmd\n\t),\n\terrCase(\n\t\t\"echo $[foo\",\n\t\tlangErr(\"1:6: reached EOF without matching `$[` with `]`\", LangBash),\n\t),\n\terrCase(\n\t\t\"echo $'\",\n\t\tlangErr(\"1:6: reached EOF without closing quote `'`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t`echo $\"`,\n\t\tlangErr(\"1:6: reached EOF without closing quote `\\\"`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo @(\",\n\t\tlangErr(\"1:6: reached EOF without matching `@(` with `)`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo @(a\",\n\t\tlangErr(\"1:6: reached EOF without matching `@(` with `)`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo @([abc)])\",\n\t\tlangErr(\"1:14: a command can only contain words and redirects; encountered `)`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"((@(\",\n\t\tlangErr(\"1:4: not a valid arithmetic operator: `(`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"time { foo;\",\n\t\tlangErr(\"1:6: reached EOF without matching `{` with `}`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"time ! foo\",\n\t\tlangErr(\"1:6: `!` can only be used in full statements\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirm(LangBash), // TODO: why is this valid?\n\t),\n\terrCase(\n\t\t\"coproc\",\n\t\tlangErr(\"1:1: coproc clause requires a command\", LangBash),\n\t),\n\terrCase(\n\t\t\"coproc\\n$\",\n\t\tlangErr(\"1:1: coproc clause requires a command\", LangBash),\n\t),\n\terrCase(\n\t\t\"coproc declare (\",\n\t\tlangErr(\"1:16: `declare` must be followed by names or assignments\", LangBash),\n\t),\n\terrCase(\n\t\t\"echo ${foo[1 2]}\",\n\t\tlangErr(\"1:14: not a valid arithmetic operator: `2`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${foo[}\",\n\t\tlangErr(\"1:11: `[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${foo]}\",\n\t\tlangErr(\"1:11: not a valid parameter expansion operator: `]`\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${foo[]}\",\n\t\tlangErr(\"1:11: `[` must be followed by an expression\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirm(LangMirBSDKorn), // TODO: why is this valid?\n\t),\n\terrCase(\n\t\t\"echo ${a/\\n\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${a/''\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${a-\\n\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo:\",\n\t\tlangErr(\"1:11: `:` must be followed by an expression\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"foo=force_expansion; echo ${foo:1 2}\",\n\t\tlangErr(\"1:35: not a valid arithmetic operator: `2`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo:1\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo:1:\",\n\t\tlangErr(\"1:13: `:` must be followed by an expression\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo:1:2\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangBash|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo:h\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ${foo,\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangBash),\n\t),\n\terrCase(\n\t\t\"echo ${foo@\",\n\t\tlangErr(\"1:11: @ expansion operator requires a literal\", LangBash),\n\t),\n\terrCase(\n\t\t\"foo=force_expansion; echo ${foo@}\",\n\t\tlangErr(\"1:33: @ expansion operator requires a literal\", LangBash),\n\t),\n\terrCase(\n\t\t\"echo ${foo@Q\",\n\t\tlangErr(\"1:6: reached EOF without matching `${` with `}`\", LangBash),\n\t),\n\terrCase(\n\t\t\"foo=force_expansion; echo ${foo@bar}\",\n\t\tlangErr(\"1:33: invalid @ expansion operator `bar`\", LangBash),\n\t),\n\terrCase(\n\t\t\"foo=force_expansion; echo ${foo@'Q'}\",\n\t\tlangErr(\"1:33: @ expansion operator requires a literal\", LangBash),\n\t),\n\terrCase(\n\t\t`echo $((echo a); (echo b))`,\n\t\tlangErr(\"1:14: not a valid arithmetic operator: `a`\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirmAll, // note that we don't backtrack\n\t),\n\terrCase(\n\t\t`((echo a); (echo b))`,\n\t\tlangErr(\"1:8: not a valid arithmetic operator: `a`\", LangBash|LangMirBSDKorn|LangZsh),\n\t\tflipConfirmAll, // note that we don't backtrack\n\t),\n\terrCase(\n\t\t\"for ((;;\",\n\t\tlangErr(\"1:5: reached EOF without matching `((` with `))`\", LangBash),\n\t),\n\terrCase(\n\t\t\"for ((;;0000000\",\n\t\tlangErr(\"1:5: reached EOF without matching `((` with `))`\", LangBash),\n\t),\n\terrCase(\n\t\t\"echo <(\",\n\t\tlangErr(\"1:6: `<` must be followed by a word\", LangPOSIX|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo >(\",\n\t\tlangErr(\"1:6: `>` must be followed by a word\", LangPOSIX|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo {var}>foo\",\n\t\tlangErr(\"1:6: `{varname}` redirects are a bash feature; tried parsing as LANG\", LangPOSIX|LangMirBSDKorn),\n\t\t// shells treat {var} as an argument, but we are a bit stricter\n\t\t// so that users won't think this will work like they expect in POSIX shell.\n\t\tflipConfirmAll,\n\t),\n\terrCase(\n\t\t\"echo ;&\",\n\t\tlangErr(\"1:7: `&` can only immediately follow a statement\", LangPOSIX),\n\t\tlangErr(\"1:6: `;&` can only be used in a case clause\", LangBash|LangMirBSDKorn|LangZsh),\n\t),\n\terrCase(\n\t\t\"echo ;;&\",\n\t\tlangErr(\"1:6: `;;` can only be used in a case clause\", LangPOSIX|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ;|\",\n\t\tlangErr(\"1:7: `|` can only immediately follow a statement\", LangPOSIX|LangBash),\n\t),\n\terrCase(\n\t\t\"for i in 1 2 3; { echo; }\",\n\t\tlangErr(\"1:17: for loops with braces are a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"echo !(a)\",\n\t\tlangErr(\"1:6: extended globs are a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"echo $a@(b)\",\n\t\tlangErr(\"1:8: extended globs are a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"foo=(1 2)\",\n\t\tlangErr(\"1:5: arrays are a bash/mksh/zsh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"a=$c\\n'\",\n\t\tlangErr(\"2:1: reached EOF without closing quote `'`\"),\n\t),\n\terrCase(\n\t\t\"echo ${!foo[@]}\",\n\t\tlangErr(\"1:6: `${!foo}` is a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"foo << < bar\",\n\t\tlangErr(\"1:5: `<<` must be followed by a word\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"echo ${foo,bar}\",\n\t\tlangErr(\"1:11: this expansion operator is a bash feature; tried parsing as LANG\", LangPOSIX|LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@Q}\",\n\t\tlangErr(\"1:11: this expansion operator is a bash/mksh feature; tried parsing as LANG\", LangPOSIX),\n\t),\n\terrCase(\n\t\t\"echo ${foo@a}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@u}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@A}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@E}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@K}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@k}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@L}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@P}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"echo ${foo@U}\",\n\t\tlangErr(\"1:12: this expansion operator is a bash feature; tried parsing as LANG\", LangMirBSDKorn),\n\t),\n\terrCase(\n\t\t\"foo=force_expansion; echo ${foo@#}\",\n\t\tlangErr(\"1:33: this expansion operator is a mksh feature; tried parsing as LANG\", LangBash),\n\t),\n\terrCase(\n\t\t\"`\\\"`\\\\\",\n\t\tlangErr(\"1:2: reached \\\"`\\\" without closing quote `\\\"`\"),\n\t),\n}\n\nfunc TestInputName(t *testing.T) {\n\tt.Parallel()\n\tin := \"(\"\n\twant := \"some-file.sh:1:1: `(` must be followed by a statement list\"\n\tp := NewParser()\n\t_, err := p.Parse(strings.NewReader(in), \"some-file.sh\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected error in %q: %v\", in, want)\n\t}\n\tgot := err.Error()\n\tif got != want {\n\t\tt.Fatalf(\"Error mismatch in %q\\nwant: %s\\ngot:  %s\",\n\t\t\tin, want, got)\n\t}\n}\n\nvar errBadReader = fmt.Errorf(\"write: expected error\")\n\ntype badReader struct{}\n\nfunc (b badReader) Read(p []byte) (int, error) { return 0, errBadReader }\n\nfunc TestReadErr(t *testing.T) {\n\tt.Parallel()\n\tp := NewParser()\n\t_, err := p.Parse(badReader{}, \"\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected error with bad reader\")\n\t}\n\tif err != errBadReader {\n\t\tt.Fatalf(\"Error mismatch with bad reader:\\nwant: %v\\ngot:  %v\",\n\t\t\terrBadReader, err)\n\t}\n}\n\ntype strictStringReader struct {\n\t*strings.Reader\n\tgaveEOF bool\n}\n\nfunc newStrictReader(s string) *strictStringReader {\n\treturn &strictStringReader{Reader: strings.NewReader(s)}\n}\n\nfunc (r *strictStringReader) Read(p []byte) (int, error) {\n\tn, err := r.Reader.Read(p)\n\tif err == io.EOF {\n\t\tif r.gaveEOF {\n\t\t\treturn n, fmt.Errorf(\"duplicate EOF read\")\n\t\t}\n\t\tr.gaveEOF = true\n\t}\n\treturn n, err\n}\n\nfunc TestParseStmtsSeq(t *testing.T) {\n\tt.Parallel()\n\tp := NewParser()\n\tinReader, inWriter := io.Pipe()\n\trecv := make(chan bool, 10)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tvar firstErr error\n\t\tfor _, err := range p.StmtsSeq(inReader) {\n\t\t\trecv <- true\n\t\t\tif firstErr == nil && err != nil {\n\t\t\t\tfirstErr = err\n\t\t\t}\n\t\t}\n\t\terrc <- firstErr\n\t}()\n\tio.WriteString(inWriter, \"foo\\n\")\n\t<-recv\n\tio.WriteString(inWriter, \"bar; baz\")\n\tinWriter.Close()\n\t<-recv\n\t<-recv\n\tif err := <-errc; err != nil {\n\t\tt.Fatalf(\"Expected no error: %v\", err)\n\t}\n}\n\nfunc TestParseStmtsSeqStopEarly(t *testing.T) {\n\tt.Parallel()\n\tp := NewParser()\n\tinReader, inWriter := io.Pipe()\n\tdefer inWriter.Close()\n\trecv := make(chan bool, 10)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tvar firstErr error\n\t\tfor stmt, err := range p.StmtsSeq(inReader) {\n\t\t\trecv <- true\n\t\t\tif firstErr == nil && err != nil {\n\t\t\t\tfirstErr = err\n\t\t\t}\n\t\t\tif stmt.Background {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\terrc <- firstErr\n\t}()\n\tio.WriteString(inWriter, \"a\\n\")\n\t<-recv\n\tio.WriteString(inWriter, \"b &\\n\") // stop here\n\t<-recv\n\tif err := <-errc; err != nil {\n\t\tt.Fatalf(\"Expected no error: %v\", err)\n\t}\n}\n\nfunc TestParseStmtsSeqError(t *testing.T) {\n\tt.Parallel()\n\tfor _, in := range []string{\n\t\t\"foo; )\",\n\t\t\"bar; <<EOF\",\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tp := NewParser()\n\t\t\trecv := make(chan bool, 10)\n\t\t\terrc := make(chan error, 1)\n\t\t\tgo func() {\n\t\t\t\tvar firstErr error\n\t\t\t\tfor _, err := range p.StmtsSeq(strings.NewReader(in)) {\n\t\t\t\t\trecv <- true\n\t\t\t\t\tif firstErr == nil && err != nil {\n\t\t\t\t\t\tfirstErr = err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\terrc <- firstErr\n\t\t\t}()\n\t\t\t<-recv\n\t\t\tif err := <-errc; err == nil {\n\t\t\t\tt.Fatalf(\"Expected an error in %q, but got nil\", in)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseWords(t *testing.T) {\n\tt.Parallel()\n\tp := NewParser()\n\tinReader, inWriter := io.Pipe()\n\trecv := make(chan bool, 10)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\terrc <- p.Words(inReader, func(w *Word) bool {\n\t\t\trecv <- true\n\t\t\treturn true\n\t\t})\n\t}()\n\t// TODO: Allow a single space to end parsing a word. At the moment, the\n\t// parser must read the next non-space token (the next literal or\n\t// newline, in this case) to finish parsing a word.\n\tio.WriteString(inWriter, \"foo \")\n\tio.WriteString(inWriter, \"bar\\n\")\n\t<-recv\n\tio.WriteString(inWriter, \"baz etc\")\n\tinWriter.Close()\n\t<-recv\n\t<-recv\n\t<-recv\n\tif err := <-errc; err != nil {\n\t\tt.Fatalf(\"Expected no error: %v\", err)\n\t}\n}\n\nfunc TestParseWordsStopEarly(t *testing.T) {\n\tt.Parallel()\n\tp := NewParser()\n\tr := strings.NewReader(\"a\\nb\\nc\\n\")\n\tparsed := 0\n\terr := p.Words(r, func(w *Word) bool {\n\t\tparsed++\n\t\treturn w.Lit() != \"b\"\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error: %v\", err)\n\t}\n\tif want := 2; parsed != want {\n\t\tt.Fatalf(\"wanted %d words parsed, got %d\", want, parsed)\n\t}\n}\n\nfunc TestParseWordsError(t *testing.T) {\n\tt.Parallel()\n\tin := \"foo )\"\n\tp := NewParser()\n\trecv := make(chan bool, 10)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\terrc <- p.Words(strings.NewReader(in), func(w *Word) bool {\n\t\t\trecv <- true\n\t\t\treturn true\n\t\t})\n\t}()\n\t<-recv\n\twant := \"1:5: `)` is not a valid word\"\n\tgot := fmt.Sprintf(\"%v\", <-errc)\n\tif got != want {\n\t\tt.Fatalf(\"Expected %q as an error, but got %q\", want, got)\n\t}\n}\n\nvar documentTests = []struct {\n\tin   string\n\twant []WordPart\n}{\n\t{\n\t\t\"foo\",\n\t\t[]WordPart{lit(\"foo\")},\n\t},\n\t{\n\t\t\" foo  $bar\",\n\t\t[]WordPart{\n\t\t\tlit(\" foo  \"),\n\t\t\tlitParamExp(\"bar\"),\n\t\t},\n\t},\n\t{\n\t\t\"$bar\\n\\n\",\n\t\t[]WordPart{\n\t\t\tlitParamExp(\"bar\"),\n\t\t\tlit(\"\\n\\n\"),\n\t\t},\n\t},\n}\n\nfunc TestParseDocument(t *testing.T) {\n\tt.Parallel()\n\tp := NewParser()\n\n\tfor _, tc := range documentTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tgot, err := p.Document(strings.NewReader(tc.in))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tWalk(got, sanityChecker{tb: t, src: tc.in}.visit)\n\t\t\twant := &Word{Parts: tc.want}\n\t\t\tqt.Assert(t, qt.CmpEquals(got, want, cmpOpt))\n\t\t})\n\t}\n}\n\nfunc TestParseDocumentError(t *testing.T) {\n\tt.Parallel()\n\tin := \"foo $(\"\n\tp := NewParser()\n\t_, err := p.Document(strings.NewReader(in))\n\twant := \"1:5: reached EOF without matching `$(` with `)`\"\n\tgot := fmt.Sprintf(\"%v\", err)\n\tif got != want {\n\t\tt.Fatalf(\"Expected %q as an error, but got %q\", want, got)\n\t}\n}\n\nvar arithmeticTests = []struct {\n\tin   string\n\twant ArithmExpr\n}{\n\t{\n\t\t\"foo\",\n\t\tlitWord(\"foo\"),\n\t},\n\t{\n\t\t\"3 + 4\",\n\t\t&BinaryArithm{\n\t\t\tOp: Add,\n\t\t\tX:  litWord(\"3\"),\n\t\t\tY:  litWord(\"4\"),\n\t\t},\n\t},\n\t{\n\t\t\"3 + 4 + 5\",\n\t\t&BinaryArithm{\n\t\t\tOp: Add,\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: Add,\n\t\t\t\tX:  litWord(\"3\"),\n\t\t\t\tY:  litWord(\"4\"),\n\t\t\t},\n\t\t\tY: litWord(\"5\"),\n\t\t},\n\t},\n\t{\n\t\t\"1 ? 0 : 2\",\n\t\t&BinaryArithm{\n\t\t\tOp: TernQuest,\n\t\t\tX:  litWord(\"1\"),\n\t\t\tY: &BinaryArithm{\n\t\t\t\tOp: TernColon,\n\t\t\t\tX:  litWord(\"0\"),\n\t\t\t\tY:  litWord(\"2\"),\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\t\"a = 3, ++a, a--\",\n\t\t&BinaryArithm{\n\t\t\tOp: Comma,\n\t\t\tX: &BinaryArithm{\n\t\t\t\tOp: Comma,\n\t\t\t\tX: &BinaryArithm{\n\t\t\t\t\tOp: Assgn,\n\t\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\t\tY:  litWord(\"3\"),\n\t\t\t\t},\n\t\t\t\tY: &UnaryArithm{\n\t\t\t\t\tOp: Inc,\n\t\t\t\t\tX:  litWord(\"a\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tY: &UnaryArithm{\n\t\t\t\tOp:   Dec,\n\t\t\t\tPost: true,\n\t\t\t\tX:    litWord(\"a\"),\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc TestParseArithmetic(t *testing.T) {\n\tt.Parallel()\n\tp := NewParser()\n\n\tfor _, tc := range arithmeticTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tgot, err := p.Arithmetic(strings.NewReader(tc.in))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tWalk(got, sanityChecker{tb: t, src: tc.in}.visit)\n\t\t\tqt.Assert(t, qt.CmpEquals(got, tc.want, cmpOpt))\n\t\t})\n\t}\n}\n\nfunc TestParseArithmeticError(t *testing.T) {\n\tt.Parallel()\n\tin := \"3 +\"\n\tp := NewParser()\n\t_, err := p.Arithmetic(strings.NewReader(in))\n\twant := \"1:3: `+` must be followed by an expression\"\n\tgot := fmt.Sprintf(\"%v\", err)\n\tif got != want {\n\t\tt.Fatalf(\"Expected %q as an error, but got %q\", want, got)\n\t}\n}\n\nvar stopAtTests = []struct {\n\tin   string\n\tstop string\n\twant any\n}{\n\t{\n\t\t\"foo bar\", \"$$\",\n\t\tlitCall(\"foo\", \"bar\"),\n\t},\n\t{\n\t\t\"$foo $\", \"$$\",\n\t\tcall(word(litParamExp(\"foo\")), litWord(\"$\")),\n\t},\n\t{\n\t\t\"echo foo $$\", \"$$\",\n\t\tlitCall(\"echo\", \"foo\"),\n\t},\n\t{\n\t\t\"$$\", \"$$\",\n\t\t&File{},\n\t},\n\t{\n\t\t\"echo foo\\n$$\\n\", \"$$\",\n\t\tlitCall(\"echo\", \"foo\"),\n\t},\n\t{\n\t\t\"echo foo; $$\", \"$$\",\n\t\tlitCall(\"echo\", \"foo\"),\n\t},\n\t{\n\t\t\"echo foo; $$\", \"$$\",\n\t\tlitCall(\"echo\", \"foo\"),\n\t},\n\t{\n\t\t\"echo foo;$$\", \"$$\",\n\t\tlitCall(\"echo\", \"foo\"),\n\t},\n\t{\n\t\t\"echo '$$'\", \"$$\",\n\t\tcall(litWord(\"echo\"), word(sglQuoted(\"$$\"))),\n\t},\n}\n\nfunc TestParseStopAt(t *testing.T) {\n\tt.Parallel()\n\tfor _, c := range stopAtTests {\n\t\tp := NewParser(StopAt(c.stop))\n\t\twant := fullProg(c.want)\n\t\tt.Run(\"\", singleParse(p, c.in, want))\n\t}\n}\n\nfunc TestValidName(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname string\n\t\tin   string\n\t\twant bool\n\t}{\n\t\t{\"Empty\", \"\", false},\n\t\t{\"Simple\", \"foo\", true},\n\t\t{\"MixedCase\", \"Foo\", true},\n\t\t{\"Underscore\", \"_foo\", true},\n\t\t{\"NumberPrefix\", \"3foo\", false},\n\t\t{\"NumberSuffix\", \"foo3\", true},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := ValidName(tc.in)\n\t\t\tif got != tc.want {\n\t\t\t\tt.Fatalf(\"ValidName(%q) got %t, wanted %t\",\n\t\t\t\t\ttc.in, got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsIncomplete(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tin       string\n\t\tnotWords bool\n\t\twant     bool\n\t}{\n\t\t{in: \"foo\\n\", want: false},\n\t\t{in: \"foo;\", want: false},\n\t\t{in: \"\\n\", want: false},\n\t\t{in: \"badsyntax)\", want: false},\n\t\t{in: \"foo 'incomp\", want: true},\n\t\t{in: `foo \"incomp`, want: true},\n\t\t{in: \"foo ${incomp\", want: true},\n\n\t\t{in: \"foo; 'incomp\", notWords: true, want: true},\n\t\t{in: `foo; \"incomp`, notWords: true, want: true},\n\t\t{in: \" (incomp\", notWords: true, want: true},\n\t}\n\tp := NewParser()\n\tfor i, tc := range tests {\n\t\tt.Run(fmt.Sprintf(\"Parse%02d\", i), func(t *testing.T) {\n\t\t\tr := strings.NewReader(tc.in)\n\t\t\t_, err := p.Parse(r, \"\")\n\t\t\tif got := IsIncomplete(err); got != tc.want {\n\t\t\t\tt.Fatalf(\"%q got %t, wanted %t\", tc.in, got, tc.want)\n\t\t\t}\n\t\t})\n\t\tt.Run(fmt.Sprintf(\"Interactive%02d\", i), func(t *testing.T) {\n\t\t\tr := strings.NewReader(tc.in)\n\t\t\tvar firstErr error\n\t\t\tfor _, err := range p.InteractiveSeq(r) {\n\t\t\t\tif firstErr == nil && err != nil {\n\t\t\t\t\tfirstErr = err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif got := IsIncomplete(firstErr); got != tc.want {\n\t\t\t\tt.Fatalf(\"%q got %t, wanted %t\", tc.in, got, tc.want)\n\t\t\t}\n\t\t})\n\t\tif !tc.notWords {\n\t\t\tt.Run(fmt.Sprintf(\"WordsSeq%02d\", i), func(t *testing.T) {\n\t\t\t\tr := strings.NewReader(tc.in)\n\t\t\t\tvar firstErr error\n\t\t\t\tfor _, err := range p.WordsSeq(r) {\n\t\t\t\t\tif firstErr == nil && err != nil {\n\t\t\t\t\t\tfirstErr = err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif got := IsIncomplete(firstErr); got != tc.want {\n\t\t\t\t\tt.Fatalf(\"%q got %t, wanted %t\", tc.in, got, tc.want)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestPosEdgeCases(t *testing.T) {\n\tin := \"`\\\\\\\\foo`\\n\" + // one escaped backslash and 3 bytes\n\t\t\"\\x00foo\\x00bar\\n\" // 8 bytes and newline\n\tp := NewParser()\n\tf, err := p.Parse(strings.NewReader(in), \"\")\n\tqt.Assert(t, qt.IsNil(err))\n\tcmdSubst := f.Stmts[0].Cmd.(*CallExpr).Args[0].Parts[0].(*CmdSubst)\n\tlit := cmdSubst.Stmts[0].Cmd.(*CallExpr).Args[0].Parts[0].(*Lit)\n\n\tqt.Check(t, qt.Equals(lit.Value, lit.Value))\n\t// Note that positions of literals with escape sequences inside backquote command substitutions\n\t// are weird, since we effectively skip over the double escaping in the literal value and positions.\n\t// Even though the input source has '\\\\foo' between columns 2 and 7 (length 5)\n\t// we end up keeping '\\foo' between columns 3 and 7 (length 4).\n\tqt.Check(t, qt.Equals(lit.ValuePos.String(), \"1:3\"))\n\tqt.Check(t, qt.Equals(lit.ValueEnd.String(), \"1:7\"))\n\n\t// Check that we skip over null bytes when counting columns.\n\tqt.Check(t, qt.Equals(f.Stmts[1].Pos().String(), \"2:2\"))\n\tqt.Check(t, qt.Equals(f.Stmts[1].End().String(), \"2:9\"))\n}\n\nfunc TestParseRecoverErrors(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tsrc string\n\n\t\twantErr     bool\n\t\twantMissing int\n\t}{\n\t\t{src: \"foo;\"},\n\t\t{src: \"foo\"},\n\t\t{\n\t\t\tsrc:         \"'incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"foo; 'incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"{ incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"(incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"(incomp; foo\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"$(incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"((incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"$((incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"if foo\",\n\t\t\twantMissing: 3,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"if foo; then bar\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"for i in 1 2 3; echo $i; done\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         `\"incomp`,\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"`incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"incomp >\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"${incomp\",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"incomp | \",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"incomp || \",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         \"incomp && \",\n\t\t\twantMissing: 1,\n\t\t},\n\t\t{\n\t\t\tsrc:         `(one | { two >`,\n\t\t\twantMissing: 3,\n\t\t},\n\t\t{\n\t\t\tsrc:         `(one > ; two | ); { three`,\n\t\t\twantMissing: 3,\n\t\t},\n\t\t{\n\t\t\tsrc:     \"badsyntax)\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tparser := NewParser(RecoverErrors(3))\n\tprinter := NewPrinter()\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tt.Logf(\"input: %s\", tc.src)\n\t\t\tr := strings.NewReader(tc.src)\n\t\t\tf, err := parser.Parse(r, \"\")\n\t\t\tif tc.wantErr {\n\t\t\t\tqt.Assert(t, qt.Not(qt.IsNil(err)))\n\t\t\t} else {\n\t\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\t\tswitch len(f.Stmts) {\n\t\t\t\tcase 0:\n\t\t\t\t\tt.Fatalf(\"result has no statements\")\n\t\t\t\tcase 1:\n\t\t\t\t\tif f.Stmts[0].Pos().IsRecovered() {\n\t\t\t\t\t\tt.Fatalf(\"result is only a recovered statement\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tqt.Assert(t, qt.Equals(countRecoveredPositions(reflect.ValueOf(f)), tc.wantMissing))\n\n\t\t\t// Check that walking or printing the syntax tree still appears to work\n\t\t\t// even when the input source was incomplete.\n\t\t\tWalk(f, func(node Node) bool {\n\t\t\t\tif node == nil {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\t// Each position should either be valid, pointing to an offset within the input,\n\t\t\t\t// or invalid, which could be due to the position being recovered.\n\t\t\t\tfor _, pos := range []Pos{node.Pos(), node.End()} {\n\t\t\t\t\tqt.Assert(t, qt.IsFalse(pos.IsValid() && pos.IsRecovered()), qt.Commentf(\"positions cannot be valid and recovered\"))\n\t\t\t\t\tif !pos.IsValid() {\n\t\t\t\t\t\tqt.Assert(t, qt.Equals(pos.Offset(), 0), qt.Commentf(\"invalid positions have no offset\"))\n\t\t\t\t\t\tqt.Assert(t, qt.Equals(pos.Line(), 0), qt.Commentf(\"invalid positions have no line\"))\n\t\t\t\t\t\tqt.Assert(t, qt.Equals(pos.Col(), 0), qt.Commentf(\"invalid positions have no column\"))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t\t// Note that we don't particularly care about good formatting here.\n\t\t\tprinter.Print(io.Discard, f)\n\t\t})\n\t}\n}\n\nfunc countRecoveredPositions(x reflect.Value) int {\n\tswitch x.Kind() {\n\tcase reflect.Interface:\n\t\treturn countRecoveredPositions(x.Elem())\n\tcase reflect.Pointer:\n\t\tif !x.IsNil() {\n\t\t\treturn countRecoveredPositions(x.Elem())\n\t\t}\n\tcase reflect.Slice:\n\t\tn := 0\n\t\tfor i := range x.Len() {\n\t\t\tn += countRecoveredPositions(x.Index(i))\n\t\t}\n\t\treturn n\n\tcase reflect.Struct:\n\t\tif pos, ok := x.Interface().(Pos); ok {\n\t\t\tif pos.IsRecovered() {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn 0\n\t\t}\n\t\tn := 0\n\t\tfor i := range x.NumField() {\n\t\t\tn += countRecoveredPositions(x.Field(i))\n\t\t}\n\t\treturn n\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "syntax/printer.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"unicode\"\n\n\t\"mvdan.cc/sh/v3/fileutil\"\n)\n\n// PrinterOption is a function which can be passed to NewPrinter\n// to alter its behavior. To apply option to existing Printer\n// call it directly, for example KeepPadding(true)(printer).\ntype PrinterOption func(*Printer)\n\n// Indent sets the number of spaces used for indentation. If set to 0,\n// tabs will be used instead.\nfunc Indent(spaces uint) PrinterOption {\n\treturn func(p *Printer) { p.indentSpaces = spaces }\n}\n\n// BinaryNextLine will make binary operators appear on the next line\n// when a binary command, such as a pipe, spans multiple lines. A\n// backslash will be used.\nfunc BinaryNextLine(enabled bool) PrinterOption {\n\treturn func(p *Printer) { p.binNextLine = enabled }\n}\n\n// SwitchCaseIndent will make switch cases be indented. As such, switch\n// case bodies will be two levels deeper than the switch itself.\nfunc SwitchCaseIndent(enabled bool) PrinterOption {\n\treturn func(p *Printer) { p.swtCaseIndent = enabled }\n}\n\n// TODO(v4): consider turning this into a \"space all operators\" option, to also\n// allow foo=( bar baz ), (( x + y )), and so on.\n\n// SpaceRedirects will put a space after most redirection operators. The\n// exceptions are '>&', '<&', '>(', and '<('.\nfunc SpaceRedirects(enabled bool) PrinterOption {\n\treturn func(p *Printer) { p.spaceRedirects = enabled }\n}\n\n// KeepPadding will keep most nodes and tokens in the same column that\n// they were in the original source. This allows the user to decide how\n// to align and pad their code with spaces.\n//\n// Note that this feature is best-effort and will only keep the\n// alignment stable, so it may need some human help the first time it is\n// run.\n//\n// Deprecated: this formatting option is flawed and buggy, and often does\n// not result in what the user wants when the code gets complex enough.\n// The next major version, v4, will remove this feature entirely.\n// See: https://github.com/mvdan/sh/issues/658\nfunc KeepPadding(enabled bool) PrinterOption {\n\treturn func(p *Printer) {\n\t\tif enabled && !p.keepPadding {\n\t\t\t// Enable the flag, and set up the writer wrapper.\n\t\t\tp.keepPadding = true\n\t\t\tp.cols.Writer = p.w.(*bufio.Writer)\n\t\t\tp.w = &p.cols\n\n\t\t} else if !enabled && p.keepPadding {\n\t\t\t// Ensure we reset the state to that of NewPrinter.\n\t\t\tp.keepPadding = false\n\t\t\tp.w = p.cols.Writer\n\t\t\tp.cols = colCounter{}\n\t\t}\n\t}\n}\n\n// Minify will print programs in a way to save the most bytes possible.\n// For example, indentation and comments are skipped, and extra\n// whitespace is avoided when possible.\nfunc Minify(enabled bool) PrinterOption {\n\treturn func(p *Printer) { p.minify = enabled }\n}\n\n// SingleLine will attempt to print programs in one line. For example, lists of\n// commands or nested blocks do not use newlines in this mode. Note that some\n// newlines must still appear, such as those following comments or around\n// here-documents.\n//\n// Print's trailing newline when given a [*File] is not affected by this option.\nfunc SingleLine(enabled bool) PrinterOption {\n\treturn func(p *Printer) { p.singleLine = enabled }\n}\n\n// FunctionNextLine will place a function's opening braces on the next line.\nfunc FunctionNextLine(enabled bool) PrinterOption {\n\treturn func(p *Printer) { p.funcNextLine = enabled }\n}\n\n// NewPrinter allocates a new Printer and applies any number of options.\nfunc NewPrinter(opts ...PrinterOption) *Printer {\n\tp := &Printer{\n\t\tw:         bufio.NewWriter(nil),\n\t\ttabWriter: new(tabwriter.Writer),\n\t}\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\treturn p\n}\n\n// Print \"pretty-prints\" the given syntax tree node to the given writer. Writes\n// to w are buffered.\n//\n// The node types supported at the moment are [*File], [*Stmt], [*Word], [*Assign], any\n// [Command] node, and any WordPart node. A trailing newline will only be printed\n// when a [*File] is used.\nfunc (p *Printer) Print(w io.Writer, node Node) error {\n\tp.reset()\n\n\tif p.minify && p.singleLine {\n\t\treturn fmt.Errorf(\"Minify and SingleLine together are not supported yet; please file an issue describing your use case: https://github.com/mvdan/sh/issues\")\n\t}\n\n\t// TODO: consider adding a raw mode to skip the tab writer, much like in\n\t// go/printer.\n\ttwmode := tabwriter.DiscardEmptyColumns | tabwriter.StripEscape\n\ttabwidth := 8\n\tif p.indentSpaces == 0 {\n\t\t// indenting with tabs\n\t\ttwmode |= tabwriter.TabIndent\n\t} else {\n\t\t// indenting with spaces\n\t\ttabwidth = int(p.indentSpaces)\n\t}\n\tp.tabWriter.Init(w, 0, tabwidth, 1, ' ', twmode)\n\tw = p.tabWriter\n\n\tp.w.Reset(w)\n\tswitch node := node.(type) {\n\tcase *File:\n\t\tp.stmtList(node.Stmts, node.Last)\n\t\tp.newline(Pos{})\n\tcase *Stmt:\n\t\tp.stmtList([]*Stmt{node}, nil)\n\tcase Command:\n\t\tp.command(node, nil)\n\tcase *Word:\n\t\tp.line = node.Pos().Line()\n\t\tp.word(node)\n\tcase WordPart:\n\t\tp.line = node.Pos().Line()\n\t\tp.wordPart(node, nil)\n\tcase *Assign:\n\t\tp.line = node.Pos().Line()\n\t\tp.assigns([]*Assign{node})\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported node type: %T\", node)\n\t}\n\tp.flushHeredocs()\n\tp.flushComments()\n\n\t// flush the writers\n\tif err := p.w.Flush(); err != nil {\n\t\treturn err\n\t}\n\tif tw, _ := w.(*tabwriter.Writer); tw != nil {\n\t\tif err := tw.Flush(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\ntype bufWriter interface {\n\tWrite([]byte) (int, error)\n\tWriteString(string) (int, error)\n\tWriteByte(byte) error\n\tReset(io.Writer)\n\tFlush() error\n}\n\ntype colCounter struct {\n\t*bufio.Writer\n\tcolumn    int\n\tlineStart bool\n}\n\nfunc (c *colCounter) addByte(b byte) {\n\tswitch b {\n\tcase '\\n':\n\t\tc.column = 0\n\t\tc.lineStart = true\n\tcase '\\t', ' ', tabwriter.Escape:\n\tdefault:\n\t\tc.lineStart = false\n\t}\n\tc.column++\n}\n\nfunc (c *colCounter) WriteByte(b byte) error {\n\tc.addByte(b)\n\treturn c.Writer.WriteByte(b)\n}\n\nfunc (c *colCounter) WriteString(s string) (int, error) {\n\tfor _, b := range []byte(s) {\n\t\tc.addByte(b)\n\t}\n\treturn c.Writer.WriteString(s)\n}\n\nfunc (c *colCounter) Reset(w io.Writer) {\n\tc.column = 1\n\tc.lineStart = true\n\tc.Writer.Reset(w)\n}\n\n// Printer holds the internal state of the printing mechanism of a\n// program.\ntype Printer struct {\n\tw         bufWriter\n\ttabWriter *tabwriter.Writer\n\tcols      colCounter // used for [KeepPadding]\n\n\tindentSpaces   uint\n\tbinNextLine    bool\n\tswtCaseIndent  bool\n\tspaceRedirects bool\n\tkeepPadding    bool\n\tminify         bool\n\tsingleLine     bool\n\tfuncNextLine   bool\n\n\twantSpace wantSpaceState // whether space is required or has been written\n\n\twantNewline bool // newline is wanted for pretty-printing; ignored by singleLine; ignored by singleLine\n\tmustNewline bool // newline is required to keep shell syntax valid\n\twroteSemi   bool // wrote ';' for the current statement\n\n\t// pendingComments are any comments in the current line or statement\n\t// that we have yet to print. This is useful because that way, we can\n\t// ensure that all comments are written immediately before a newline.\n\t// Otherwise, in some edge cases we might wrongly place words after a\n\t// comment in the same line, breaking programs.\n\tpendingComments []Comment\n\n\t// firstLine means we are still writing the first line\n\tfirstLine bool\n\t// line is the current line number\n\tline uint\n\n\t// lastLevel is the last level of indentation that was used.\n\tlastLevel uint\n\t// level is the current level of indentation.\n\tlevel uint\n\t// levelIncs records which indentation level increments actually\n\t// took place, to revert them once their section ends.\n\tlevelIncs []bool\n\n\tnestedBinary bool\n\n\t// pendingHdocs is the list of pending heredocs to write.\n\tpendingHdocs []*Redirect\n\n\t// used when printing <<- heredocs with tab indentation\n\ttabsPrinter *Printer\n}\n\nfunc (p *Printer) reset() {\n\tp.wantSpace = spaceWritten\n\tp.wantNewline, p.mustNewline = false, false\n\tp.pendingComments = p.pendingComments[:0]\n\n\t// minification uses its own newline logic\n\tp.firstLine = !p.minify\n\tp.line = 0\n\n\tp.lastLevel, p.level = 0, 0\n\tp.levelIncs = p.levelIncs[:0]\n\tp.nestedBinary = false\n\tp.pendingHdocs = p.pendingHdocs[:0]\n}\n\nfunc (p *Printer) spaces(n uint) {\n\tfor range n {\n\t\tp.w.WriteByte(' ')\n\t}\n}\n\nfunc (p *Printer) space() {\n\tp.w.WriteByte(' ')\n\tp.wantSpace = spaceWritten\n}\n\nfunc (p *Printer) spacePad(pos Pos) {\n\tif p.cols.lineStart && p.indentSpaces == 0 {\n\t\t// Never add padding at the start of a line unless we are indenting\n\t\t// with spaces, since this may result in mixing of spaces and tabs.\n\t\treturn\n\t}\n\tif p.wantSpace == spaceRequired {\n\t\tp.w.WriteByte(' ')\n\t\tp.wantSpace = spaceWritten\n\t}\n\tfor p.cols.column > 0 && p.cols.column < int(pos.Col()) {\n\t\tp.w.WriteByte(' ')\n\t}\n}\n\n// wantsNewline reports whether we want to print at least one newline before\n// printing a node at a given position. A zero position can be given to simply\n// tell if we want a newline following what's just been printed.\nfunc (p *Printer) wantsNewline(pos Pos, escapingNewline bool) bool {\n\tif p.mustNewline {\n\t\t// We must have a newline here.\n\t\treturn true\n\t}\n\tif p.singleLine && len(p.pendingComments) == 0 {\n\t\t// The newline is optional, and singleLine skips it.\n\t\t// Don't skip if there are any pending comments,\n\t\t// as that might move them further down to the wrong place.\n\t\treturn false\n\t}\n\tif escapingNewline && p.minify {\n\t\treturn false\n\t}\n\t// The newline is optional, and we want it via either wantNewline or via\n\t// the position's line.\n\treturn p.wantNewline || pos.Line() > p.line\n}\n\nfunc (p *Printer) bslashNewl() {\n\tif p.wantSpace == spaceRequired {\n\t\tp.space()\n\t}\n\tp.w.WriteString(\"\\\\\\n\")\n\tp.line++\n\tp.indent()\n}\n\nfunc (p *Printer) spacedString(s string, pos Pos) {\n\tp.spacePad(pos)\n\tp.w.WriteString(s)\n\tp.wantSpace = spaceRequired\n}\n\nfunc (p *Printer) spacedToken(s string, pos Pos) {\n\tif p.minify {\n\t\tp.w.WriteString(s)\n\t\tp.wantSpace = spaceNotRequired\n\t\treturn\n\t}\n\tp.spacePad(pos)\n\tp.w.WriteString(s)\n\tp.wantSpace = spaceRequired\n}\n\nfunc (p *Printer) semiOrNewl(s string, pos Pos) {\n\tif p.wantsNewline(Pos{}, false) {\n\t\tp.newline(pos)\n\t\tp.indent()\n\t} else {\n\t\tif !p.wroteSemi {\n\t\t\tp.w.WriteByte(';')\n\t\t}\n\t\tif !p.minify {\n\t\t\tp.space()\n\t\t}\n\t\tp.advanceLine(pos.Line())\n\t}\n\tp.w.WriteString(s)\n\tp.wantSpace = spaceRequired\n}\n\nfunc (p *Printer) writeLit(s string) {\n\t// If p.tabWriter is nil, this is the nested printer being used to print\n\t// <<- heredoc bodies, so the parent printer will add the escape bytes\n\t// later.\n\tif p.tabWriter != nil && strings.Contains(s, \"\\t\") {\n\t\tp.w.WriteByte(tabwriter.Escape)\n\t\tdefer p.w.WriteByte(tabwriter.Escape)\n\t}\n\tp.w.WriteString(s)\n}\n\nfunc (p *Printer) incLevel() {\n\tinc := false\n\tif p.level <= p.lastLevel || len(p.levelIncs) == 0 {\n\t\tp.level++\n\t\tinc = true\n\t} else if last := &p.levelIncs[len(p.levelIncs)-1]; *last {\n\t\t*last = false\n\t\tinc = true\n\t}\n\tp.levelIncs = append(p.levelIncs, inc)\n}\n\nfunc (p *Printer) decLevel() {\n\tif p.levelIncs[len(p.levelIncs)-1] {\n\t\tp.level--\n\t}\n\tp.levelIncs = p.levelIncs[:len(p.levelIncs)-1]\n}\n\nfunc (p *Printer) indent() {\n\tif p.minify {\n\t\treturn\n\t}\n\tp.lastLevel = p.level\n\tswitch {\n\tcase p.level == 0:\n\tcase p.indentSpaces == 0:\n\t\tp.w.WriteByte(tabwriter.Escape)\n\t\tfor i := uint(0); i < p.level; i++ {\n\t\t\tp.w.WriteByte('\\t')\n\t\t}\n\t\tp.w.WriteByte(tabwriter.Escape)\n\tdefault:\n\t\tp.spaces(p.indentSpaces * p.level)\n\t}\n}\n\n// TODO(mvdan): add an indent call at the end of newline?\n\n// newline prints one newline and advances p.line to pos.Line().\nfunc (p *Printer) newline(pos Pos) {\n\tp.flushHeredocs()\n\tp.flushComments()\n\tp.w.WriteByte('\\n')\n\tp.wantSpace = spaceWritten\n\tp.wantNewline, p.mustNewline = false, false\n\tp.advanceLine(pos.Line())\n}\n\nfunc (p *Printer) advanceLine(line uint) {\n\tif p.line < line {\n\t\tp.line = line\n\t}\n}\n\nfunc (p *Printer) flushHeredocs() {\n\tif len(p.pendingHdocs) == 0 {\n\t\treturn\n\t}\n\thdocs := p.pendingHdocs\n\tp.pendingHdocs = p.pendingHdocs[:0]\n\tcoms := p.pendingComments\n\tp.pendingComments = nil\n\tif len(coms) > 0 {\n\t\tc := coms[0]\n\t\tif c.Pos().Line() == p.line {\n\t\t\tp.pendingComments = append(p.pendingComments, c)\n\t\t\tp.flushComments()\n\t\t\tcoms = coms[1:]\n\t\t}\n\t}\n\n\t// Reuse the last indentation level, as\n\t// indentation levels are usually changed before\n\t// newlines are printed along with their\n\t// subsequent indentation characters.\n\tnewLevel := p.level\n\tp.level = p.lastLevel\n\n\tfor _, r := range hdocs {\n\t\tp.line++\n\t\tp.w.WriteByte('\\n')\n\t\tp.wantSpace = spaceWritten\n\t\tp.wantNewline, p.wantNewline = false, false\n\t\tif r.Op == DashHdoc && p.indentSpaces == 0 && !p.minify {\n\t\t\tif r.Hdoc != nil {\n\t\t\t\textra := extraIndenter{\n\t\t\t\t\tbufWriter:   p.w,\n\t\t\t\t\tbaseIndent:  int(p.level + 1),\n\t\t\t\t\tfirstIndent: -1,\n\t\t\t\t}\n\t\t\t\tp.tabsPrinter = &Printer{\n\t\t\t\t\tw: &extra,\n\n\t\t\t\t\t// The options need to persist.\n\t\t\t\t\tindentSpaces:   p.indentSpaces,\n\t\t\t\t\tbinNextLine:    p.binNextLine,\n\t\t\t\t\tswtCaseIndent:  p.swtCaseIndent,\n\t\t\t\t\tspaceRedirects: p.spaceRedirects,\n\t\t\t\t\tkeepPadding:    p.keepPadding,\n\t\t\t\t\tminify:         p.minify,\n\t\t\t\t\tfuncNextLine:   p.funcNextLine,\n\n\t\t\t\t\tline: r.Hdoc.Pos().Line(),\n\t\t\t\t}\n\t\t\t\tp.tabsPrinter.wordParts(r.Hdoc.Parts, true)\n\t\t\t}\n\t\t\tp.indent()\n\t\t} else if r.Hdoc != nil {\n\t\t\tp.wordParts(r.Hdoc.Parts, true)\n\t\t}\n\t\tp.unquotedWord(r.Word)\n\t\tif r.Hdoc != nil {\n\t\t\t// Overwrite p.line, since printing r.Word again can set\n\t\t\t// p.line to the beginning of the heredoc again.\n\t\t\tp.advanceLine(r.Hdoc.End().Line())\n\t\t}\n\t\tp.wantSpace = spaceNotRequired\n\t}\n\tp.level = newLevel\n\tp.pendingComments = coms\n\tp.mustNewline = true\n}\n\n// newline prints between zero and two newlines.\n// If any newlines are printed, it advances p.line to pos.Line().\nfunc (p *Printer) newlines(pos Pos) {\n\tif p.firstLine && len(p.pendingComments) == 0 {\n\t\tp.firstLine = false\n\t\treturn // no empty lines at the top\n\t}\n\tif !p.wantsNewline(pos, false) {\n\t\treturn\n\t}\n\tp.flushHeredocs()\n\tp.flushComments()\n\tp.w.WriteByte('\\n')\n\tp.wantSpace = spaceWritten\n\tp.wantNewline, p.mustNewline = false, false\n\n\tl := pos.Line()\n\tif l > p.line+1 && !p.minify {\n\t\tp.w.WriteByte('\\n') // preserve single empty lines\n\t}\n\tp.advanceLine(l)\n\tp.indent()\n}\n\nfunc (p *Printer) rightParen(pos Pos) {\n\tif len(p.pendingHdocs) > 0 || !p.minify {\n\t\tp.newlines(pos)\n\t}\n\tp.w.WriteByte(')')\n\tp.wantSpace = spaceRequired\n}\n\nfunc (p *Printer) semiRsrv(s string, pos Pos) {\n\tif p.wantsNewline(pos, false) {\n\t\tp.newlines(pos)\n\t} else {\n\t\tif !p.wroteSemi {\n\t\t\tp.w.WriteByte(';')\n\t\t}\n\t\tif !p.minify {\n\t\t\tp.spacePad(pos)\n\t\t}\n\t}\n\tp.w.WriteString(s)\n\tp.wantSpace = spaceRequired\n}\n\nfunc (p *Printer) flushComments() {\n\tfor i, c := range p.pendingComments {\n\t\tif i == 0 {\n\t\t\t// Flush any pending heredocs first. Otherwise, the\n\t\t\t// comments would become part of a heredoc body.\n\t\t\tp.flushHeredocs()\n\t\t}\n\t\tp.firstLine = false\n\t\t// We can't call any of the newline methods, as they call this\n\t\t// function and we'd recurse forever.\n\t\tcline := c.Hash.Line()\n\t\tswitch {\n\t\tcase p.mustNewline, i > 0, cline > p.line && p.line > 0:\n\t\t\tp.w.WriteByte('\\n')\n\t\t\tif cline > p.line+1 {\n\t\t\t\tp.w.WriteByte('\\n')\n\t\t\t}\n\t\t\tp.indent()\n\t\t\tp.wantSpace = spaceWritten\n\t\t\tp.spacePad(c.Pos())\n\t\tcase p.wantSpace == spaceRequired:\n\t\t\tif p.keepPadding {\n\t\t\t\tp.spacePad(c.Pos())\n\t\t\t} else {\n\t\t\t\tp.w.WriteByte('\\t')\n\t\t\t}\n\t\tcase p.wantSpace != spaceWritten:\n\t\t\tp.space()\n\t\t}\n\t\t// don't go back one line, which may happen in some edge cases\n\t\tp.advanceLine(cline)\n\t\tp.w.WriteByte('#')\n\t\tp.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace))\n\t\tp.wantNewline = true\n\t\tp.mustNewline = true\n\t}\n\tp.pendingComments = nil\n}\n\nfunc (p *Printer) comments(comments ...Comment) {\n\tif p.minify {\n\t\tfor _, c := range comments {\n\t\t\tif fileutil.Shebang([]byte(\"#\"+c.Text)) != \"\" && c.Hash.Col() == 1 && c.Hash.Line() == 1 {\n\t\t\t\tp.w.WriteString(strings.TrimRightFunc(\"#\"+c.Text, unicode.IsSpace))\n\t\t\t\tp.w.WriteString(\"\\n\")\n\t\t\t\tp.line++\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\tp.pendingComments = append(p.pendingComments, comments...)\n}\n\nfunc (p *Printer) wordParts(wps []WordPart, quoted bool) {\n\t// We disallow unquoted escaped newlines between word parts below.\n\t// However, we want to allow a leading escaped newline for cases such as:\n\t//\n\t//   foo <<< \\\n\t//     \"bar baz\"\n\tif !quoted && !p.singleLine && wps[0].Pos().Line() > p.line {\n\t\tp.bslashNewl()\n\t}\n\tfor i, wp := range wps {\n\t\tvar next WordPart\n\t\tif i+1 < len(wps) {\n\t\t\tnext = wps[i+1]\n\t\t}\n\t\t// Keep escaped newlines separating word parts when quoted.\n\t\t// Note that those escaped newlines don't cause indentaiton.\n\t\t// When not quoted, we strip them out consistently,\n\t\t// because attempting to keep them would prevent indentation.\n\t\t// Can't use p.wantsNewline here, since this is only about\n\t\t// escaped newlines.\n\t\tfor quoted && !p.singleLine && wp.Pos().Line() > p.line {\n\t\t\tp.w.WriteString(\"\\\\\\n\")\n\t\t\tp.line++\n\t\t}\n\t\tp.wordPart(wp, next)\n\t\tp.advanceLine(wp.End().Line())\n\t}\n}\n\nfunc (p *Printer) wordPart(wp, next WordPart) {\n\tswitch wp := wp.(type) {\n\tcase *Lit:\n\t\tp.writeLit(wp.Value)\n\tcase *SglQuoted:\n\t\tif wp.Dollar {\n\t\t\tp.w.WriteByte('$')\n\t\t}\n\t\tp.w.WriteByte('\\'')\n\t\tp.writeLit(wp.Value)\n\t\tp.w.WriteByte('\\'')\n\t\tp.advanceLine(wp.End().Line())\n\tcase *DblQuoted:\n\t\tp.dblQuoted(wp)\n\tcase *CmdSubst:\n\t\tp.advanceLine(wp.Pos().Line())\n\t\tp.cmdSubst(wp)\n\tcase *ParamExp:\n\t\tlitCont := \";\"\n\t\tif nextLit, ok := next.(*Lit); ok && nextLit.Value != \"\" {\n\t\t\tlitCont = nextLit.Value[:1]\n\t\t}\n\t\tif p.minify && !wp.Short && wp.simple() {\n\t\t\tname := wp.Param.Value\n\t\t\tswitch {\n\t\t\tcase len(name) > 1 && !ValidName(name): // ${10}\n\t\t\tcase ValidName(name + litCont): // ${var}cont\n\t\t\tdefault:\n\t\t\t\tx2 := *wp\n\t\t\t\tx2.Short = true\n\t\t\t\tp.paramExp(&x2)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tp.paramExp(wp)\n\tcase *ArithmExp:\n\t\tp.w.WriteString(\"$((\")\n\t\tif wp.Unsigned {\n\t\t\tp.w.WriteString(\"# \")\n\t\t}\n\t\tp.arithmExpr(wp.X, false, false)\n\t\tp.w.WriteString(\"))\")\n\tcase *ExtGlob:\n\t\tp.w.WriteString(wp.Op.String())\n\t\tp.writeLit(wp.Pattern.Value)\n\t\tp.w.WriteByte(')')\n\tcase *ProcSubst:\n\t\t// avoid conflict with << and others\n\t\tif p.wantSpace == spaceRequired {\n\t\t\tp.space()\n\t\t}\n\t\tp.w.WriteString(wp.Op.String())\n\t\tp.nestedStmts(wp.Stmts, wp.Last, wp.Rparen)\n\t\tp.rightParen(wp.Rparen)\n\t}\n}\n\nfunc (p *Printer) dblQuoted(dq *DblQuoted) {\n\tif dq.Dollar {\n\t\tp.w.WriteByte('$')\n\t}\n\tp.w.WriteByte('\"')\n\tif len(dq.Parts) > 0 {\n\t\tp.wordParts(dq.Parts, true)\n\t}\n\t// Add any trailing escaped newlines.\n\tfor p.line < dq.Right.Line() {\n\t\tp.w.WriteString(\"\\\\\\n\")\n\t\tp.line++\n\t}\n\tp.w.WriteByte('\"')\n}\n\nfunc (p *Printer) wroteIndex(index ArithmExpr) bool {\n\tif index == nil {\n\t\treturn false\n\t}\n\tp.w.WriteByte('[')\n\t// Note that e.g. foo[1,3]=$bar in Zsh does not allow any spaces around the comma,\n\t// as that breaks the assignment word.\n\tp.arithmExpr(index, true, false)\n\tp.w.WriteByte(']')\n\treturn true\n}\n\nfunc (p *Printer) paramExp(pe *ParamExp) {\n\tif pe.nakedIndex() { // arr[x]\n\t\tp.writeLit(pe.Param.Value)\n\t\tp.wroteIndex(pe.Index)\n\t\treturn\n\t}\n\tp.w.WriteByte('$')\n\tif !pe.Short {\n\t\tp.w.WriteByte('{')\n\t}\n\tif pe.Flags != nil {\n\t\tp.w.WriteByte('(')\n\t\tp.writeLit(pe.Flags.Value)\n\t\tp.w.WriteByte(')')\n\t}\n\tswitch {\n\tcase pe.Length:\n\t\tp.w.WriteByte('#')\n\tcase pe.Width:\n\t\tp.w.WriteByte('%')\n\tcase pe.IsSet:\n\t\tp.w.WriteByte('+')\n\tcase pe.Excl:\n\t\tp.w.WriteByte('!')\n\t}\n\tif pe.Param != nil {\n\t\tp.writeLit(pe.Param.Value)\n\t} else {\n\t\t// Note that Zsh supports ${${nested}} but not ${$nested},\n\t\t// so we need to avoid that simplification here.\n\t\tsaved := p.minify\n\t\tp.minify = false\n\t\tp.wordPart(pe.NestedParam, nil)\n\t\tp.minify = saved\n\t}\n\tp.wroteIndex(pe.Index)\n\tswitch {\n\tcase len(pe.Modifiers) > 0:\n\t\tfor _, lit := range pe.Modifiers {\n\t\t\tp.w.WriteByte(':')\n\t\t\tp.w.WriteString(lit.Value)\n\t\t}\n\tcase pe.Slice != nil:\n\t\tp.w.WriteByte(':')\n\t\tp.arithmExpr(pe.Slice.Offset, true, true)\n\t\tif pe.Slice.Length != nil {\n\t\t\tp.w.WriteByte(':')\n\t\t\tp.arithmExpr(pe.Slice.Length, true, false)\n\t\t}\n\tcase pe.Repl != nil:\n\t\tif pe.Repl.All {\n\t\t\tp.w.WriteByte('/')\n\t\t}\n\t\tp.w.WriteByte('/')\n\t\tif pe.Repl.Orig != nil {\n\t\t\tp.word(pe.Repl.Orig)\n\t\t}\n\t\tp.w.WriteByte('/')\n\t\tif pe.Repl.With != nil {\n\t\t\tp.word(pe.Repl.With)\n\t\t}\n\tcase pe.Names != 0:\n\t\tp.writeLit(pe.Names.String())\n\tcase pe.Exp != nil:\n\t\tp.w.WriteString(pe.Exp.Op.String())\n\t\tif pe.Exp.Word != nil {\n\t\t\tp.word(pe.Exp.Word)\n\t\t}\n\t}\n\tif !pe.Short {\n\t\tp.w.WriteByte('}')\n\t}\n}\n\nfunc (p *Printer) cmdSubst(cs *CmdSubst) {\n\tswitch {\n\tcase cs.TempFile:\n\t\tp.w.WriteString(\"${\")\n\t\tp.wantSpace = spaceRequired\n\t\tp.nestedStmts(cs.Stmts, cs.Last, cs.Right)\n\t\tp.wantSpace = spaceNotRequired\n\t\tp.semiRsrv(\"}\", cs.Right)\n\tcase cs.ReplyVar:\n\t\tp.w.WriteString(\"${|\")\n\t\tp.nestedStmts(cs.Stmts, cs.Last, cs.Right)\n\t\tp.wantSpace = spaceNotRequired\n\t\tp.semiRsrv(\"}\", cs.Right)\n\t// Special case: `# inline comment`\n\tcase cs.Backquotes && len(cs.Stmts) == 0 &&\n\t\tlen(cs.Last) == 1 && cs.Right.Line() == p.line:\n\t\tp.w.WriteString(\"`#\")\n\t\tp.w.WriteString(cs.Last[0].Text)\n\t\tp.w.WriteString(\"`\")\n\tdefault:\n\t\tp.w.WriteString(\"$(\")\n\t\tif len(cs.Stmts) > 0 && startsWithLparen(cs.Stmts[0]) {\n\t\t\tp.wantSpace = spaceRequired\n\t\t} else {\n\t\t\tp.wantSpace = spaceNotRequired\n\t\t}\n\t\tp.nestedStmts(cs.Stmts, cs.Last, cs.Right)\n\t\tp.rightParen(cs.Right)\n\t}\n}\n\nfunc (p *Printer) loop(loop Loop) {\n\tswitch loop := loop.(type) {\n\tcase *WordIter:\n\t\tp.writeLit(loop.Name.Value)\n\t\tif loop.InPos.IsValid() {\n\t\t\tp.spacedString(\" in\", Pos{})\n\t\t\tp.wordJoin(loop.Items)\n\t\t}\n\tcase *CStyleLoop:\n\t\tp.w.WriteString(\"((\")\n\t\tif loop.Init == nil {\n\t\t\tp.space()\n\t\t}\n\t\tp.arithmExpr(loop.Init, false, false)\n\t\tp.w.WriteString(\"; \")\n\t\tp.arithmExpr(loop.Cond, false, false)\n\t\tp.w.WriteString(\"; \")\n\t\tp.arithmExpr(loop.Post, false, false)\n\t\tp.w.WriteString(\"))\")\n\t}\n}\n\nfunc (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) {\n\tif p.minify {\n\t\tcompact = true\n\t}\n\tp.arithmExprRecurse(expr, compact, spacePlusMinus)\n}\n\nfunc (p *Printer) arithmExprRecurse(expr ArithmExpr, compact, spacePlusMinus bool) {\n\tswitch expr := expr.(type) {\n\tcase *Word:\n\t\tp.word(expr)\n\tcase *BinaryArithm:\n\t\tif compact {\n\t\t\tp.arithmExprRecurse(expr.X, compact, spacePlusMinus)\n\t\t\tp.w.WriteString(expr.Op.String())\n\t\t\tp.arithmExprRecurse(expr.Y, compact, false)\n\t\t} else {\n\t\t\tp.arithmExprRecurse(expr.X, compact, spacePlusMinus)\n\t\t\tif expr.Op != Comma {\n\t\t\t\tp.space()\n\t\t\t}\n\t\t\tp.w.WriteString(expr.Op.String())\n\t\t\tp.space()\n\t\t\tp.arithmExprRecurse(expr.Y, compact, false)\n\t\t}\n\tcase *UnaryArithm:\n\t\tif expr.Post {\n\t\t\tp.arithmExprRecurse(expr.X, compact, spacePlusMinus)\n\t\t\tp.w.WriteString(expr.Op.String())\n\t\t} else {\n\t\t\tif spacePlusMinus {\n\t\t\t\tswitch expr.Op {\n\t\t\t\tcase Plus, Minus:\n\t\t\t\t\tp.space()\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.w.WriteString(expr.Op.String())\n\t\t\tp.arithmExprRecurse(expr.X, compact, false)\n\t\t}\n\tcase *ParenArithm:\n\t\tp.w.WriteByte('(')\n\t\tp.arithmExprRecurse(expr.X, false, false)\n\t\tp.w.WriteByte(')')\n\tcase *FlagsArithm:\n\t\tp.w.WriteByte('(')\n\t\tp.w.WriteString(expr.Flags.Value)\n\t\tp.w.WriteByte(')')\n\t\tif expr.X != nil {\n\t\t\tp.arithmExprRecurse(expr.X, compact, false)\n\t\t}\n\t}\n}\n\nfunc (p *Printer) testExpr(expr TestExpr) {\n\t// Multi-line test expressions don't need to escape newlines.\n\tif expr.Pos().Line() > p.line {\n\t\tp.newlines(expr.Pos())\n\t\tp.spacePad(expr.Pos())\n\t} else if p.wantSpace == spaceRequired {\n\t\tp.space()\n\t}\n\tp.testExprSameLine(expr)\n}\n\nfunc (p *Printer) testExprSameLine(expr TestExpr) {\n\tp.advanceLine(expr.Pos().Line())\n\tswitch expr := expr.(type) {\n\tcase *Word:\n\t\tp.word(expr)\n\tcase *BinaryTest:\n\t\tp.testExprSameLine(expr.X)\n\t\tp.space()\n\t\tp.w.WriteString(expr.Op.String())\n\t\tswitch expr.Op {\n\t\tcase AndTest, OrTest:\n\t\t\tp.wantSpace = spaceRequired\n\t\t\tp.testExpr(expr.Y)\n\t\tdefault:\n\t\t\tp.space()\n\t\t\tp.testExprSameLine(expr.Y)\n\t\t}\n\tcase *UnaryTest:\n\t\tp.w.WriteString(expr.Op.String())\n\t\tp.space()\n\t\tp.testExprSameLine(expr.X)\n\tcase *ParenTest:\n\t\tp.w.WriteByte('(')\n\t\tif startsWithLparen(expr.X) {\n\t\t\tp.wantSpace = spaceRequired\n\t\t} else {\n\t\t\tp.wantSpace = spaceNotRequired\n\t\t}\n\t\tp.testExpr(expr.X)\n\t\tp.w.WriteByte(')')\n\t}\n}\n\nfunc (p *Printer) word(w *Word) {\n\tp.wordParts(w.Parts, false)\n\tp.wantSpace = spaceRequired\n}\n\nfunc (p *Printer) unquotedWord(w *Word) {\n\tfor _, wp := range w.Parts {\n\t\tswitch wp := wp.(type) {\n\t\tcase *SglQuoted:\n\t\t\tp.writeLit(wp.Value)\n\t\tcase *DblQuoted:\n\t\t\tp.wordParts(wp.Parts, true)\n\t\tcase *Lit:\n\t\t\tfor i := 0; i < len(wp.Value); i++ {\n\t\t\t\tif b := wp.Value[i]; b == '\\\\' {\n\t\t\t\t\tif i++; i < len(wp.Value) {\n\t\t\t\t\t\tp.w.WriteByte(wp.Value[i])\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tp.w.WriteByte(b)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (p *Printer) wordJoin(ws []*Word) {\n\tanyNewline := false\n\tfor _, w := range ws {\n\t\tif pos := w.Pos(); pos.Line() > p.line && !p.singleLine {\n\t\t\tif !anyNewline {\n\t\t\t\tp.incLevel()\n\t\t\t\tanyNewline = true\n\t\t\t}\n\t\t\tp.bslashNewl()\n\t\t}\n\t\tp.spacePad(w.Pos())\n\t\tp.word(w)\n\t}\n\tif anyNewline {\n\t\tp.decLevel()\n\t}\n}\n\nfunc (p *Printer) casePatternJoin(pats []*Word) {\n\tanyNewline := false\n\tfor i, w := range pats {\n\t\t// Only valid situation for a literal 'esac' here is with a preceding left paran.\n\t\tif i == 0 && w.Lit() == \"esac\" {\n\t\t\tp.w.WriteString(\"(\")\n\t\t}\n\t\tif i > 0 {\n\t\t\tp.spacedToken(\"|\", Pos{})\n\t\t}\n\t\tif p.wantsNewline(w.Pos(), true) {\n\t\t\tif !anyNewline {\n\t\t\t\tp.incLevel()\n\t\t\t\tanyNewline = true\n\t\t\t}\n\t\t\tp.bslashNewl()\n\t\t} else {\n\t\t\tp.spacePad(w.Pos())\n\t\t}\n\t\tp.word(w)\n\t}\n\tif anyNewline {\n\t\tp.decLevel()\n\t}\n}\n\nfunc (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) {\n\tp.incLevel()\n\tfor _, el := range elems {\n\t\tvar left []Comment\n\t\tfor _, c := range el.Comments {\n\t\t\tif c.Pos().After(el.Pos()) {\n\t\t\t\tleft = append(left, c)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.comments(c)\n\t\t}\n\t\t// Multi-line array expressions don't need to escape newlines.\n\t\tif el.Pos().Line() > p.line {\n\t\t\tp.newlines(el.Pos())\n\t\t\tp.spacePad(el.Pos())\n\t\t} else if p.wantSpace == spaceRequired {\n\t\t\tp.space()\n\t\t}\n\t\tif p.wroteIndex(el.Index) {\n\t\t\tp.w.WriteByte('=')\n\t\t}\n\t\tif el.Value != nil {\n\t\t\tp.word(el.Value)\n\t\t}\n\t\tp.comments(left...)\n\t}\n\tif len(last) > 0 {\n\t\tp.comments(last...)\n\t\tp.flushComments()\n\t}\n\tp.decLevel()\n}\n\nfunc (p *Printer) stmt(s *Stmt) {\n\tp.wroteSemi = false\n\tif s.Negated {\n\t\tp.spacedString(\"!\", s.Pos())\n\t}\n\tvar startRedirs int\n\tif s.Cmd != nil {\n\t\tstartRedirs = p.command(s.Cmd, s.Redirs)\n\t}\n\tp.incLevel()\n\tfor _, r := range s.Redirs[startRedirs:] {\n\t\tif p.wantsNewline(r.OpPos, true) {\n\t\t\tp.bslashNewl()\n\t\t}\n\t\tif p.wantSpace == spaceRequired {\n\t\t\tp.spacePad(r.Pos())\n\t\t}\n\t\tif r.N != nil {\n\t\t\tp.writeLit(r.N.Value)\n\t\t}\n\t\tp.w.WriteString(r.Op.String())\n\t\tif p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) {\n\t\t\tp.space()\n\t\t} else {\n\t\t\tp.wantSpace = spaceRequired\n\t\t}\n\t\tp.word(r.Word)\n\t\tif r.Op == Hdoc || r.Op == DashHdoc {\n\t\t\tp.pendingHdocs = append(p.pendingHdocs, r)\n\t\t}\n\t}\n\tsep := s.Semicolon.IsValid() && s.Semicolon.Line() > p.line && !p.singleLine\n\tif sep || s.Background || s.Coprocess || s.Disown {\n\t\tif sep {\n\t\t\tp.bslashNewl()\n\t\t} else if !p.minify {\n\t\t\tp.space()\n\t\t}\n\t\tif s.Background {\n\t\t\tp.w.WriteString(\"&\")\n\t\t} else if s.Coprocess {\n\t\t\tp.w.WriteString(\"|&\")\n\t\t} else if s.Disown {\n\t\t\tp.w.WriteString(\"&|\")\n\t\t} else {\n\t\t\tp.w.WriteString(\";\")\n\t\t}\n\t\tp.wroteSemi = true\n\t\tp.wantSpace = spaceRequired\n\t}\n\tp.decLevel()\n}\n\nfunc (p *Printer) printRedirsUntil(redirs []*Redirect, startRedirs int, pos Pos) int {\n\tfor _, r := range redirs[startRedirs:] {\n\t\tif r.Pos().After(pos) || r.Op == Hdoc || r.Op == DashHdoc {\n\t\t\tbreak\n\t\t}\n\t\tif p.wantSpace == spaceRequired {\n\t\t\tp.spacePad(r.Pos())\n\t\t}\n\t\tif r.N != nil {\n\t\t\tp.writeLit(r.N.Value)\n\t\t}\n\t\tp.w.WriteString(r.Op.String())\n\t\tif p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) {\n\t\t\tp.space()\n\t\t} else {\n\t\t\tp.wantSpace = spaceRequired\n\t\t}\n\t\tp.word(r.Word)\n\t\tstartRedirs++\n\t}\n\treturn startRedirs\n}\n\nfunc (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {\n\tp.advanceLine(cmd.Pos().Line())\n\tp.spacePad(cmd.Pos())\n\tswitch cmd := cmd.(type) {\n\tcase *CallExpr:\n\t\tp.assigns(cmd.Assigns)\n\t\tif len(cmd.Args) > 0 {\n\t\t\tstartRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[0].Pos())\n\t\t}\n\t\tif len(cmd.Args) <= 1 {\n\t\t\tp.wordJoin(cmd.Args)\n\t\t\treturn startRedirs\n\t\t}\n\t\tp.wordJoin(cmd.Args[:1])\n\t\tstartRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[1].Pos())\n\t\tp.wordJoin(cmd.Args[1:])\n\tcase *Block:\n\t\tp.w.WriteByte('{')\n\t\t// avoid ; in an empty block\n\t\tp.wroteSemi = true\n\t\tp.wantSpace = spaceRequired\n\t\t// Forbid \"foo()\\n{ bar; }\"\n\t\tp.wantNewline = p.wantNewline || p.funcNextLine\n\t\tp.nestedStmts(cmd.Stmts, cmd.Last, cmd.Rbrace)\n\t\tp.semiRsrv(\"}\", cmd.Rbrace)\n\tcase *IfClause:\n\t\tp.ifClause(cmd, false)\n\tcase *Subshell:\n\t\tp.w.WriteByte('(')\n\t\tstmts := cmd.Stmts\n\t\tif len(stmts) > 0 && startsWithLparen(stmts[0]) {\n\t\t\tp.wantSpace = spaceRequired\n\t\t\t// Add a space between nested parentheses if we're printing them in a single line,\n\t\t\t// to avoid the ambiguity between `((` and `( (`.\n\t\t\tif (cmd.Lparen.Line() != stmts[0].Pos().Line() || len(stmts) > 1) && !p.singleLine {\n\t\t\t\tp.wantSpace = spaceNotRequired\n\n\t\t\t\tif p.minify {\n\t\t\t\t\tp.mustNewline = true\n\t\t\t\t}\n\t\t\t}\n\t\t} else if len(stmts) == 0 {\n\t\t\t// Zsh allows empty subshells, but prevent `()`\n\t\t\t// from looking like `() { anon-func; }`.\n\t\t\tp.wantSpace = spaceRequired\n\t\t} else {\n\t\t\tp.wantSpace = spaceNotRequired\n\t\t}\n\n\t\tp.spacePad(stmtsPos(cmd.Stmts, cmd.Last))\n\t\tp.nestedStmts(cmd.Stmts, cmd.Last, cmd.Rparen)\n\t\tp.wantSpace = spaceNotRequired\n\t\tp.spacePad(cmd.Rparen)\n\t\tp.rightParen(cmd.Rparen)\n\tcase *WhileClause:\n\t\tif cmd.Until {\n\t\t\tp.spacedString(\"until\", cmd.Pos())\n\t\t} else {\n\t\t\tp.spacedString(\"while\", cmd.Pos())\n\t\t}\n\t\tp.nestedStmts(cmd.Cond, cmd.CondLast, Pos{})\n\t\tp.semiOrNewl(\"do\", cmd.DoPos)\n\t\tp.nestedStmts(cmd.Do, cmd.DoLast, cmd.DonePos)\n\t\tp.semiRsrv(\"done\", cmd.DonePos)\n\tcase *ForClause:\n\t\tif cmd.Select {\n\t\t\tp.w.WriteString(\"select \")\n\t\t} else {\n\t\t\tp.w.WriteString(\"for \")\n\t\t}\n\t\tp.loop(cmd.Loop)\n\t\tp.semiOrNewl(\"do\", cmd.DoPos)\n\t\tp.nestedStmts(cmd.Do, cmd.DoLast, cmd.DonePos)\n\t\tp.semiRsrv(\"done\", cmd.DonePos)\n\tcase *BinaryCmd:\n\t\tp.stmt(cmd.X)\n\t\tif p.minify || p.singleLine || cmd.Y.Pos().Line() <= p.line {\n\t\t\t// leave p.nestedBinary untouched\n\t\t\tp.spacedToken(cmd.Op.String(), cmd.OpPos)\n\t\t\tp.advanceLine(cmd.Y.Pos().Line())\n\t\t\tp.stmt(cmd.Y)\n\t\t\tbreak\n\t\t}\n\t\tindent := !p.nestedBinary\n\t\tif indent {\n\t\t\tp.incLevel()\n\t\t}\n\t\tif p.binNextLine {\n\t\t\tif len(p.pendingHdocs) == 0 {\n\t\t\t\tp.bslashNewl()\n\t\t\t}\n\t\t\tp.spacedToken(cmd.Op.String(), cmd.OpPos)\n\t\t\tif len(cmd.Y.Comments) > 0 {\n\t\t\t\tp.wantSpace = spaceNotRequired\n\t\t\t\tp.newline(cmd.Y.Pos())\n\t\t\t\tp.indent()\n\t\t\t\tp.comments(cmd.Y.Comments...)\n\t\t\t\tp.newline(Pos{})\n\t\t\t\tp.indent()\n\t\t\t}\n\t\t} else {\n\t\t\tp.spacedToken(cmd.Op.String(), cmd.OpPos)\n\t\t\tp.advanceLine(cmd.OpPos.Line())\n\t\t\tp.comments(cmd.Y.Comments...)\n\t\t\tp.newline(Pos{})\n\t\t\tp.indent()\n\t\t}\n\t\tp.advanceLine(cmd.Y.Pos().Line())\n\t\t_, p.nestedBinary = cmd.Y.Cmd.(*BinaryCmd)\n\t\tp.stmt(cmd.Y)\n\t\tif indent {\n\t\t\tp.decLevel()\n\t\t}\n\t\tp.nestedBinary = false\n\tcase *FuncDecl:\n\t\tif cmd.RsrvWord {\n\t\t\tp.spacedString(\"function\", Pos{})\n\t\t}\n\t\tif cmd.Name != nil {\n\t\t\tp.spacedString(cmd.Name.Value, Pos{})\n\t\t} else {\n\t\t\tfor _, name := range cmd.Names {\n\t\t\t\tp.spacedString(name.Value, Pos{})\n\t\t\t}\n\t\t}\n\t\tif !cmd.RsrvWord || cmd.Parens {\n\t\t\tp.w.WriteString(\"()\")\n\t\t\tp.wantSpace = spaceNotRequired\n\t\t}\n\t\tif p.funcNextLine {\n\t\t\tp.newline(Pos{})\n\t\t\tp.indent()\n\t\t} else if !cmd.Parens || !p.minify {\n\t\t\tp.space()\n\t\t}\n\t\tp.advanceLine(cmd.Body.Pos().Line())\n\t\tp.comments(cmd.Body.Comments...)\n\t\tp.stmt(cmd.Body)\n\tcase *CaseClause:\n\t\tp.w.WriteString(\"case \")\n\t\tp.word(cmd.Word)\n\t\tp.w.WriteString(\" in\")\n\t\tp.advanceLine(cmd.In.Line())\n\t\tp.wantSpace = spaceRequired\n\t\tif p.swtCaseIndent {\n\t\t\tp.incLevel()\n\t\t}\n\t\tif len(cmd.Items) == 0 {\n\t\t\t// Apparently \"case x in; esac\" is invalid shell.\n\t\t\tp.mustNewline = true\n\t\t}\n\t\tfor i, ci := range cmd.Items {\n\t\t\tvar last []Comment\n\t\t\tfor i, c := range ci.Comments {\n\t\t\t\tif c.Pos().After(ci.Pos()) {\n\t\t\t\t\tlast = ci.Comments[i:]\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tp.comments(c)\n\t\t\t}\n\t\t\tp.newlines(ci.Pos())\n\t\t\tp.spacePad(ci.Pos())\n\t\t\tp.casePatternJoin(ci.Patterns)\n\t\t\tp.w.WriteByte(')')\n\t\t\tif !p.minify {\n\t\t\t\tp.wantSpace = spaceRequired\n\t\t\t} else {\n\t\t\t\tp.wantSpace = spaceNotRequired\n\t\t\t}\n\n\t\t\tp.nestedStmts(ci.Stmts, ci.Last, ci.OpPos)\n\t\t\tp.level++\n\t\t\tif !p.minify || i != len(cmd.Items)-1 {\n\t\t\t\tif p.wantsNewline(ci.OpPos, false) {\n\t\t\t\t\tp.newlines(ci.OpPos)\n\t\t\t\t\tp.wantNewline = true\n\t\t\t\t}\n\t\t\t\tp.spacedToken(ci.Op.String(), ci.OpPos)\n\t\t\t\tp.advanceLine(ci.OpPos.Line())\n\t\t\t\t// avoid ; directly after tokens like ;;\n\t\t\t\tp.wroteSemi = true\n\t\t\t}\n\t\t\tp.comments(last...)\n\t\t\tp.flushComments()\n\t\t\tp.level--\n\t\t}\n\t\tp.comments(cmd.Last...)\n\t\tif p.swtCaseIndent {\n\t\t\tp.flushComments()\n\t\t\tp.decLevel()\n\t\t}\n\t\tp.semiRsrv(\"esac\", cmd.Esac)\n\tcase *ArithmCmd:\n\t\tp.w.WriteString(\"((\")\n\t\tif cmd.Unsigned {\n\t\t\tp.w.WriteString(\"# \")\n\t\t}\n\t\tp.arithmExpr(cmd.X, false, false)\n\t\tp.w.WriteString(\"))\")\n\tcase *TestClause:\n\t\tp.w.WriteString(\"[[ \")\n\t\tp.incLevel()\n\t\tp.testExpr(cmd.X)\n\t\tp.decLevel()\n\t\tp.spacedString(\"]]\", cmd.Right)\n\tcase *DeclClause:\n\t\tp.spacedString(cmd.Variant.Value, cmd.Pos())\n\t\tp.assigns(cmd.Args)\n\tcase *TimeClause:\n\t\tp.spacedString(\"time\", cmd.Pos())\n\t\tif cmd.PosixFormat {\n\t\t\tp.spacedString(\"-p\", cmd.Pos())\n\t\t}\n\t\tif cmd.Stmt != nil {\n\t\t\tp.stmt(cmd.Stmt)\n\t\t}\n\tcase *CoprocClause:\n\t\tp.spacedString(\"coproc\", cmd.Pos())\n\t\tif cmd.Name != nil {\n\t\t\tp.space()\n\t\t\tp.word(cmd.Name)\n\t\t}\n\t\tp.space()\n\t\tp.stmt(cmd.Stmt)\n\tcase *LetClause:\n\t\tp.spacedString(\"let\", cmd.Pos())\n\t\tfor _, n := range cmd.Exprs {\n\t\t\tp.space()\n\t\t\tp.arithmExpr(n, true, false)\n\t\t}\n\tcase *TestDecl:\n\t\tp.spacedString(\"@test\", cmd.Pos())\n\t\tp.space()\n\t\tp.word(cmd.Description)\n\t\tp.space()\n\t\tp.stmt(cmd.Body)\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"syntax.Printer: unexpected node type %T\", cmd))\n\t}\n\treturn startRedirs\n}\n\nfunc (p *Printer) ifClause(ic *IfClause, elif bool) {\n\tif !elif {\n\t\tp.spacedString(\"if\", ic.Pos())\n\t}\n\tp.nestedStmts(ic.Cond, ic.CondLast, Pos{})\n\tp.semiOrNewl(\"then\", ic.ThenPos)\n\tthenEnd := ic.FiPos\n\tel := ic.Else\n\tif el != nil {\n\t\tthenEnd = el.Position\n\t}\n\tp.nestedStmts(ic.Then, ic.ThenLast, thenEnd)\n\n\tif el != nil && el.ThenPos.IsValid() {\n\t\tp.comments(ic.Last...)\n\t\tp.semiRsrv(\"elif\", el.Position)\n\t\tp.ifClause(el, true)\n\t\treturn\n\t}\n\tif el == nil {\n\t\tp.comments(ic.Last...)\n\t} else {\n\t\tvar left []Comment\n\t\tfor _, c := range ic.Last {\n\t\t\tif c.Pos().After(el.Position) {\n\t\t\t\tleft = append(left, c)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tp.comments(c)\n\t\t}\n\t\tp.semiRsrv(\"else\", el.Position)\n\t\tp.comments(left...)\n\t\tp.nestedStmts(el.Then, el.ThenLast, ic.FiPos)\n\t\tp.comments(el.Last...)\n\t}\n\tp.semiRsrv(\"fi\", ic.FiPos)\n}\n\nfunc (p *Printer) stmtList(stmts []*Stmt, last []Comment) {\n\tsep := p.wantNewline || (len(stmts) > 0 && stmts[0].Pos().Line() > p.line)\n\tfor i, s := range stmts {\n\t\tif i > 0 && p.singleLine && p.wantNewline && !p.wroteSemi {\n\t\t\t// In singleLine mode, ensure we use semicolons between\n\t\t\t// statements.\n\t\t\tp.w.WriteByte(';')\n\t\t\tp.wantSpace = spaceRequired\n\t\t}\n\t\tpos := s.Pos()\n\t\tvar midComs, endComs []Comment\n\t\tfor _, c := range s.Comments {\n\t\t\t// Comments after the end of this command. Note that\n\t\t\t// this includes \"<<EOF # comment\".\n\t\t\tif s.Cmd != nil && c.End().After(s.Cmd.End()) {\n\t\t\t\tendComs = append(endComs, c)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Comments between the beginning of the statement and\n\t\t\t// the end of the command.\n\t\t\tif c.Pos().After(pos) {\n\t\t\t\tmidComs = append(midComs, c)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// The rest of the comments are before the entire\n\t\t\t// statement.\n\t\t\tp.comments(c)\n\t\t}\n\t\tif p.mustNewline || !p.minify || p.wantSpace == spaceRequired {\n\t\t\tp.newlines(pos)\n\t\t}\n\t\tp.advanceLine(pos.Line())\n\t\tp.comments(midComs...)\n\t\tp.stmt(s)\n\t\tp.comments(endComs...)\n\t\tp.wantNewline = true\n\t}\n\tif len(stmts) == 1 && !sep {\n\t\tp.wantNewline = false\n\t}\n\tp.comments(last...)\n}\n\nfunc (p *Printer) nestedStmts(stmts []*Stmt, last []Comment, closing Pos) {\n\tp.incLevel()\n\tswitch {\n\tcase len(stmts) > 1:\n\t\t// Force a newline if we find:\n\t\t//     { stmt; stmt; }\n\t\tp.wantNewline = true\n\tcase closing.Line() > p.line && len(stmts) > 0 &&\n\t\tstmtsEnd(stmts, last).Line() < closing.Line():\n\t\t// Force a newline if we find:\n\t\t//     { stmt\n\t\t//     }\n\t\tp.wantNewline = true\n\tcase len(p.pendingComments) > 0 && len(stmts) > 0:\n\t\t// Force a newline if we find:\n\t\t//     for i in a b # stmt\n\t\t//     do foo; done\n\t\tp.wantNewline = true\n\t}\n\tp.stmtList(stmts, last)\n\tif closing.IsValid() {\n\t\tp.flushComments()\n\t}\n\tp.decLevel()\n}\n\nfunc (p *Printer) assigns(assigns []*Assign) {\n\tp.incLevel()\n\tfor _, a := range assigns {\n\t\tif p.wantsNewline(a.Pos(), true) {\n\t\t\tp.bslashNewl()\n\t\t} else {\n\t\t\tp.spacePad(a.Pos())\n\t\t}\n\t\tif a.Name != nil {\n\t\t\tp.writeLit(a.Name.Value)\n\t\t\tp.wroteIndex(a.Index)\n\t\t\tif a.Append {\n\t\t\t\tp.w.WriteByte('+')\n\t\t\t}\n\t\t\tif !a.Naked {\n\t\t\t\tp.w.WriteByte('=')\n\t\t\t}\n\t\t}\n\t\tif a.Value != nil {\n\t\t\t// Ensure we don't use an escaped newline after '=',\n\t\t\t// because that can result in indentation, thus\n\t\t\t// splitting \"foo=bar\" into \"foo= bar\".\n\t\t\tp.advanceLine(a.Value.Pos().Line())\n\t\t\tp.word(a.Value)\n\t\t} else if a.Array != nil {\n\t\t\tp.wantSpace = spaceNotRequired\n\t\t\tp.w.WriteByte('(')\n\t\t\tp.elemJoin(a.Array.Elems, a.Array.Last)\n\t\t\tp.rightParen(a.Array.Rparen)\n\t\t}\n\t\tp.wantSpace = spaceRequired\n\t}\n\tp.decLevel()\n}\n\ntype wantSpaceState uint8\n\nconst (\n\tspaceNotRequired wantSpaceState = iota\n\tspaceRequired                   // we should generally print a space or a newline next\n\tspaceWritten                    // we have just written a space or newline\n)\n\n// extraIndenter ensures that all lines in a '<<-' heredoc body have at least\n// baseIndent leading tabs. Those that had more tab indentation than the first\n// heredoc line will keep that relative indentation.\ntype extraIndenter struct {\n\tbufWriter\n\tbaseIndent int\n\n\tfirstIndent int\n\tfirstChange int\n\tcurLine     []byte\n}\n\nfunc (e *extraIndenter) WriteByte(b byte) error {\n\te.curLine = append(e.curLine, b)\n\tif b != '\\n' {\n\t\treturn nil\n\t}\n\ttrimmed := bytes.TrimLeft(e.curLine, \"\\t\")\n\tif len(trimmed) == 1 {\n\t\t// no tabs if this is an empty line, i.e. \"\\n\"\n\t\te.bufWriter.Write(trimmed)\n\t\te.curLine = e.curLine[:0]\n\t\treturn nil\n\t}\n\n\tlineIndent := len(e.curLine) - len(trimmed)\n\tif e.firstIndent < 0 {\n\t\t// This is the first heredoc line we add extra indentation to.\n\t\t// Keep track of how much we indented.\n\t\te.firstIndent = lineIndent\n\t\te.firstChange = e.baseIndent - lineIndent\n\t\tlineIndent = e.baseIndent\n\n\t} else if lineIndent < e.firstIndent {\n\t\t// This line did not have enough indentation; simply indent it\n\t\t// like the first line.\n\t\tlineIndent = e.firstIndent\n\t} else {\n\t\t// This line had plenty of indentation. Add the extra\n\t\t// indentation that the first line had, for consistency.\n\t\tlineIndent += e.firstChange\n\t}\n\te.bufWriter.WriteByte(tabwriter.Escape)\n\tfor range lineIndent {\n\t\te.bufWriter.WriteByte('\\t')\n\t}\n\te.bufWriter.WriteByte(tabwriter.Escape)\n\te.bufWriter.Write(trimmed)\n\te.curLine = e.curLine[:0]\n\treturn nil\n}\n\nfunc (e *extraIndenter) WriteString(s string) (int, error) {\n\tfor i := range len(s) {\n\t\te.WriteByte(s[i])\n\t}\n\treturn len(s), nil\n}\n\nfunc startsWithLparen(node Node) bool {\n\tswitch node := node.(type) {\n\tcase *Stmt:\n\t\treturn startsWithLparen(node.Cmd)\n\tcase *BinaryCmd:\n\t\treturn startsWithLparen(node.X)\n\tcase *Subshell:\n\t\treturn true // keep ( (\n\tcase *ArithmCmd:\n\t\treturn true // keep ( ((\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "syntax/printer_test.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestPrintFiles(t *testing.T) {\n\tt.Parallel()\n\tfor lang := range langResolvedVariants.bits() {\n\t\tt.Run(lang.String(), func(t *testing.T) {\n\t\t\tparser := NewParser(Variant(lang), KeepComments(true))\n\t\t\tprinter := NewPrinter()\n\t\t\tfor _, c := range append(fileTests, fileTestsKeepComments...) {\n\t\t\t\twant, _ := c.byLangIndex[lang.index()].(*File)\n\t\t\t\tif want == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif wantBash, _ := c.byLangIndex[LangBash.index()].(*File); wantBash != nil &&\n\t\t\t\t\tlang != LangBash && want != wantBash {\n\t\t\t\t\t// Skip cases where a non-Bash language parses differently than Bash.\n\t\t\t\t\t// For example, `((foo))` prints as `( (foo))` only when in POSIX.\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\t\t\tin := c.inputs[0]\n\t\t\t\t\tprintTest(t, parser, printer, in, in)\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc strPrint(p *Printer, node Node) (string, error) {\n\tvar buf bytes.Buffer\n\terr := p.Print(&buf, node)\n\treturn buf.String(), err\n}\n\ntype printCase struct {\n\tin, want string\n}\n\nfunc samePrint(s string) printCase { return printCase{in: s, want: s} }\n\nvar printTests = []printCase{\n\tsamePrint(`fo○ b\\år`),\n\tsamePrint(`\"fo○ b\\år\"`),\n\tsamePrint(`'fo○ b\\år'`),\n\tsamePrint(`${a#fo○ b\\år}`),\n\tsamePrint(`#fo○ b\\år`),\n\tsamePrint(\"<<EOF\\nfo○ b\\\\år\\nEOF\"),\n\tsamePrint(`$'○ b\\år'`),\n\tsamePrint(\"${a/b//○}\"),\n\t{strings.Repeat(\" \", bufSize-3) + \"○\", \"○\"}, // at the end of a chunk\n\t{strings.Repeat(\" \", bufSize-0) + \"○\", \"○\"}, // at the start of a chunk\n\t{strings.Repeat(\" \", bufSize-2) + \"○\", \"○\"}, // split after 1st byte\n\t{strings.Repeat(\" \", bufSize-1) + \"○\", \"○\"}, // split after 2nd byte\n\t// peekByte that would (but cannot) go to the next chunk\n\t{strings.Repeat(\" \", bufSize-2) + \">(a)\", \">(a)\"},\n\t// escaped newline at end of chunk\n\t{\"a\" + strings.Repeat(\" \", bufSize-2) + \"\\\\\\nb\", \"a \\\\\\n\\tb\"},\n\t// panics if padding is only 4 (utf8.UTFMax)\n\t{strings.Repeat(\" \", bufSize-10) + \"${a/b//○}\", \"${a/b//○}\"},\n\t// multiple p.fill calls\n\t{\"a\" + strings.Repeat(\" \", bufSize*4) + \"b\", \"a b\"},\n\t// newline at the beginning of second chunk\n\t{\"a\" + strings.Repeat(\" \", bufSize-2) + \"\\nb\", \"a\\nb\"},\n\t{\"foo; bar\", \"foo\\nbar\"},\n\t{\"foo\\n\\n\\nbar\", \"foo\\n\\nbar\"},\n\t{\"foo\\n\\n\", \"foo\"},\n\t{\"\\n\\nfoo\", \"foo\"},\n\t{\"# foo \\n # bar\\t\", \"# foo\\n# bar\"},\n\tsamePrint(\"#\"),\n\tsamePrint(\"#c1\\\\\\n#c2\"),\n\tsamePrint(\"#\\\\\\n#\"),\n\t{\"#\\\\\\r\\n#\", \"#\\\\\\n#\"},\n\tsamePrint(\"{\\n\\tfoo\\n\\t# bar \\\\\\n}\"),\n\tsamePrint(\"foo\\\\\\\\\\nbar\"),\n\tsamePrint(\"a=b # inline\\nbar\"),\n\tsamePrint(\"a=$(b) # inline\"),\n\tsamePrint(\"foo # inline\\n# after\"),\n\tsamePrint(\"$(a) $(b)\"),\n\t{\"if a\\nthen\\n\\tb\\nfi\", \"if a; then\\n\\tb\\nfi\"},\n\tsamePrint(\"if a; then\\n\\tb\\nelse\\n\\tc\\nfi\"),\n\t{\"if a; then b\\nelse c\\nfi\", \"if a; then\\n\\tb\\nelse\\n\\tc\\nfi\"},\n\tsamePrint(\"foo >&2 <f bar\"),\n\tsamePrint(\"foo >&2 bar <f\"),\n\tsamePrint(\">&2 foo bar <f\"),\n\tsamePrint(\">&2 foo\"),\n\tsamePrint(\">&2 foo 2>&1 bar <f\"),\n\t{\"foo >&2>/dev/null\", \"foo >&2 >/dev/null\"},\n\t{\"foo <<EOF bar\\nl1\\nEOF\", \"foo bar <<EOF\\nl1\\nEOF\"},\n\tsamePrint(\"foo <<\\\\\\\\\\\\\\\\EOF\\nbar\\n\\\\\\\\EOF\"),\n\tsamePrint(\"foo <<\\\"\\\\EOF\\\"\\nbar\\n\\\\EOF\"),\n\tsamePrint(\"foo <<\\\"EOF\\\"\\nbar\\nEOF\\nbar\"),\n\tsamePrint(\"foo <<EOF && bar\\nl1\\nEOF\"),\n\tsamePrint(\"foo <<EOF &&\\nl1\\nEOF\\n\\tbar\"),\n\tsamePrint(\"foo <<EOF\\nl1\\nEOF\\n\\nfoo2\"),\n\tsamePrint(\"<<EOF\\nfoo\\\\\\nbar\\nEOF\"),\n\tsamePrint(\"<<'EOF'\\nfoo\\\\\\nbar\\nEOF\"),\n\tsamePrint(\"<<EOF\\n\\\\\\n$foo\\nEOF\"),\n\tsamePrint(\"<<'EOF'\\n\\\\\\nEOF\"),\n\tsamePrint(\"{\\n\\t<<EOF\\nfoo\\\\\\nbar\\nEOF\\n}\"),\n\tsamePrint(\"{\\n\\t<<'EOF'\\nfoo\\\\\\nbar\\nEOF\\n}\"),\n\tsamePrint(\"<<EOF\\nEOF\"),\n\tsamePrint(\"foo <<EOF\\nEOF\\n\\nbar\"),\n\tsamePrint(\"foo <<'EOF'\\nEOF\\n\\nbar\"),\n\t{\n\t\t\"{ foo; bar; }\",\n\t\t\"{\\n\\tfoo\\n\\tbar\\n}\",\n\t},\n\t{\n\t\t\"{ foo; bar; }\\n#etc\",\n\t\t\"{\\n\\tfoo\\n\\tbar\\n}\\n#etc\",\n\t},\n\t{\n\t\t\"{\\n\\tfoo; }\",\n\t\t\"{\\n\\tfoo\\n}\",\n\t},\n\t{\n\t\t\"{ foo\\n}\",\n\t\t\"{\\n\\tfoo\\n}\",\n\t},\n\t{\n\t\t\"(foo\\n)\",\n\t\t\"(\\n\\tfoo\\n)\",\n\t},\n\t{\n\t\t\"$(foo\\n)\",\n\t\t\"$(\\n\\tfoo\\n)\",\n\t},\n\t{\n\t\t\"a\\n\\n\\n# etc\\nb\",\n\t\t\"a\\n\\n# etc\\nb\",\n\t},\n\t{\n\t\t\"a b\\\\\\nc d\",\n\t\t\"a bc d\",\n\t},\n\t{\n\t\t\"a bb\\\\\\ncc d\",\n\t\t\"a bbcc d\",\n\t},\n\tsamePrint(\"a \\\\\\n\\tb \\\\\\n\\t\\\"c\\\" \\\\\\n\\t;\"),\n\tsamePrint(\"a=1 \\\\\\n\\tb=2 \\\\\\n\\tc=\\\"3\\\" \\\\\\n\\t;\"),\n\t{\n\t\t\"a=\\\\\\nfoo\\nb=\\\\\\n\\\"bar\\\"\\nc=\\\\\\n'baz'\",\n\t\t\"a=foo\\nb=\\\"bar\\\"\\nc='baz'\",\n\t},\n\n\tsamePrint(\"if a \\\\\\n\\t; then b; fi\"),\n\tsamePrint(\"a > \\\\\\n\\tfoo\"),\n\tsamePrint(\"a <<< \\\\\\n\\t\\\"foo\\\"\"),\n\tsamePrint(\"a 'b\\nb' c\"),\n\tsamePrint(\"a $'b\\nb' c\"),\n\t{\n\t\t\"(foo; bar)\",\n\t\t\"(\\n\\tfoo\\n\\tbar\\n)\",\n\t},\n\t{\n\t\t\"{\\nfoo\\nbar; }\",\n\t\t\"{\\n\\tfoo\\n\\tbar\\n}\",\n\t},\n\tsamePrint(\"\\\"$foo\\\"\\n{\\n\\tbar\\n}\"),\n\t{\n\t\t\"{\\nbar\\n# extra\\n}\",\n\t\t\"{\\n\\tbar\\n\\t# extra\\n}\",\n\t},\n\t{\n\t\t\"foo\\nbar  # extra\",\n\t\t\"foo\\nbar # extra\",\n\t},\n\t{\n\t\t\"foo # 1\\nfooo # 2\\nfo # 3\",\n\t\t\"foo  # 1\\nfooo # 2\\nfo   # 3\",\n\t},\n\t{\n\t\t\" foo # 1\\n fooo # 2\\n fo # 3\",\n\t\t\"foo  # 1\\nfooo # 2\\nfo   # 3\",\n\t},\n\t{\n\t\t\"foo   # 1\\nfooo  # 2\\nfo    # 3\",\n\t\t\"foo  # 1\\nfooo # 2\\nfo   # 3\",\n\t},\n\t{\n\t\t\"foooooa\\nfoo # 1\\nfooo # 2\\nfo # 3\\nfooooo\",\n\t\t\"foooooa\\nfoo  # 1\\nfooo # 2\\nfo   # 3\\nfooooo\",\n\t},\n\t{\n\t\t\"foo\\nbar\\nfoo # 1\\nfooo # 2\",\n\t\t\"foo\\nbar\\nfoo  # 1\\nfooo # 2\",\n\t},\n\tsamePrint(\"foobar # 1\\nfoo\\nfoo # 2\"),\n\tsamePrint(\"foobar # 1\\n#foo\\nfoo # 2\"),\n\tsamePrint(\"foobar # 1\\n\\nfoo # 2\"),\n\t{\n\t\t\"foo # 2\\nfoo2 bar # 1\",\n\t\t\"foo      # 2\\nfoo2 bar # 1\",\n\t},\n\t{\n\t\t\"foo bar # 1\\n! foo # 2\",\n\t\t\"foo bar # 1\\n! foo   # 2\",\n\t},\n\t{\n\t\t\"aa #b\\nc  #d\\ne\\nf #g\",\n\t\t\"aa #b\\nc  #d\\ne\\nf #g\",\n\t},\n\t{\n\t\t\"{ a; } #x\\nbbb #y\\n{ #z\\nc\\n}\",\n\t\t\"{ a; } #x\\nbbb    #y\\n{      #z\\n\\tc\\n}\",\n\t},\n\t{\n\t\t\"foo; foooo # 1\",\n\t\t\"foo\\nfoooo # 1\",\n\t},\n\t{\n\t\t\"aaa; b #1\\nc #2\",\n\t\t\"aaa\\nb #1\\nc #2\",\n\t},\n\t{\n\t\t\"a #1\\nbbb; c #2\\nd #3\",\n\t\t\"a #1\\nbbb\\nc #2\\nd #3\",\n\t},\n\tsamePrint(\"aa #c1\\n{  #c2\\n\\tb\\n}\"),\n\t{\n\t\t\"aa #c1\\n{ b; c; } #c2\",\n\t\t\"aa #c1\\n{\\n\\tb\\n\\tc\\n} #c2\",\n\t},\n\tsamePrint(\"a #c1\\n'b\\ncc' #c2\"),\n\t{\n\t\t\"(\\nbar\\n# extra\\n)\",\n\t\t\"(\\n\\tbar\\n\\t# extra\\n)\",\n\t},\n\t{\n\t\t\"for a in 1 2\\ndo\\n\\t# bar\\n\\tx\\ndone\",\n\t\t\"for a in 1 2; do\\n\\t# bar\\n\\tx\\ndone\",\n\t},\n\tsamePrint(\"#before\\nfoo | bar\"),\n\tsamePrint(\"#before\\nfoo && bar\"),\n\tsamePrint(\"foo | bar # inline\"),\n\tsamePrint(\"foo && bar # inline\"),\n\tsamePrint(\"foo `# inline` \\\\\\n\\tbar\"),\n\tsamePrint(\"for a in 1 2; do\\n\\n\\tbar\\ndone\"),\n\t{\n\t\t\"a \\\\\\n\\t&& b\",\n\t\t\"a &&\\n\\tb\",\n\t},\n\t{\n\t\t\"a \\\\\\n\\t&& b\\nc\",\n\t\t\"a &&\\n\\tb\\nc\",\n\t},\n\t{\n\t\t\"{\\n(a \\\\\\n&& b)\\nc\\n}\",\n\t\t\"{\\n\\t(a &&\\n\\t\\tb)\\n\\tc\\n}\",\n\t},\n\t{\n\t\t\"a && b \\\\\\n&& c\",\n\t\t\"a && b &&\\n\\tc\",\n\t},\n\t{\n\t\t\"a \\\\\\n&& $(b) && c \\\\\\n&& d\",\n\t\t\"a &&\\n\\t$(b) && c &&\\n\\td\",\n\t},\n\t{\n\t\t\"a \\\\\\n&& b\\nc \\\\\\n&& d\",\n\t\t\"a &&\\n\\tb\\nc &&\\n\\td\",\n\t},\n\t{\n\t\t\"a \\\\\\n&&\\n#c\\nb\",\n\t\t\"a &&\\n\\t#c\\n\\tb\",\n\t},\n\t{\n\t\t\"a | {\\nb \\\\\\n| c\\n}\",\n\t\t\"a | {\\n\\tb |\\n\\t\\tc\\n}\",\n\t},\n\t{\n\t\t\"a \\\\\\n\\t&& if foo; then\\nbar\\nfi\",\n\t\t\"a &&\\n\\tif foo; then\\n\\t\\tbar\\n\\tfi\",\n\t},\n\t{\n\t\t\"if\\nfoo\\nthen\\nbar\\nfi\",\n\t\t\"if\\n\\tfoo\\nthen\\n\\tbar\\nfi\",\n\t},\n\t{\n\t\t\"if foo \\\\\\nbar\\nthen\\nbar\\nfi\",\n\t\t\"if foo \\\\\\n\\tbar; then\\n\\tbar\\nfi\",\n\t},\n\t{\n\t\t\"if foo \\\\\\n&& bar\\nthen\\nbar\\nfi\",\n\t\t\"if foo &&\\n\\tbar; then\\n\\tbar\\nfi\",\n\t},\n\t{\n\t\t\"a |\\nb |\\nc\",\n\t\t\"a |\\n\\tb |\\n\\tc\",\n\t},\n\tsamePrint(\"a |\\n\\tb | c |\\n\\td\"),\n\tsamePrint(\"a | b |\\n\\tc |\\n\\td\"),\n\t{\n\t\t\"foo |\\n# misplaced\\nbar\",\n\t\t\"foo |\\n\\t# misplaced\\n\\tbar\",\n\t},\n\tsamePrint(\"{\\n\\tfoo\\n\\t#a\\n\\tbar\\n} | etc\"),\n\t{\n\t\t\"foo &&\\n#a1\\n#a2\\n$(bar)\",\n\t\t\"foo &&\\n\\t#a1\\n\\t#a2\\n\\t$(bar)\",\n\t},\n\t{\n\t\t\"{\\n\\tfoo\\n\\t#a\\n} |\\n# misplaced\\nbar\",\n\t\t\"{\\n\\tfoo\\n\\t#a\\n} |\\n\\t# misplaced\\n\\tbar\",\n\t},\n\tsamePrint(\"foo | bar\\n#after\"),\n\t{\n\t\t\"a |\\nb | #c2\\nc\",\n\t\t\"a |\\n\\tb | #c2\\n\\tc\",\n\t},\n\t{\n\t\t\"{\\nfoo &&\\n#a1\\n#a2\\n$(bar)\\n}\",\n\t\t\"{\\n\\tfoo &&\\n\\t\\t#a1\\n\\t\\t#a2\\n\\t\\t$(bar)\\n}\",\n\t},\n\t{\n\t\t\"foo | while read l; do\\nbar\\ndone\",\n\t\t\"foo | while read l; do\\n\\tbar\\ndone\",\n\t},\n\tsamePrint(\"while x; do\\n\\t#comment\\n\\ty\\ndone\"),\n\t{\n\t\t\"while x\\ndo\\n\\ty\\ndone\",\n\t\t\"while x; do\\n\\ty\\ndone\",\n\t},\n\tsamePrint(\"\\\"\\\\\\nfoo\\\"\"),\n\tsamePrint(\"'\\\\\\nfoo'\"),\n\tsamePrint(\"\\\"foo\\\\\\n  bar\\\"\"),\n\tsamePrint(\"'foo\\\\\\n  bar'\"),\n\tsamePrint(\"v=\\\"\\\\\\nfoo\\\"\"),\n\t{\n\t\t\"v=foo\\\\\\nbar\",\n\t\t\"v=foobar\",\n\t},\n\t{\n\t\t\"v='foo'\\\\\\n'bar'\",\n\t\t\"v='foo''bar'\",\n\t},\n\t{\n\t\t\"v=\\\\\\n\\\"foo\\\"\",\n\t\t\"v=\\\"foo\\\"\",\n\t},\n\t{\n\t\t\"v=\\\\\\nfoo\\\\\\n$bar\",\n\t\t\"v=foo$bar\",\n\t},\n\tsamePrint(\"\\\"\\\\\\n\\\\\\nfoo\\\\\\n\\\\\\n\\\"\"),\n\tsamePrint(\"'\\\\\\n\\\\\\nfoo\\\\\\n\\\\\\n'\"),\n\t{\n\t\t\"foo \\\\\\n>bar\\netc\",\n\t\t\"foo \\\\\\n\\t>bar\\netc\",\n\t},\n\t{\n\t\t\"foo \\\\\\nfoo2 \\\\\\n>bar\",\n\t\t\"foo \\\\\\n\\tfoo2 \\\\\\n\\t>bar\",\n\t},\n\tsamePrint(\"> >(foo)\"),\n\tsamePrint(\"x > >(foo) y\"),\n\tsamePrint(\"a | (x) |\\n\\tb\"),\n\tsamePrint(\"a | (\\n\\tx\\n\\ty\\n) |\\n\\tb\"),\n\tsamePrint(\"a |\\n\\tif foo; then\\n\\t\\tbar\\n\\tfi |\\n\\tb\"),\n\tsamePrint(\"a | if foo; then\\n\\tbar\\nfi\"),\n\tsamePrint(\"a | b | if foo; then\\n\\tbar\\nfi\"),\n\t{\n\t\t\"case $i in\\n1)\\nfoo\\n;;\\nesac\",\n\t\t\"case $i in\\n1)\\n\\tfoo\\n\\t;;\\nesac\",\n\t},\n\t{\n\t\t\"case $i in\\n1)\\nfoo\\nesac\",\n\t\t\"case $i in\\n1)\\n\\tfoo\\n\\t;;\\nesac\",\n\t},\n\t{\n\t\t\"case $i in\\n1) foo\\nesac\",\n\t\t\"case $i in\\n1) foo ;;\\nesac\",\n\t},\n\t{\n\t\t\"case $i in\\n1) foo; bar\\nesac\",\n\t\t\"case $i in\\n1)\\n\\tfoo\\n\\tbar\\n\\t;;\\nesac\",\n\t},\n\t{\n\t\t\"case $i in\\n1) foo; bar;;\\nesac\",\n\t\t\"case $i in\\n1)\\n\\tfoo\\n\\tbar\\n\\t;;\\nesac\",\n\t},\n\t{\n\t\t\"case $i in\\n1)\\n#foo \\t\\n;;\\nesac\",\n\t\t\"case $i in\\n1)\\n\\t#foo\\n\\t;;\\nesac\",\n\t},\n\tsamePrint(\"case $i in\\n1)\\n\\t;;\\n\\n2)\\n\\t;;\\nesac\"),\n\t{\n\t\t\"case $i\\nin\\n1)\\n\\t;;\\nesac\",\n\t\t\"case $i in\\n1)\\n\\t;;\\nesac\",\n\t},\n\tsamePrint(\"case $i in\\n1)\\n\\ta\\n\\t#b\\n\\t;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1) foo() { bar; } ;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1) ;; #foo\\nesac\"),\n\tsamePrint(\"case $i in\\n#foo\\nesac\"),\n\tsamePrint(\"case $i in\\n#before\\n1) ;;\\nesac\"),\n\tsamePrint(\"case $i in\\n#bef\\n1) ;; #inl\\nesac\"),\n\tsamePrint(\"case $i in\\n#before1\\n'1') ;;\\n#before2\\n'2') ;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1) ;; #inl1\\n2) ;; #inl2\\nesac\"),\n\tsamePrint(\"case $i in\\n#bef\\n1) #inl\\n\\tfoo\\n\\t;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1) #inl\\n\\t;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1) a \\\\\\n\\tb ;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1 | 2 | \\\\\\n\\t3 | 4) a b ;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1 | 2 | \\\\\\n\\t3 | 4)\\n\\ta b\\n\\t;;\\nesac\"),\n\tsamePrint(\"case $i in\\nx) ;;\\ny) for n in 1; do echo $n; done ;;\\nesac\"),\n\tsamePrint(\"case a in b) [[ x =~ y ]] ;; esac\"),\n\tsamePrint(\"case a in b) [[ a =~ b$ || c =~ d$ ]] ;; esac\"),\n\tsamePrint(\"case a in b) [[ a =~ (b) ]] ;; esac\"),\n\tsamePrint(\"[[ (a =~ b$) ]]\"),\n\tsamePrint(\"[[ a && ((b || c) && d) ]]\"),\n\tsamePrint(\"[[ a &&\\n\\tb ]]\"),\n\tsamePrint(\"[[ a ||\\n\\tb ]]\"),\n\t{\n\t\t\"[[ -f \\\\\\n\\tfoo ]]\",\n\t\t\"[[ -f foo ]]\",\n\t},\n\t{\n\t\t\"[[ foo \\\\\\n\\t-ef \\\\\\n\\tbar ]]\",\n\t\t\"[[ foo -ef bar ]]\",\n\t},\n\t{\n\t\t\"[[ a && \\\\\\nb \\\\\\n && c ]]\",\n\t\t\"[[ a &&\\n\\tb &&\\n\\tc ]]\",\n\t},\n\tsamePrint(\"{\\n\\t[[ a || b ]]\\n}\"),\n\t{\n\t\t\"a=(\\nb\\nc\\n) b=c\",\n\t\t\"a=(\\n\\tb\\n\\tc\\n) b=c\",\n\t},\n\tsamePrint(\"a=(\\n\\t#before\\n\\tb #inline\\n)\"),\n\tsamePrint(\"a=(\\n\\tb #foo\\n\\tc #bar\\n)\"),\n\tsamePrint(\"a=(\\n\\tb\\n\\n\\t#foo\\n\\t#bar\\n\\tc\\n)\"),\n\tsamePrint(\"a=(\\n\\t#foo\\n\\t#bar\\n\\tc\\n)\"),\n\tsamePrint(\"a=(\\n\\t#lone\\n)\"),\n\tsamePrint(\"a=(\\n\\n)\"),\n\tsamePrint(\"a=(\\n\\tx\\n\\n\\ty\\n)\"),\n\tsamePrint(\"foo <<EOF | $(bar)\\n3\\nEOF\"),\n\t{\n\t\t\"a <<EOF\\n$(\\n\\tb\\n\\tc)\\nEOF\",\n\t\t\"a <<EOF\\n$(\\n\\tb\\n\\tc\\n)\\nEOF\",\n\t},\n\tsamePrint(\"<<EOF1\\n$(\\n\\t<<EOF2\\ninner\\nEOF2\\n)\\nEOF1\"),\n\t{\n\t\t\"<(<<EOF\\nbody\\nEOF\\n)\",\n\t\t\"<(\\n\\t<<EOF\\nbody\\nEOF\\n)\",\n\t},\n\t{\n\t\t\"( (foo) )\\n$( (foo) )\\n<( (foo) )\",\n\t\t\"( (foo))\\n$( (foo))\\n<((foo))\",\n\t},\n\t{\n\t\t\"if ( ((foo)) || bar ); then baz; fi\",\n\t\t\"if ( ((foo)) || bar); then baz; fi\",\n\t},\n\tsamePrint(\"if x; then (\\n\\ty\\n) & fi\"),\n\tsamePrint(\"\\\"foo\\n$(bar)\\\"\"),\n\tsamePrint(\"\\\"foo\\\\\\n$(bar)\\\"\"),\n\tsamePrint(\"\\\"foo\\\\\\nbar\\\"\"),\n\tsamePrint(\"((foo++)) || bar\"),\n\t{\n\t\t\"a=b \\\\\\nc=d \\\\\\nfoo\",\n\t\t\"a=b \\\\\\n\\tc=d \\\\\\n\\tfoo\",\n\t},\n\t{\n\t\t\"a=b \\\\\\nc=d \\\\\\nfoo \\\\\\nbar\",\n\t\t\"a=b \\\\\\n\\tc=d \\\\\\n\\tfoo \\\\\\n\\tbar\",\n\t},\n\tsamePrint(\"a $(x) \\\\\\n\\tb\"),\n\tsamePrint(\"\\\"foo\\nbar\\\"\\netc\"),\n\tsamePrint(\"\\\"foo\\nbar\\nbar2\\\"\\netc\"),\n\tsamePrint(\"a=\\\"$b\\n\\\"\\nd=e\"),\n\tsamePrint(\"\\\"\\n\\\"\\n\\nfoo\"),\n\tsamePrint(\"$\\\"\\n\\\"\\n\\nfoo\"),\n\tsamePrint(\"'\\n'\\n\\nfoo\"),\n\tsamePrint(\"$'\\n'\\n\\nfoo\"),\n\tsamePrint(\"foo <<EOF\\na\\nb\\nc\\nd\\nEOF\\n{\\n\\tbar\\n}\"),\n\tsamePrint(\"foo bar # one\\nif a; then\\n\\tb\\nfi # two\"),\n\t{\n\t\t\"# foo\\n\\n\\nbar\",\n\t\t\"# foo\\n\\nbar\",\n\t},\n\t{\n\t\t\"# foo\\n\\n\\nbar\\nbaz\",\n\t\t\"# foo\\n\\nbar\\nbaz\",\n\t},\n\tsamePrint(\"#foo\\n#\\n#bar\"),\n\t{\n\t\t\"(0 #\\n0)#\\n0\",\n\t\t\"(\\n\\t0 #\\n\\t0\\n) #\\n0\",\n\t},\n\tsamePrint(\"a | #c1\\n\\t(\\n\\t\\tb\\n\\t)\"),\n\tsamePrint(\"a | #c1\\n\\t{\\n\\t\\tb\\n\\t}\"),\n\tsamePrint(\"a | #c1\\n\\tif b; then\\n\\t\\tc\\n\\tfi\"),\n\tsamePrint(\"a | #c1\\n\\t#c2\\n\\t#c3\\n\\tb\"),\n\tsamePrint(\"a && #c1\\n\\t(\\n\\t\\tb\\n\\t)\"),\n\tsamePrint(\"f() body # comment\"),\n\tsamePrint(\"f <<EOF\\nbody\\nEOF\"),\n\tsamePrint(\"f <<EOF\\nEOF\"),\n\tsamePrint(\"f <<-EOF\\n\\tbody\\nEOF\"),\n\t{\n\t\t\"f <<-EOF\\nbody\\nEOF\",\n\t\t\"f <<-EOF\\n\\tbody\\nEOF\",\n\t},\n\tsamePrint(\"f <<-EOF\\nEOF\"),\n\tsamePrint(\"f <<-EOF\\n\\n\\nEOF\"),\n\tsamePrint(\"f <<-EOF\\n\\n\\tindented\\n\\nEOF\"),\n\tsamePrint(\"{\\n\\tf <<EOF\\nEOF\\n}\"),\n\tsamePrint(\"{\\n\\tf <<-EOF\\n\\t\\tbody\\n\\tEOF\\n}\"),\n\tsamePrint(\"{\\n\\tf <<-EOF\\n\\t\\tbody\\n\\tEOF\\n\\tf2\\n}\"),\n\tsamePrint(\"f <<-EOF\\n\\t{\\n\\t\\tnicely indented\\n\\t}\\nEOF\"),\n\tsamePrint(\"f <<-EOF\\n\\t{\\n\\t\\tnicely indented\\n\\t}\\nEOF\"),\n\t{\n\t\t\"f <<-EOF\\n\\t{\\nbadly indented\\n\\t}\\nEOF\",\n\t\t\"f <<-EOF\\n\\t{\\n\\tbadly indented\\n\\t}\\nEOF\",\n\t},\n\t{\n\t\t\"f <<-EOF\\n\\t\\t{\\n\\t\\t\\ttoo indented\\n\\t\\t}\\nEOF\",\n\t\t\"f <<-EOF\\n\\t{\\n\\t\\ttoo indented\\n\\t}\\nEOF\",\n\t},\n\t{\n\t\t\"f <<-EOF\\n{\\n\\ttoo little indented\\n}\\nEOF\",\n\t\t\"f <<-EOF\\n\\t{\\n\\t\\ttoo little indented\\n\\t}\\nEOF\",\n\t},\n\tsamePrint(\"<<-EOF\\n\\t$foo\\nEOF\\n\\n{\\n\\tbar\\n}\"),\n\tsamePrint(\"f <<EOF\\nEOF\\n# comment\"),\n\tsamePrint(\"f <<EOF\\nEOF\\n# comment\\nbar\"),\n\tsamePrint(\"f <<EOF # inline\\n$(\\n\\t# inside\\n)\\nEOF\\n# outside\\nbar\"),\n\tsamePrint(\"while foo; do\\n\\tbar\\ndone <<-EOF # inline\\n\\tbaz\\nEOF\"),\n\tsamePrint(\"{\\n\\tcat <<EOF\\nEOF\\n\\t# comment\\n}\"),\n\t{\n\t\t\"if foo # inline\\nthen\\n\\tbar\\nfi\",\n\t\t\"if foo; then # inline\\n\\tbar\\nfi\",\n\t},\n\tsamePrint(\"for i; do echo $i; done\"),\n\tsamePrint(\"for i in; do echo $i; done\"),\n\t{\n\t\t\"for foo in a b # inline\\ndo\\n\\tbar\\ndone\",\n\t\t\"for foo in a b; do # inline\\n\\tbar\\ndone\",\n\t},\n\t{\n\t\t\"if x # inline\\nthen bar; fi\",\n\t\t\"if x; then # inline\\n\\tbar\\nfi\",\n\t},\n\t{\n\t\t\"for i in a b # inline\\ndo bar; done\",\n\t\t\"for i in a b; do # inline\\n\\tbar\\ndone\",\n\t},\n\t{\n\t\t\"for i #a\\n\\tin 1; do #b\\nx\\ndone\",\n\t\t\"for i in \\\\\\n\\t1; do #a\\n\\t#b\\n\\tx\\ndone\",\n\t},\n\t{\n\t\t\"foo() # inline\\n{\\n\\tbar\\n}\",\n\t\t\"foo() { # inline\\n\\tbar\\n}\",\n\t},\n\t{\n\t\t\"foo() #before\\n(\\n\\tbar #inline\\n)\",\n\t\t\"foo() ( #before\\n\\tbar #inline\\n)\",\n\t},\n\t{\n\t\t\"foo() (#before\\n\\tbar #inline\\n)\",\n\t\t\"foo() ( #before\\n\\tbar #inline\\n)\",\n\t},\n\t{\n\t\t\"foo()\\n#before-1\\n(#before-2\\n\\tbar #inline\\n)\",\n\t\t\"foo() ( #before-1\\n\\t#before-2\\n\\tbar #inline\\n)\",\n\t},\n\t{\n\t\t\"(#before\\n\\tbar #inline\\n)\",\n\t\t\"( #before\\n\\tbar #inline\\n)\",\n\t},\n\t{\n\t\t\"(\\n#before\\n\\tbar #inline\\n)\",\n\t\t\"(\\n\\t#before\\n\\tbar #inline\\n)\",\n\t},\n\t{\n\t\t\"foo=$(#before\\n\\tbar #inline\\n)\",\n\t\t\"foo=$( #before\\n\\tbar #inline\\n)\",\n\t},\n\t{\n\t\t\"foo=`#before\\nbar`\",\n\t\t\"foo=$( #before\\n\\tbar\\n)\",\n\t},\n\tsamePrint(\"if foo; then\\n\\tbar\\n\\t# comment\\nfi\"),\n\tsamePrint(\"if foo; then\\n\\tbar\\n# else commented out\\nfi\"),\n\tsamePrint(\"if foo; then\\n\\tx\\nelse\\n\\tbar\\n\\t# comment\\nfi\"),\n\tsamePrint(\"if foo; then\\n\\tx\\n#comment\\nelse\\n\\ty\\nfi\"),\n\tsamePrint(\"if foo; then\\n\\tx\\n\\t#comment\\nelse\\n\\ty\\nfi\"),\n\t{\n\t\t\"if foo; then\\n\\tx\\n#a\\n\\t#b\\n\\t#c\\nelse\\n\\ty\\nfi\",\n\t\t\"if foo; then\\n\\tx\\n\\t#a\\n\\t#b\\n\\t#c\\nelse\\n\\ty\\nfi\",\n\t},\n\tsamePrint(\"if foo; then\\n\\tx\\nelse #comment\\n\\ty\\nfi\"),\n\tsamePrint(\"if foo; then\\n\\tx\\n#comment\\nelif bar; then\\n\\ty\\nfi\"),\n\tsamePrint(\"if foo; then\\n\\tx\\n\\t#comment\\nelif bar; then\\n\\ty\\nfi\"),\n\tsamePrint(\"case i in\\nx)\\n\\ta\\n\\t;;\\n#comment\\ny) ;;\\nesac\"),\n\tsamePrint(\"case i in\\nx)\\n\\ta\\n\\t;;\\n\\t#comment\\ny) ;;\\nesac\"),\n\t{\n\t\t\"case i in\\nx)\\n\\ta\\n\\t;;\\n\\t#a\\n#b\\n\\t#c\\ny) ;;\\nesac\",\n\t\t\"case i in\\nx)\\n\\ta\\n\\t;;\\n\\t#a\\n\\t#b\\n\\t#c\\ny) ;;\\nesac\",\n\t},\n\tsamePrint(\"'foo\\tbar'\\n'foooo\\tbar'\"),\n\tsamePrint(\"\\\"foo\\tbar\\\"\\n\\\"foooo\\tbar\\\"\"),\n\tsamePrint(\"foo\\\\\\tbar\\nfoooo\\\\\\tbar\"),\n\tsamePrint(\"#foo\\tbar\\n#foooo\\tbar\"),\n\t{\n\t\t\"array=('one'\\n\\t\\t# 'two'\\n\\t\\t'three')\",\n\t\t\"array=('one'\\n\\t# 'two'\\n\\t'three')\",\n\t},\n\tsamePrint(\"#comment\\n>redir\"),\n\t{\n\t\t\">redir \\\\\\n\\tfoo\",\n\t\t\">redir foo\",\n\t},\n\tsamePrint(\"$(declare)\"),\n\t{\n\t\t\"`declare`\",\n\t\t\"$(declare)\",\n\t},\n\t{\n\t\t\"(\\n(foo >redir))\",\n\t\t\"(\\n\\t(foo >redir)\\n)\",\n\t},\n\t{\n\t\t\"( (foo) )\",\n\t\t\"( (foo))\",\n\t},\n\t{\n\t\t\"( (foo); bar )\",\n\t\t\"(\\n\\t(foo)\\n\\tbar\\n)\",\n\t},\n\t{\n\t\t\"( ((foo++)) )\",\n\t\t\"( ((foo++)))\",\n\t},\n\t{\n\t\t\"( ((foo++)); bar )\",\n\t\t\"(\\n\\t((foo++))\\n\\tbar\\n)\",\n\t},\n\tsamePrint(\"(\\n\\t((foo++))\\n)\"),\n\tsamePrint(\"(foo && bar)\"),\n\tsamePrint(`$foo#bar ${foo}#bar 'foo'#bar \"foo\"#bar`),\n\tsamePrint(\"case $i in\\n1)\\n\\ta\\n\\t;;\\n(esac)\\n\\tb\\n\\t;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1)\\n\\ta\\n\\t;;\\n(esac | 2)\\n\\tb\\n\\t;;\\nesac\"),\n\tsamePrint(\"case $i in\\n1)\\n\\ta\\n\\t;;\\n2 | esac)\\n\\tb\\n\\t;;\\nesac\"),\n\t// TODO: support cases with command substitutions as well\n\t// {\n\t// \t\"`foo`#bar\",\n\t// \t\"$(foo)#bar\",\n\t// },\n\t// samePrint(`$(\"foo\"#bar)#bar`),\n}\n\nfunc TestPrintTable(t *testing.T) {\n\tt.Parallel()\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter()\n\tfor i, tc := range printTests {\n\t\tt.Run(fmt.Sprintf(\"#%03d\", i), func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t\tt.Run(fmt.Sprintf(\"#%03d-nl\", i), func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, \"\\n\"+tc.in+\"\\n\", tc.want)\n\t\t})\n\t\tt.Run(fmt.Sprintf(\"#%03d-redo\", i), func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.want, tc.want)\n\t\t})\n\t}\n}\n\nfunc parsePath(tb testing.TB, path string) *File {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\ttb.Fatal(err)\n\t}\n\tdefer f.Close()\n\tprog, err := NewParser(KeepComments(true)).Parse(f, \"\")\n\tif err != nil {\n\t\ttb.Fatal(err)\n\t}\n\treturn prog\n}\n\nconst canonicalPath = \"canonical.sh\"\n\nfunc TestPrintMultiline(t *testing.T) {\n\tt.Parallel()\n\tprog := parsePath(t, canonicalPath)\n\tgot, err := strPrint(NewPrinter(), prog)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\twantBs, err := os.ReadFile(canonicalPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// If we're on Windows and it was set up to automatically replace LF\n\t// with CRLF, that might make this test fail. Just ignore \\r characters.\n\twant := strings.ReplaceAll(string(wantBs), \"\\r\", \"\")\n\tgot = strings.ReplaceAll(got, \"\\r\", \"\")\n\tif got != want {\n\t\tt.Fatalf(\"Print mismatch in canonical.sh\")\n\t}\n}\n\nfunc TestPrintSpaces(t *testing.T) {\n\tt.Parallel()\n\tspaceFormats := [...]struct {\n\t\tspaces   uint\n\t\tin, want string\n\t}{\n\t\t{\n\t\t\t0,\n\t\t\t\"{\\nfoo \\\\\\nbar\\n}\",\n\t\t\t\"{\\n\\tfoo \\\\\\n\\t\\tbar\\n}\",\n\t\t},\n\t\t{\n\t\t\t2,\n\t\t\t\"{\\nfoo \\\\\\nbar\\n}\",\n\t\t\t\"{\\n  foo \\\\\\n    bar\\n}\",\n\t\t},\n\t\t{\n\t\t\t4,\n\t\t\t\"{\\nfoo \\\\\\nbar\\n}\",\n\t\t\t\"{\\n    foo \\\\\\n        bar\\n}\",\n\t\t},\n\t\t{\n\t\t\t2,\n\t\t\t\"if foo; then # inline1\\nbar # inline2\\n# withfi\\nfi\",\n\t\t\t\"if foo; then # inline1\\n  bar        # inline2\\n# withfi\\nfi\",\n\t\t},\n\t\t{\n\t\t\t2,\n\t\t\t\"array=('one'\\n    # 'two'\\n    'three')\",\n\t\t\t\"array=('one'\\n  # 'two'\\n  'three')\",\n\t\t},\n\t}\n\n\tparser := NewParser(KeepComments(true))\n\tfor _, tc := range spaceFormats {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprinter := NewPrinter(Indent(tc.spaces))\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nvar errBadWriter = fmt.Errorf(\"write: expected error\")\n\ntype badWriter struct{}\n\nfunc (b badWriter) Write(p []byte) (int, error) { return 0, errBadWriter }\n\nfunc TestWriteErr(t *testing.T) {\n\tt.Parallel()\n\tf := &File{Stmts: []*Stmt{\n\t\t{\n\t\t\tRedirs: []*Redirect{{\n\t\t\t\tOp:   RdrOut,\n\t\t\t\tWord: litWord(\"foo\"),\n\t\t\t}},\n\t\t\tCmd: &Subshell{},\n\t\t},\n\t}}\n\terr := NewPrinter().Print(badWriter{}, f)\n\tif err == nil {\n\t\tt.Fatalf(\"Expected error with bad writer\")\n\t}\n\tif err != errBadWriter {\n\t\tt.Fatalf(\"Error mismatch with bad writer:\\nwant: %v\\ngot:  %v\",\n\t\t\terrBadWriter, err)\n\t}\n}\n\nfunc TestPrintBinaryNextLine(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]printCase{\n\t\t{\n\t\t\t\"foo <<EOF &&\\nl1\\nEOF\\nbar\",\n\t\t\t\"foo <<EOF && bar\\nl1\\nEOF\",\n\t\t},\n\t\tsamePrint(\"a \\\\\\n\\t&& b\"),\n\t\tsamePrint(\"a \\\\\\n\\t&& b\\nc\"),\n\t\t{\n\t\t\t\"{\\n(a \\\\\\n&& b)\\nc\\n}\",\n\t\t\t\"{\\n\\t(a \\\\\\n\\t\\t&& b)\\n\\tc\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"a && b \\\\\\n&& c\",\n\t\t\t\"a && b \\\\\\n\\t&& c\",\n\t\t},\n\t\t{\n\t\t\t\"a \\\\\\n&& $(b) && c \\\\\\n&& d\",\n\t\t\t\"a \\\\\\n\\t&& $(b) && c \\\\\\n\\t&& d\",\n\t\t},\n\t\t{\n\t\t\t\"a \\\\\\n&& b\\nc \\\\\\n&& d\",\n\t\t\t\"a \\\\\\n\\t&& b\\nc \\\\\\n\\t&& d\",\n\t\t},\n\t\t{\n\t\t\t\"a | {\\nb \\\\\\n| c\\n}\",\n\t\t\t\"a | {\\n\\tb \\\\\\n\\t\\t| c\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"a \\\\\\n\\t&& if foo; then\\nbar\\nfi\",\n\t\t\t\"a \\\\\\n\\t&& if foo; then\\n\\t\\tbar\\n\\tfi\",\n\t\t},\n\t\t{\n\t\t\t\"if foo \\\\\\n&& bar\\nthen\\nbar\\nfi\",\n\t\t\t\"if foo \\\\\\n\\t&& bar; then\\n\\tbar\\nfi\",\n\t\t},\n\t\t{\n\t\t\t\"a |\\nb |\\nc\",\n\t\t\t\"a \\\\\\n\\t| b \\\\\\n\\t| c\",\n\t\t},\n\t\t{\n\t\t\t\"foo |\\n# misplaced\\nbar\",\n\t\t\t\"foo \\\\\\n\\t|\\n\\t# misplaced\\n\\tbar\",\n\t\t},\n\t\tsamePrint(\"{\\n\\tfoo\\n\\t#a\\n\\tbar\\n} | etc\"),\n\t\t{\n\t\t\t\"foo &&\\n#a1\\n#a2\\n$(bar)\",\n\t\t\t\"foo \\\\\\n\\t&&\\n\\t#a1\\n\\t#a2\\n\\t$(bar)\",\n\t\t},\n\t\t{\n\t\t\t\"{\\n\\tfoo\\n\\t#a\\n} |\\n# misplaced\\nbar\",\n\t\t\t\"{\\n\\tfoo\\n\\t#a\\n} \\\\\\n\\t|\\n\\t# misplaced\\n\\tbar\",\n\t\t},\n\t\tsamePrint(\"foo | bar\\n#after\"),\n\t\t{\n\t\t\t\"a |\\nb | #c2\\nc\",\n\t\t\t\"a \\\\\\n\\t| b \\\\\\n\\t|\\n\\t#c2\\n\\tc\",\n\t\t},\n\t\tsamePrint(\"a \\\\\\n\\t&\"),\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter(BinaryNextLine(true))\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestPrintSwitchCaseIndent(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]printCase{\n\t\t{\n\t\t\t\"case $i in\\n1)\\nfoo\\n;;\\nesac\",\n\t\t\t\"case $i in\\n\\t1)\\n\\t\\tfoo\\n\\t\\t;;\\nesac\",\n\t\t},\n\t\t{\n\t\t\t\"case $i in\\n1)\\na\\n;;\\n2)\\nb\\n;;\\nesac\",\n\t\t\t\"case $i in\\n\\t1)\\n\\t\\ta\\n\\t\\t;;\\n\\t2)\\n\\t\\tb\\n\\t\\t;;\\nesac\",\n\t\t},\n\t\tsamePrint(\"case $i in\\n\\t#foo\\nesac\"),\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter(SwitchCaseIndent(true))\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestPrintFunctionNextLine(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]printCase{\n\t\t{\n\t\t\t\"foo() { bar; }\",\n\t\t\t\"foo()\\n{\\n\\tbar\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"foo()\\n{ bar; }\",\n\t\t\t\"foo()\\n{\\n\\tbar\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"foo()\\n\\n{\\n\\n\\tbar\\n}\",\n\t\t\t\"foo()\\n{\\n\\n\\tbar\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"function foo {\\n\\tbar\\n}\",\n\t\t\t\"function foo\\n{\\n\\tbar\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"function foo() {\\n\\tbar\\n}\",\n\t\t\t\"function foo()\\n{\\n\\tbar\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"{ foo() { bar; }; }\",\n\t\t\t\"{\\n\\tfoo()\\n\\t{\\n\\t\\tbar\\n\\t}\\n}\",\n\t\t},\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter(FunctionNextLine(true))\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestPrintSpaceRedirects(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]printCase{\n\t\tsamePrint(\"echo foo bar > f\"),\n\t\tsamePrint(\"echo > f foo bar\"),\n\t\tsamePrint(\"echo >(cmd)\"),\n\t\tsamePrint(\"echo > >(cmd)\"),\n\t\tsamePrint(\"<< EOF\\nfoo\\nEOF\"),\n\t\tsamePrint(\"<<- EOF\\n\\t$(< foo)\\nEOF\"),\n\t\tsamePrint(\"echo 2> f\"),\n\t\tsamePrint(\"echo foo bar >&1\"),\n\t\tsamePrint(\"echo 2<&1 foo bar\"),\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter(SpaceRedirects(true))\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestPrintKeepPadding(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]printCase{\n\t\tsamePrint(\"echo foo bar\"),\n\t\tsamePrint(\"echo  foo   bar\"),\n\t\tsamePrint(\"a=b  c=d   bar\"),\n\t\tsamePrint(\"echo foo    >bar\"),\n\t\tsamePrint(\"echo foo    2>bar\"),\n\t\tsamePrint(\"{ foo;  }\"),\n\t\tsamePrint(\"a()   { foo; }\"),\n\t\tsamePrint(\"a   && b\"),\n\t\tsamePrint(\"a   | b\"),\n\t\tsamePrint(\"a |  b\"),\n\t\tsamePrint(\"{  a b c; }\"),\n\t\tsamePrint(\"foo    # x\\nbaaar  # y\"),\n\t\tsamePrint(\"{ { a; }; }\"),\n\t\tsamePrint(\"{  a;  }\"),\n\t\tsamePrint(\"(  a   )\"),\n\t\tsamePrint(\"'foo\\nbar'   # x\"),\n\t\t{\"\\tfoo\", \"foo\"},\n\t\t{\"  if foo; then bar; fi\", \"if   foo; then bar; fi\"},\n\t\tsamePrint(\"echo '★'  || true\"),\n\t\t{\n\t\t\t\"1234 || { x; y; }\",\n\t\t\t\"1234 || {\\n\\tx\\n\\ty\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"array=('one'\\n\\t\\t# 'two'\\n\\t\\t'three')\",\n\t\t\t\"array=('one'\\n\\t# 'two'\\n\\t'three')\",\n\t\t},\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter(KeepPadding(true))\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\t// ensure that Reset does properly reset colCounter\n\t\t\tprinter.w.WriteByte('x')\n\t\t\tprinter.w.Reset(nil)\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestPrintKeepPaddingSpaces(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]printCase{\n\t\tsamePrint(\"array=('one'\\n        # 'two'\\n        'three')\"),\n\t\tsamePrint(\"    abc=123\"),\n\t\tsamePrint(\"foo \\\\\\n  bar \\\\\\n    baz\"),\n\t\tsamePrint(\"{\\n  foo\\n    bar\\n}\"),\n\t\tsamePrint(\"# foo\\n  # bar\"),\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter(KeepPadding(true), Indent(2))\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestPrintMinify(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]printCase{\n\t\tsamePrint(\"echo foo bar $a $(b)\"),\n\t\t{\n\t\t\t\"#comment\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"foo #comment\",\n\t\t\t\"foo\",\n\t\t},\n\t\t{\n\t\t\t\"foo\\n\\nbar\",\n\t\t\t\"foo\\nbar\",\n\t\t},\n\t\t{\n\t\t\t\"foo &\",\n\t\t\t\"foo&\",\n\t\t},\n\t\tsamePrint(\"foo >bar 2>baz <etc\"),\n\t\t{\n\t\t\t\"{\\n\\tfoo\\n}\",\n\t\t\t\"{\\nfoo\\n}\",\n\t\t},\n\t\t{\n\t\t\t\"(\\n\\ta\\n)\\n(\\n\\tb\\n\\tc\\n)\",\n\t\t\t\"(a)\\n(b\\nc)\",\n\t\t},\n\t\t{\n\t\t\t\"$(\\n\\ta\\n)\\n$(\\n\\tb\\n\\tc\\n)\",\n\t\t\t\"$(a)\\n$(b\\nc)\",\n\t\t},\n\t\t{\n\t\t\t\"f() { x; }\",\n\t\t\t\"f(){ x;}\",\n\t\t},\n\t\t{\n\t\t\t\"((1 + 2))\",\n\t\t\t\"((1+2))\",\n\t\t},\n\t\t// TODO: see https://github.com/mvdan/sh/issues/1169\n\t\t{\n\t\t\t\"((\\nfoo\\n))\",\n\t\t\t\"((\\\\\\nfoo))\",\n\t\t},\n\t\t{\n\t\t\t\"((\\na > 0\\n&&\\na < 10\\n))\",\n\t\t\t\"((\\\\\\na>0&& \\\\\\na<10))\",\n\t\t},\n\t\t{\n\t\t\t\"((a > 0 &&\\na < 10))\",\n\t\t\t\"((a>0&& \\\\\\na<10))\",\n\t\t},\n\t\t{\n\t\t\t\"echo $a ${b} ${c}-d ${e}f ${g}_h\",\n\t\t\t\"echo $a $b $c-d ${e}f ${g}_h\",\n\t\t},\n\t\t{\n\t\t\t\"echo ${0} ${3} ${10} ${22}\",\n\t\t\t\"echo $0 $3 ${10} ${22}\",\n\t\t},\n\t\t{\n\t\t\t\"case $a in\\nx) c ;;\\ny | z)\\n\\td\\n\\t;;\\nesac\",\n\t\t\t\"case $a in\\nx)c;;\\ny|z)d\\nesac\",\n\t\t},\n\t\t{\n\t\t\t\"a && b | c\",\n\t\t\t\"a&&b|c\",\n\t\t},\n\t\t{\n\t\t\t\"a &&\\n\\tb |\\n\\tc\",\n\t\t\t\"a&&b|c\",\n\t\t},\n\t\t{\n\t\t\t\"${0/${a}\\\\\\n}\",\n\t\t\t\"${0/$a/}\",\n\t\t},\n\t\t{\n\t\t\t\"#!/bin/sh\\necho 1\\n#!/bin/sh\\necho 2\",\n\t\t\t\"#!/bin/sh\\necho 1\\necho 2\",\n\t\t},\n\t\tsamePrint(\"foo >bar 2>baz <etc\"),\n\t\tsamePrint(\"<<-EOF\\n$(a|b)\\nEOF\"),\n\t\t{\n\t\t\t\"a=$(\\n\\tcat <<'EOF'\\n  hello\\nEOF\\n)\",\n\t\t\t\"a=$(cat <<'EOF'\\n  hello\\nEOF\\n)\",\n\t\t},\n\t\t{\n\t\t\t\"(\\n\\tcat <<EOF\\n hello\\nEOF\\n)\",\n\t\t\t\"(cat <<EOF\\n hello\\nEOF\\n)\",\n\t\t},\n\t\tsamePrint(\"diff -y <(cat <<EOF\\n1\\n2\\n3\\nEOF\\n) <(cat <<EOF\\n1\\n4\\n3\\nEOF\\n)\"),\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter(Minify(true))\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestPrintSingleLine(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]printCase{\n\t\tsamePrint(\"echo foo bar $a $(b)\"),\n\t\tsamePrint(\"foo #comment\"),\n\t\t{\n\t\t\t\"foo\\n\\nbar\",\n\t\t\t\"foo; bar\",\n\t\t},\n\t\tsamePrint(\"foo &\"),\n\t\tsamePrint(\"foo >bar 2>baz <etc\"),\n\t\t{\n\t\t\t\"{\\n\\tfoo\\n}\",\n\t\t\t\"{ foo; }\",\n\t\t},\n\t\t{\n\t\t\t\"(\\n\\ta\\n)\\n(\\n\\tb\\n\\tc\\n)\",\n\t\t\t\"(a); (b; c)\",\n\t\t},\n\t\t{\n\t\t\t\"$(\\n\\ta\\n)\\n$(\\n\\tb\\n\\tc\\n)\",\n\t\t\t\"$(a); $(b; c)\",\n\t\t},\n\t\tsamePrint(\"f() { x; }\"),\n\t\tsamePrint(\"((1 + 2))\"),\n\t\tsamePrint(\"echo $a ${b} ${c}-d ${e}f ${g}_h\"),\n\t\tsamePrint(\"echo ${0} ${3} ${10} ${22}\"),\n\t\t{\n\t\t\t\"case $a in\\nx)c;;\\ny|z)d\\nesac\",\n\t\t\t\"case $a in x) c ;; y | z) d ;; esac\",\n\t\t},\n\t\tsamePrint(\"a && b | c\"),\n\t\t{\n\t\t\t\"a &&\\n\\tb |\\n\\tc\",\n\t\t\t\"a && b | c\",\n\t\t},\n\t\t{\n\t\t\t\"if\\nfoo\\nthen\\nbar\\nfi\",\n\t\t\t\"if foo; then bar; fi\",\n\t\t},\n\t\t{\n\t\t\t\"a \\\\\\n >b\",\n\t\t\t\"a >b\",\n\t\t},\n\t\tsamePrint(\"foo >bar 2>baz <etc\"),\n\t\tsamePrint(\"<<-EOF\\n\\t$(a | b)\\nEOF\"),\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter(SingleLine(true))\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprintTest(t, parser, printer, tc.in, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestPrintOptionsNotBroken(t *testing.T) {\n\tt.Parallel()\n\tsingleLineException := regexp.MustCompile(`#|<<|'|\"`)\n\tcheckSingleLine := func(t *testing.T, got string) {\n\t\tif singleLineException.MatchString(got) {\n\t\t\treturn\n\t\t}\n\t\tgot = strings.TrimSuffix(got, \"\\n\") // trailing newline is expected\n\t\tif strings.Contains(got, \"\\n\") {\n\t\t\tt.Fatalf(\"unexpected newline with SingleLine: %q\", got)\n\t\t}\n\t}\n\n\tfor _, opts := range []struct {\n\t\tname string\n\t\tlist []PrinterOption\n\t}{\n\t\t{\"Minify\", []PrinterOption{Minify(true)}},\n\t\t{\"SingleLine\", []PrinterOption{SingleLine(true)}},\n\t} {\n\t\tprinter := NewPrinter(opts.list...)\n\t\tfor lang := range langResolvedVariants.bits() {\n\t\t\tparser := NewParser(Variant(lang), KeepComments(true))\n\t\t\tfor _, tc := range append(fileTests, fileTestsNoPrint...) {\n\t\t\t\twant, _ := tc.byLangIndex[lang.index()].(*File)\n\t\t\t\tif want == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tt.Run(fmt.Sprintf(\"File%s/%s\", opts.name, lang), func(t *testing.T) {\n\t\t\t\t\tin := tc.inputs[0]\n\t\t\t\t\tt.Logf(\"input: %s\", in)\n\t\t\t\t\tprog, err := parser.Parse(strings.NewReader(in), \"\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Skipf(\"skipping due to error; TestParsePass should catch this\")\n\t\t\t\t\t}\n\t\t\t\t\tgot, err := strPrint(printer, prog)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tif opts.name == \"SingleLine\" {\n\t\t\t\t\t\tcheckSingleLine(t, got)\n\t\t\t\t\t}\n\t\t\t\t\t_, err = parser.Parse(strings.NewReader(got), \"\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"program was broken: %v\\noriginal:\\n%s\\nfinal:\\n%s\", err, in, got)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tparser := NewParser(KeepComments(true))\n\t\tfor _, tc := range printTests {\n\t\t\tt.Run(fmt.Sprintf(\"Print%s\", opts.name), func(t *testing.T) {\n\t\t\t\tprog, err := parser.Parse(strings.NewReader(tc.in), \"\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Skipf(\"skipping due to error; TestPrintTable should catch this\")\n\t\t\t\t}\n\t\t\t\tgot, err := strPrint(printer, prog)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tif opts.name == \"SingleLine\" {\n\t\t\t\t\tcheckSingleLine(t, got)\n\t\t\t\t}\n\t\t\t\t_, err = parser.Parse(strings.NewReader(got), \"\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"program was broken: %v\\noriginal:\\n%s\\nfinal:\\n%s\", err, tc.in, got)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc printTest(t *testing.T, parser *Parser, printer *Printer, in, want string) {\n\tt.Helper()\n\tt.Logf(\"input: %s\", in)\n\tprog, err := parser.Parse(strings.NewReader(in), \"\")\n\tif err != nil {\n\t\tt.Fatalf(\"parsing got an error: %s:\\n%s\", err, in)\n\t}\n\torigWant := want\n\twant += \"\\n\"\n\tgot, err := strPrint(printer, prog)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif got != want {\n\t\tt.Fatalf(\"Print mismatch:\\nwant:\\n%q\\ngot:\\n%q\", want, got)\n\t}\n\n\t// With the original \"want\" output string,\n\t// make sure that it's idempotent when formatted again.\n\t// Note that we don't want the added newline,\n\t// as that can change the meaning of trailing backslashes.\n\tprogAgain, err := parser.Parse(strings.NewReader(origWant), \"\")\n\tif err != nil {\n\t\tt.Fatalf(\"Result is not valid shell:\\n%s\", want)\n\t}\n\tgotAgain, err := strPrint(printer, progAgain)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif gotAgain != want {\n\t\tt.Fatalf(\"Re-print mismatch:\\nwant:\\n%q\\ngot:\\n%q\", want, gotAgain)\n\t}\n}\n\nfunc TestPrintNodeTypes(t *testing.T) {\n\tt.Parallel()\n\n\tmultiline, err := NewParser().Parse(strings.NewReader(`\n\t\techo foo\n\t`), \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := [...]struct {\n\t\tin      Node\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tin:   &File{Stmts: litStmts(\"foo\")},\n\t\t\twant: \"foo\\n\",\n\t\t},\n\t\t{\n\t\t\tin:   &File{Stmts: litStmts(\"foo\", \"bar\")},\n\t\t\twant: \"foo\\nbar\\n\",\n\t\t},\n\t\t{\n\t\t\tin:   litStmt(\"foo\", \"bar\"),\n\t\t\twant: \"foo bar\",\n\t\t},\n\t\t{\n\t\t\tin:   litCall(\"foo\", \"bar\"),\n\t\t\twant: \"foo bar\",\n\t\t},\n\t\t{\n\t\t\tin:   litWord(\"foo\"),\n\t\t\twant: \"foo\",\n\t\t},\n\t\t{\n\t\t\tin:   lit(\"foo\"),\n\t\t\twant: \"foo\",\n\t\t},\n\t\t{\n\t\t\tin:   sglQuoted(\"foo\"),\n\t\t\twant: \"'foo'\",\n\t\t},\n\t\t{\n\t\t\tin:      &Comment{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tin:   multiline.Stmts[0],\n\t\t\twant: \"echo foo\",\n\t\t},\n\t\t{\n\t\t\tin:   multiline.Stmts[0].Cmd,\n\t\t\twant: \"echo foo\",\n\t\t},\n\t\t{\n\t\t\tin:   multiline.Stmts[0].Cmd.(*CallExpr).Args[0],\n\t\t\twant: \"echo\",\n\t\t},\n\t\t{\n\t\t\tin:   multiline.Stmts[0].Cmd.(*CallExpr).Args[0].Parts[0],\n\t\t\twant: \"echo\",\n\t\t},\n\t}\n\tprinter := NewPrinter()\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tgot, err := strPrint(printer, tc.in)\n\t\t\tif err == nil && tc.wantErr {\n\t\t\t\tt.Fatalf(\"wanted an error but found none\")\n\t\t\t} else if err != nil && !tc.wantErr {\n\t\t\t\tt.Fatalf(\"didn't want an error but got %v\", err)\n\t\t\t}\n\t\t\tif got != tc.want {\n\t\t\t\tt.Fatalf(\"Print mismatch:\\nwant:\\n%s\\ngot:\\n%s\",\n\t\t\t\t\ttc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPrintManyStmts(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]struct {\n\t\tin, want string\n\t}{\n\t\t{\"foo; bar\", \"foo\\nbar\\n\"},\n\t\t{\"foo\\nbar\", \"foo\\nbar\\n\"},\n\t\t{\"\\n\\nfoo\\nbar\\n\\n\", \"foo\\nbar\\n\"},\n\t\t{\"foo\\nbar <<EOF\\nbody\\nEOF\\n\", \"foo\\nbar <<EOF\\nbody\\nEOF\\n\"},\n\t\t{\"foo\\nbar # inline\", \"foo\\nbar # inline\\n\"},\n\t\t{\"# comment before\\nfoo bar\", \"# comment before\\nfoo bar\\n\"},\n\t}\n\tparser := NewParser(KeepComments(true))\n\tprinter := NewPrinter()\n\tfor _, tc := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tf, err := parser.Parse(strings.NewReader(tc.in), \"\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tvar buf bytes.Buffer\n\t\t\tfor _, stmt := range f.Stmts {\n\t\t\t\tprinter.Print(&buf, stmt)\n\t\t\t\tbuf.WriteByte('\\n')\n\t\t\t}\n\t\t\tgot := buf.String()\n\t\t\tif got != tc.want {\n\t\t\t\tt.Fatalf(\"Print mismatch:\\nwant:\\n%s\\ngot:\\n%s\",\n\t\t\t\t\ttc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestKeepPaddingRepeated(t *testing.T) {\n\tt.Parallel()\n\tparser := NewParser()\n\tprinter := NewPrinter()\n\n\t// Enable the KeepPadding option twice. This used to crash, since the\n\t// option made an invalid type assertion the second time.\n\tKeepPadding(true)(printer)\n\tKeepPadding(true)(printer)\n\n\t// Ensure the option is enabled.\n\tprintTest(t, parser, printer, \"foo  bar\", \"foo  bar\")\n\n\t// Disable the option, and ensure it's disabled.\n\tKeepPadding(false)(printer)\n\tprintTest(t, parser, printer, \"foo  bar\", \"foo bar\")\n}\n"
  },
  {
    "path": "syntax/quote.go",
    "content": "// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\ntype QuoteError struct {\n\tByteOffset int\n\tMessage    string\n}\n\nfunc (e QuoteError) Error() string {\n\treturn fmt.Sprintf(\"cannot quote character at byte %d: %s\", e.ByteOffset, e.Message)\n}\n\nconst (\n\tquoteErrNull  = \"shell strings cannot contain null bytes\"\n\tquoteErrPOSIX = \"POSIX shell lacks escape sequences\"\n\tquoteErrRange = \"rune out of range\"\n\tquoteErrMksh  = \"mksh cannot escape codepoints above 16 bits\"\n)\n\n// Quote returns a quoted version of the input string,\n// so that the quoted version is expanded or interpreted\n// as the original string in the given language variant.\n//\n// Quoting is necessary when using arbitrary literal strings\n// as words in a shell script or command.\n// Without quoting, one can run into syntax errors,\n// as well as the possibility of running unintended code.\n//\n// An error is returned when a string cannot be quoted for a variant.\n// For instance, POSIX lacks escape sequences for non-printable characters,\n// and no language variant can represent a string containing null bytes.\n// In such cases, the returned error type will be *QuoteError.\n//\n// The quoting strategy is chosen on a best-effort basis,\n// to minimize the amount of extra bytes necessary.\n//\n// Some strings do not require any quoting and are returned unchanged.\n// Those strings can be directly surrounded in single quotes as well.\nfunc Quote(s string, lang LangVariant) (string, error) {\n\tif s == \"\" {\n\t\t// Special case; an empty string must always be quoted,\n\t\t// as otherwise it expands to zero fields.\n\t\treturn \"''\", nil\n\t}\n\tshellChars := false\n\tnonPrintable := false\n\toffs := 0\n\tfor rem := s; len(rem) > 0; {\n\t\tr, size := utf8.DecodeRuneInString(rem)\n\t\tswitch r {\n\t\t// Like regOps; token characters.\n\t\tcase ';', '\"', '\\'', '(', ')', '$', '|', '&', '>', '<', '`',\n\t\t\t// Whitespace; might result in multiple fields.\n\t\t\t' ', '\\t', '\\r', '\\n',\n\t\t\t// Escape sequences would be expanded.\n\t\t\t'\\\\',\n\t\t\t// Would start a comment unless quoted.\n\t\t\t'#',\n\t\t\t// Might result in brace expansion.\n\t\t\t'{',\n\t\t\t// Might result in tilde expansion.\n\t\t\t'~',\n\t\t\t// Might result in globbing.\n\t\t\t'*', '?', '[',\n\t\t\t// Might result in an assignment.\n\t\t\t'=':\n\t\t\tshellChars = true\n\t\tcase '\\x00':\n\t\t\treturn \"\", &QuoteError{ByteOffset: offs, Message: quoteErrNull}\n\t\t}\n\t\tif r == utf8.RuneError || !unicode.IsPrint(r) {\n\t\t\tif lang.in(LangPOSIX) {\n\t\t\t\treturn \"\", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX}\n\t\t\t}\n\t\t\tnonPrintable = true\n\t\t}\n\t\trem = rem[size:]\n\t\toffs += size\n\t}\n\tif !shellChars && !nonPrintable && !IsKeyword(s) {\n\t\t// Nothing to quote; avoid allocating.\n\t\treturn s, nil\n\t}\n\n\t// Single quotes are usually best,\n\t// as they don't require any escaping of characters.\n\t// If we have any invalid utf8 or non-printable runes,\n\t// use $'' so that we can escape them.\n\t// Note that we can't use double quotes for those.\n\tvar b strings.Builder\n\tif nonPrintable {\n\t\tb.WriteString(\"$'\")\n\t\tlastRequoteIfHex := false\n\t\toffs := 0\n\t\tfor rem := s; len(rem) > 0; {\n\t\t\tnextRequoteIfHex := false\n\t\t\tr, size := utf8.DecodeRuneInString(rem)\n\t\t\tswitch {\n\t\t\tcase r == '\\'', r == '\\\\':\n\t\t\t\tb.WriteByte('\\\\')\n\t\t\t\tb.WriteRune(r)\n\t\t\tcase unicode.IsPrint(r) && r != utf8.RuneError:\n\t\t\t\tif lastRequoteIfHex && isHex(r) {\n\t\t\t\t\tb.WriteString(\"'$'\")\n\t\t\t\t}\n\t\t\t\tb.WriteRune(r)\n\t\t\tcase r == '\\a':\n\t\t\t\tb.WriteString(`\\a`)\n\t\t\tcase r == '\\b':\n\t\t\t\tb.WriteString(`\\b`)\n\t\t\tcase r == '\\f':\n\t\t\t\tb.WriteString(`\\f`)\n\t\t\tcase r == '\\n':\n\t\t\t\tb.WriteString(`\\n`)\n\t\t\tcase r == '\\r':\n\t\t\t\tb.WriteString(`\\r`)\n\t\t\tcase r == '\\t':\n\t\t\t\tb.WriteString(`\\t`)\n\t\t\tcase r == '\\v':\n\t\t\t\tb.WriteString(`\\v`)\n\t\t\tcase r < utf8.RuneSelf, r == utf8.RuneError && size == 1:\n\t\t\t\t// \\xXX, fixed at two hexadecimal characters.\n\t\t\t\tfmt.Fprintf(&b, \"\\\\x%02x\", rem[0])\n\t\t\t\t// Unfortunately, mksh allows \\x to consume more hex characters.\n\t\t\t\t// Ensure that we don't allow it to read more than two.\n\t\t\t\tif lang.in(LangMirBSDKorn) {\n\t\t\t\t\tnextRequoteIfHex = true\n\t\t\t\t}\n\t\t\tcase r > utf8.MaxRune:\n\t\t\t\t// Not a valid Unicode code point?\n\t\t\t\treturn \"\", &QuoteError{ByteOffset: offs, Message: quoteErrRange}\n\t\t\tcase lang.in(LangMirBSDKorn) && r > 0xFFFD:\n\t\t\t\t// From the CAVEATS section in R59's man page:\n\t\t\t\t//\n\t\t\t\t// mksh currently uses OPTU-16 internally, which is the same as\n\t\t\t\t// UTF-8 and CESU-8 with 0000..FFFD being valid codepoints.\n\t\t\t\treturn \"\", &QuoteError{ByteOffset: offs, Message: quoteErrMksh}\n\t\t\tcase r < 0x10000:\n\t\t\t\t// \\uXXXX, fixed at four hexadecimal characters.\n\t\t\t\tfmt.Fprintf(&b, \"\\\\u%04x\", r)\n\t\t\tdefault:\n\t\t\t\t// \\UXXXXXXXX, fixed at eight hexadecimal characters.\n\t\t\t\tfmt.Fprintf(&b, \"\\\\U%08x\", r)\n\t\t\t}\n\t\t\trem = rem[size:]\n\t\t\tlastRequoteIfHex = nextRequoteIfHex\n\t\t\toffs += size\n\t\t}\n\t\tb.WriteString(\"'\")\n\t\treturn b.String(), nil\n\t}\n\n\t// Single quotes without any need for escaping.\n\tif !strings.Contains(s, \"'\") {\n\t\treturn \"'\" + s + \"'\", nil\n\t}\n\n\t// The string contains single quotes,\n\t// so fall back to double quotes.\n\tb.WriteByte('\"')\n\tfor _, r := range s {\n\t\tswitch r {\n\t\tcase '\"', '\\\\', '`', '$':\n\t\t\tb.WriteByte('\\\\')\n\t\t}\n\t\tb.WriteRune(r)\n\t}\n\tb.WriteByte('\"')\n\treturn b.String(), nil\n}\n\nfunc isHex(r rune) bool {\n\treturn (r >= '0' && r <= '9') ||\n\t\t(r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')\n}\n"
  },
  {
    "path": "syntax/quote_test.go",
    "content": "// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-quicktest/qt\"\n)\n\nfunc TestQuote(t *testing.T) {\n\tt.Parallel()\n\ttests := [...]struct {\n\t\tstr  string\n\t\tlang LangVariant\n\t\twant any\n\t}{\n\t\t{\"\", LangBash, `''`},\n\t\t{\"\\a\", LangBash, `$'\\a'`},\n\t\t{\"\\b\", LangBash, `$'\\b'`},\n\t\t{\"\\f\", LangBash, `$'\\f'`},\n\t\t{\"\\n\", LangBash, `$'\\n'`},\n\t\t{\"\\r\", LangBash, `$'\\r'`},\n\t\t{\"\\t\", LangBash, `$'\\t'`},\n\t\t{\"\\v\", LangBash, `$'\\v'`},\n\t\t{\"null\\x00\", LangBash, &QuoteError{4, quoteErrNull}},\n\t\t{\"posix\\x1b\", LangPOSIX, &QuoteError{5, quoteErrPOSIX}},\n\t\t{\"posix\\n\", LangPOSIX, &QuoteError{5, quoteErrPOSIX}},\n\t\t{\"mksh16\\U00086199\", LangMirBSDKorn, &QuoteError{6, quoteErrMksh}},\n\t\t{\"\\x1b\\x1caaa\", LangBash, `$'\\x1b\\x1caaa'`},\n\t\t{\"\\x1b\\x1caaa\", LangMirBSDKorn, `$'\\x1b\\x1c'$'aaa'`},\n\t\t{\"\\xff\\x00\", LangBash, &QuoteError{1, quoteErrNull}},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot, gotErr := Quote(test.str, test.lang)\n\t\t\tswitch want := test.want.(type) {\n\t\t\tcase string:\n\t\t\t\tqt.Assert(t, qt.Equals(got, want))\n\t\t\t\tqt.Assert(t, qt.IsNil(gotErr))\n\t\t\tcase *QuoteError:\n\t\t\t\tqt.Assert(t, qt.Equals(got, \"\"))\n\t\t\t\tqt.Assert(t, qt.DeepEquals(gotErr, error(want)))\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unexpected type: %T\", want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "syntax/simplify.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport \"strings\"\n\n// Simplify modifies a node to remove redundant pieces of syntax, and returns\n// whether any changes were made.\n//\n// The changes currently applied are:\n//\n//\tRemove clearly useless parentheses       $(( (expr) ))\n//\tRemove dollars from vars in exprs        (($var))\n//\tRemove duplicate subshells               $( (stmts) )\n//\tRemove redundant quotes                  [[ \"$var\" == str ]]\n//\tMerge negations with unary operators     [[ ! -n $var ]]\n//\tUse single quotes to shorten literals    \"\\$foo\"\nfunc Simplify(n Node) bool {\n\ts := simplifier{}\n\tWalk(n, s.visit)\n\treturn s.modified\n}\n\ntype simplifier struct {\n\tmodified bool\n}\n\nfunc (s *simplifier) visit(node Node) bool {\n\tswitch node := node.(type) {\n\tcase *Assign:\n\t\tnode.Index = s.removeParensArithm(node.Index)\n\t\t// Don't inline params, as x[i] and x[$i] mean\n\t\t// different things when x is an associative\n\t\t// array; the first means \"i\", the second \"$i\".\n\tcase *ParamExp:\n\t\tnode.Index = s.removeParensArithm(node.Index)\n\t\t// don't inline params - same as above.\n\n\t\tif node.Slice == nil {\n\t\t\tbreak\n\t\t}\n\t\tnode.Slice.Offset = s.removeParensArithm(node.Slice.Offset)\n\t\tnode.Slice.Offset = s.inlineSimpleParams(node.Slice.Offset)\n\t\tnode.Slice.Length = s.removeParensArithm(node.Slice.Length)\n\t\tnode.Slice.Length = s.inlineSimpleParams(node.Slice.Length)\n\tcase *ArithmExp:\n\t\tnode.X = s.removeParensArithm(node.X)\n\t\tnode.X = s.inlineSimpleParams(node.X)\n\tcase *ArithmCmd:\n\t\tnode.X = s.removeParensArithm(node.X)\n\t\tnode.X = s.inlineSimpleParams(node.X)\n\tcase *ParenArithm:\n\t\tnode.X = s.removeParensArithm(node.X)\n\t\tnode.X = s.inlineSimpleParams(node.X)\n\tcase *BinaryArithm:\n\t\tnode.X = s.inlineSimpleParams(node.X)\n\t\tnode.Y = s.inlineSimpleParams(node.Y)\n\tcase *CmdSubst:\n\t\tnode.Stmts = s.inlineSubshell(node.Stmts)\n\tcase *Subshell:\n\t\tnode.Stmts = s.inlineSubshell(node.Stmts)\n\tcase *Word:\n\t\tnode.Parts = s.simplifyWord(node.Parts)\n\tcase *TestClause:\n\t\tnode.X = s.removeParensTest(node.X)\n\t\tnode.X = s.removeNegateTest(node.X)\n\tcase *ParenTest:\n\t\tnode.X = s.removeParensTest(node.X)\n\t\tnode.X = s.removeNegateTest(node.X)\n\tcase *BinaryTest:\n\t\tnode.X = s.unquoteParams(node.X)\n\t\tnode.X = s.removeNegateTest(node.X)\n\t\tif node.Op == TsMatchShort {\n\t\t\ts.modified = true\n\t\t\tnode.Op = TsMatch\n\t\t}\n\t\tswitch node.Op {\n\t\tcase TsMatch, TsNoMatch:\n\t\t\t// unquoting enables globbing\n\t\tdefault:\n\t\t\tnode.Y = s.unquoteParams(node.Y)\n\t\t}\n\t\tnode.Y = s.removeNegateTest(node.Y)\n\tcase *UnaryTest:\n\t\tnode.X = s.unquoteParams(node.X)\n\t}\n\treturn true\n}\n\nfunc (s *simplifier) simplifyWord(wps []WordPart) []WordPart {\nparts:\n\tfor i, wp := range wps {\n\t\tdq, _ := wp.(*DblQuoted)\n\t\tif dq == nil || len(dq.Parts) != 1 {\n\t\t\tbreak\n\t\t}\n\t\tlit, _ := dq.Parts[0].(*Lit)\n\t\tif lit == nil {\n\t\t\tbreak\n\t\t}\n\t\tvar sb strings.Builder\n\t\tescaped := false\n\t\tfor _, r := range lit.Value {\n\t\t\tswitch r {\n\t\t\tcase '\\\\':\n\t\t\t\tescaped = !escaped\n\t\t\t\tif escaped {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase '\\'':\n\t\t\t\tcontinue parts\n\t\t\tcase '$', '\"', '`':\n\t\t\t\tescaped = false\n\t\t\tdefault:\n\t\t\t\tif escaped {\n\t\t\t\t\tcontinue parts\n\t\t\t\t}\n\t\t\t\tescaped = false\n\t\t\t}\n\t\t\tsb.WriteRune(r)\n\t\t}\n\t\tnewVal := sb.String()\n\t\tif newVal == lit.Value {\n\t\t\tbreak\n\t\t}\n\t\ts.modified = true\n\t\twps[i] = &SglQuoted{\n\t\t\tLeft:   dq.Pos(),\n\t\t\tRight:  dq.End(),\n\t\t\tDollar: dq.Dollar,\n\t\t\tValue:  newVal,\n\t\t}\n\t}\n\treturn wps\n}\n\nfunc (s *simplifier) removeParensArithm(x ArithmExpr) ArithmExpr {\n\tfor {\n\t\tpar, _ := x.(*ParenArithm)\n\t\tif par == nil {\n\t\t\treturn x\n\t\t}\n\t\ts.modified = true\n\t\tx = par.X\n\t}\n}\n\nfunc (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr {\n\tw, _ := x.(*Word)\n\tif w == nil || len(w.Parts) != 1 {\n\t\treturn x\n\t}\n\tpe, _ := w.Parts[0].(*ParamExp)\n\tif pe == nil || !ValidName(pe.Param.Value) {\n\t\t// Not a parameter expansion, or not a valid name, like $3.\n\t\treturn x\n\t}\n\tif !pe.simple() {\n\t\t// A complex parameter expansion can't be simplified.\n\t\t//\n\t\t// Note that index expressions can't generally be simplified\n\t\t// either. It's fine to turn ${a[0]} into a[0], but others like\n\t\t// a[*] are invalid in many shells including Bash.\n\t\treturn x\n\t}\n\ts.modified = true\n\treturn &Word{Parts: []WordPart{pe.Param}}\n}\n\nfunc (s *simplifier) inlineSubshell(stmts []*Stmt) []*Stmt {\n\tfor len(stmts) == 1 {\n\t\tst := stmts[0]\n\t\tif st.Negated || st.Background || st.Coprocess || st.Disown ||\n\t\t\tlen(st.Redirs) > 0 {\n\t\t\tbreak\n\t\t}\n\t\tsub, _ := st.Cmd.(*Subshell)\n\t\tif sub == nil {\n\t\t\tbreak\n\t\t}\n\t\ts.modified = true\n\t\tstmts = sub.Stmts\n\t}\n\treturn stmts\n}\n\nfunc (s *simplifier) unquoteParams(x TestExpr) TestExpr {\n\tw, _ := x.(*Word)\n\tif w == nil || len(w.Parts) != 1 {\n\t\treturn x\n\t}\n\tdq, _ := w.Parts[0].(*DblQuoted)\n\tif dq == nil || len(dq.Parts) != 1 {\n\t\treturn x\n\t}\n\tif _, ok := dq.Parts[0].(*ParamExp); !ok {\n\t\treturn x\n\t}\n\ts.modified = true\n\tw.Parts = dq.Parts\n\treturn w\n}\n\nfunc (s *simplifier) removeParensTest(x TestExpr) TestExpr {\n\tfor {\n\t\tpar, _ := x.(*ParenTest)\n\t\tif par == nil {\n\t\t\treturn x\n\t\t}\n\t\ts.modified = true\n\t\tx = par.X\n\t}\n}\n\nfunc (s *simplifier) removeNegateTest(x TestExpr) TestExpr {\n\tu, _ := x.(*UnaryTest)\n\tif u == nil || u.Op != TsNot {\n\t\treturn x\n\t}\n\tswitch y := u.X.(type) {\n\tcase *UnaryTest:\n\t\tswitch y.Op {\n\t\tcase TsEmpStr:\n\t\t\ty.Op = TsNempStr\n\t\t\ts.modified = true\n\t\t\treturn y\n\t\tcase TsNempStr:\n\t\t\ty.Op = TsEmpStr\n\t\t\ts.modified = true\n\t\t\treturn y\n\t\tcase TsNot:\n\t\t\ts.modified = true\n\t\t\treturn y.X\n\t\t}\n\tcase *BinaryTest:\n\t\tswitch y.Op {\n\t\tcase TsMatch:\n\t\t\ty.Op = TsNoMatch\n\t\t\ts.modified = true\n\t\t\treturn y\n\t\tcase TsNoMatch:\n\t\t\ty.Op = TsMatch\n\t\t\ts.modified = true\n\t\t\treturn y\n\t\t}\n\t}\n\treturn x\n}\n"
  },
  {
    "path": "syntax/simplify_test.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n)\n\ntype simplifyTest struct {\n\tin, want string\n}\n\nfunc noSimple(in string) simplifyTest {\n\treturn simplifyTest{in: in, want: in}\n}\n\nvar simplifyTests = [...]simplifyTest{\n\t// arithmetic exprs\n\t{\"$((a + ((b - c))))\", \"$((a + (b - c)))\"},\n\t{\"$((a + (((b - c)))))\", \"$((a + (b - c)))\"},\n\t{\"$(((b - c)))\", \"$((b - c))\"},\n\t{\"(((b - c)))\", \"((b - c))\"},\n\t{\"${foo[(1)]}\", \"${foo[1]}\"},\n\t{\"${foo:(1):(2)}\", \"${foo:1:2}\"},\n\t{\"a[(1)]=2\", \"a[1]=2\"},\n\t{\"$(($a + ${b}))\", \"$((a + b))\"},\n\tnoSimple(\"$((${!a} + ${#b}))\"),\n\tnoSimple(\"a[$b]=2\"),\n\tnoSimple(\"${a[$b]}\"),\n\tnoSimple(\"${a[@]}\"),\n\tnoSimple(\"((${a[@]}))\"),\n\tnoSimple(\"((${a[*]}))\"),\n\tnoSimple(\"((${a[0]}))\"),\n\tnoSimple(\"(($3 == $#))\"),\n\n\t// test exprs\n\t{`[[ \"$foo\" == \"bar\" ]]`, `[[ $foo == \"bar\" ]]`},\n\t{`[[ (-z \"$foo\") ]]`, `[[ -z $foo ]]`},\n\t{`[[ \"a b\" > \"$c\" ]]`, `[[ \"a b\" > $c ]]`},\n\t{`[[ ! -n $foo ]]`, `[[ -z $foo ]]`},\n\t{`[[ ! ! -e a && ! -z $b ]]`, `[[ -e a && -n $b ]]`},\n\t{`[[ (! a == b) || (! c != d) ]]`, `[[ (a != b) || (c == d) ]]`},\n\tnoSimple(`[[ -n a$b && -n $c ]]`),\n\tnoSimple(`[[ ! -e foo ]]`),\n\tnoSimple(`[[ foo == bar ]]`),\n\t{`[[ foo = bar ]]`, `[[ foo == bar ]]`},\n\n\t// stmts\n\t{\"$( (sts))\", \"$(sts)\"},\n\t{\"( ( (sts)))\", \"(sts)\"},\n\tnoSimple(\"( (sts) >f)\"),\n\tnoSimple(\"(\\n\\tx\\n\\t(sts)\\n)\"),\n\n\t// strings\n\tnoSimple(`\"foo\"`),\n\tnoSimple(`\"foo$bar\"`),\n\tnoSimple(`\"$bar\"`),\n\tnoSimple(`\"f'o\\\\o\"`),\n\tnoSimple(`\"fo\\'o\"`),\n\tnoSimple(`\"fo\\\\'o\"`),\n\tnoSimple(`\"fo\\no\"`),\n\t{`\"fo\\$o\"`, `'fo$o'`},\n\t{`\"fo\\\"o\"`, `'fo\"o'`},\n\t{\"\\\"fo\\\\`o\\\"\", \"'fo`o'\"},\n\tnoSimple(`fo\"o\"bar`),\n\tnoSimple(`foo\"\"bar`),\n}\n\nfunc TestSimplify(t *testing.T) {\n\tt.Parallel()\n\tparser := NewParser()\n\tprinter := NewPrinter()\n\tfor _, tc := range simplifyTests {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tprog, err := parser.Parse(strings.NewReader(tc.in), \"\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tsimplified := Simplify(prog)\n\t\t\tvar buf bytes.Buffer\n\t\t\tprinter.Print(&buf, prog)\n\t\t\twant := tc.want + \"\\n\"\n\t\t\tif got := buf.String(); got != want {\n\t\t\t\tt.Fatalf(\"Simplify mismatch of %q\\nwant: %q\\ngot:  %q\",\n\t\t\t\t\ttc.in, want, got)\n\t\t\t}\n\t\t\tif simplified && tc.in == tc.want {\n\t\t\t\tt.Fatalf(\"returned true but did not simplify\")\n\t\t\t} else if !simplified && tc.in != tc.want {\n\t\t\t\tt.Fatalf(\"returned false but did simplify\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzParsePrint/293db3718a4ab7a5",
    "content": "go test fuzz v1\nstring(\"A=0(\")\nbyte('\\x00')\nbool(true)\nbool(false)\nbyte('\\x00')\nbool(false)\nbool(false)\nbool(false)\nbool(false)\nbool(false)\nbool(false)\nbool(false)\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzParsePrint/6d0dc226922dc40c",
    "content": "go test fuzz v1\nstring(\"`\\\\$\\\\\\\\000\")\nbyte('\\x02')\nbool(true)\nbool(false)\nbyte('\\x00')\nbool(false)\nbool(false)\nbool(false)\nbool(false)\nbool(false)\nbool(false)\nbool(false)\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzParsePrint/cb6d714b0a2d2315",
    "content": "go test fuzz v1\nstring(\"ec!!ho ${(\")\nbyte('\\x01')\nbool(true)\nbool(false)\nbyte('\\x00')\nbool(false)\nbool(false)\nbool(true)\nbool(false)\nbool(true)\nbool(false)\nbool(false)\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/23cf0175e40438e8033b11cdd1441a2d2893a99144c4ac0f2b5f4caa113c9edd",
    "content": "go test fuzz v1\nstring(\"\\uffff\")\nbyte('\\x02')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/25f36feab4af00bc4dfc3cf56da02b842b62ba8c5ac44862b5b3b776a0d519b4",
    "content": "go test fuzz v1\nstring(\"\\xb3c\")\nbyte('\\x02')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/2788bd30d386289e06a1024a030ad5ab7f363c703bea8a5d035de174491029bf",
    "content": "go test fuzz v1\nstring(\"\\x0fC\")\nbyte('\\x00')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/39d5fdf93d52b2cd50fb9582b27c82d159de0575623865538ced2a7780499fa6",
    "content": "go test fuzz v1\nstring(\"\\u05f5A\")\nbyte('\\x00')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/6fcce067200fb8ae6d4c2b1b7c1f55d3f7e4b38f4ee4f05e50e496a7c399f2d8",
    "content": "go test fuzz v1\nstring(\"\\U00086199\")\nbyte('\\x02')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/b26cd471412059c6ab6aa27b6153d42d2d00cbb00ad11d3cd88a192a7dfd2cdf",
    "content": "go test fuzz v1\nstring(\"\\xb6\")\nbyte('\\x01')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/df6b5d69da50c7d58ca13f6dde15e2a7224a53ce7bd72a02d49893e580b6775b",
    "content": "go test fuzz v1\nstring(\"\\x050\")\nbyte('\\x02')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/ea14da9b0299f4463c20659e2a51808fef8d5fb0de6324f0de64153511d4b1f8",
    "content": "go test fuzz v1\nstring(\"\\U000600a04\")\nbyte('\\x00')\n"
  },
  {
    "path": "syntax/token_string.go",
    "content": "// Code generated by \"stringer -type token -linecomment -trimprefix _\"; DO NOT EDIT.\n\npackage syntax\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[illegalTok-0]\n\t_ = x[_EOF-1]\n\t_ = x[_Newl-2]\n\t_ = x[_Lit-3]\n\t_ = x[_LitWord-4]\n\t_ = x[_LitRedir-5]\n\t_ = x[_realTokenBoundary-6]\n\t_ = x[sglQuote-7]\n\t_ = x[dblQuote-8]\n\t_ = x[bckQuote-9]\n\t_ = x[and-10]\n\t_ = x[andAnd-11]\n\t_ = x[orOr-12]\n\t_ = x[or-13]\n\t_ = x[orAnd-14]\n\t_ = x[andPipe-15]\n\t_ = x[andBang-16]\n\t_ = x[dollar-17]\n\t_ = x[dollSglQuote-18]\n\t_ = x[dollDblQuote-19]\n\t_ = x[dollBrace-20]\n\t_ = x[dollBrack-21]\n\t_ = x[dollParen-22]\n\t_ = x[dollDblParen-23]\n\t_ = x[leftBrace-24]\n\t_ = x[leftBrack-25]\n\t_ = x[dblLeftBrack-26]\n\t_ = x[leftParen-27]\n\t_ = x[dblLeftParen-28]\n\t_ = x[rightBrace-29]\n\t_ = x[rightBrack-30]\n\t_ = x[dblRightBrack-31]\n\t_ = x[rightParen-32]\n\t_ = x[dblRightParen-33]\n\t_ = x[semicolon-34]\n\t_ = x[dblSemicolon-35]\n\t_ = x[semiAnd-36]\n\t_ = x[dblSemiAnd-37]\n\t_ = x[semiOr-38]\n\t_ = x[exclMark-39]\n\t_ = x[tilde-40]\n\t_ = x[addAdd-41]\n\t_ = x[subSub-42]\n\t_ = x[star-43]\n\t_ = x[power-44]\n\t_ = x[equal-45]\n\t_ = x[nequal-46]\n\t_ = x[lequal-47]\n\t_ = x[gequal-48]\n\t_ = x[addAssgn-49]\n\t_ = x[subAssgn-50]\n\t_ = x[mulAssgn-51]\n\t_ = x[quoAssgn-52]\n\t_ = x[remAssgn-53]\n\t_ = x[andAssgn-54]\n\t_ = x[orAssgn-55]\n\t_ = x[xorAssgn-56]\n\t_ = x[shlAssgn-57]\n\t_ = x[shrAssgn-58]\n\t_ = x[andBoolAssgn-59]\n\t_ = x[orBoolAssgn-60]\n\t_ = x[xorBoolAssgn-61]\n\t_ = x[powAssgn-62]\n\t_ = x[rdrOut-63]\n\t_ = x[appOut-64]\n\t_ = x[rdrIn-65]\n\t_ = x[rdrInOut-66]\n\t_ = x[dplIn-67]\n\t_ = x[dplOut-68]\n\t_ = x[rdrClob-69]\n\t_ = x[appClob-70]\n\t_ = x[hdoc-71]\n\t_ = x[dashHdoc-72]\n\t_ = x[wordHdoc-73]\n\t_ = x[rdrAll-74]\n\t_ = x[rdrAllClob-75]\n\t_ = x[appAll-76]\n\t_ = x[appAllClob-77]\n\t_ = x[cmdIn-78]\n\t_ = x[assgnParen-79]\n\t_ = x[cmdOut-80]\n\t_ = x[plus-81]\n\t_ = x[colPlus-82]\n\t_ = x[minus-83]\n\t_ = x[colMinus-84]\n\t_ = x[quest-85]\n\t_ = x[colQuest-86]\n\t_ = x[assgn-87]\n\t_ = x[colAssgn-88]\n\t_ = x[perc-89]\n\t_ = x[dblPerc-90]\n\t_ = x[hash-91]\n\t_ = x[dblHash-92]\n\t_ = x[colHash-93]\n\t_ = x[colPipe-94]\n\t_ = x[colStar-95]\n\t_ = x[caret-96]\n\t_ = x[dblCaret-97]\n\t_ = x[comma-98]\n\t_ = x[dblComma-99]\n\t_ = x[at-100]\n\t_ = x[slash-101]\n\t_ = x[dblSlash-102]\n\t_ = x[period-103]\n\t_ = x[colon-104]\n\t_ = x[tsExists-105]\n\t_ = x[tsRegFile-106]\n\t_ = x[tsDirect-107]\n\t_ = x[tsCharSp-108]\n\t_ = x[tsBlckSp-109]\n\t_ = x[tsNmPipe-110]\n\t_ = x[tsSocket-111]\n\t_ = x[tsSmbLink-112]\n\t_ = x[tsSticky-113]\n\t_ = x[tsGIDSet-114]\n\t_ = x[tsUIDSet-115]\n\t_ = x[tsGrpOwn-116]\n\t_ = x[tsUsrOwn-117]\n\t_ = x[tsModif-118]\n\t_ = x[tsRead-119]\n\t_ = x[tsWrite-120]\n\t_ = x[tsExec-121]\n\t_ = x[tsNoEmpty-122]\n\t_ = x[tsFdTerm-123]\n\t_ = x[tsEmpStr-124]\n\t_ = x[tsNempStr-125]\n\t_ = x[tsOptSet-126]\n\t_ = x[tsVarSet-127]\n\t_ = x[tsRefVar-128]\n\t_ = x[tsReMatch-129]\n\t_ = x[tsNewer-130]\n\t_ = x[tsOlder-131]\n\t_ = x[tsDevIno-132]\n\t_ = x[tsEql-133]\n\t_ = x[tsNeq-134]\n\t_ = x[tsLeq-135]\n\t_ = x[tsGeq-136]\n\t_ = x[tsLss-137]\n\t_ = x[tsGtr-138]\n\t_ = x[globQuest-139]\n\t_ = x[globStar-140]\n\t_ = x[globPlus-141]\n\t_ = x[globAt-142]\n\t_ = x[globExcl-143]\n}\n\nconst _token_name = \"illegalTokEOFNewlLitLitWordLitRedirrealTokenBoundary'\\\"`&&&||||&&|&!$$'$\\\"${$[$($(({[[[(((}]]])));;;;&;;&;|!~++--***==!=<=>=+=-=*=/=%=&=|=^=<<=>>=&&=||=^^=**=>>><<><&>&>|>>|<<<<-<<<&>&>|&>>&>>|<(=(>(+:+-:-?:?=:=%%%###:#:|:*^^^,,,@///.:-e-f-d-c-b-p-S-L-k-g-u-G-O-N-r-w-x-s-t-z-n-o-v-R=~-nt-ot-ef-eq-ne-le-ge-lt-gt?(*(+(@(!(\"\n\nvar _token_index = [...]uint16{0, 10, 13, 17, 20, 27, 35, 52, 53, 54, 55, 56, 58, 60, 61, 63, 65, 67, 68, 70, 72, 74, 76, 78, 81, 82, 83, 85, 86, 88, 89, 90, 92, 93, 95, 96, 98, 100, 103, 105, 106, 107, 109, 111, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 141, 144, 147, 150, 153, 156, 157, 159, 160, 162, 164, 166, 168, 171, 173, 176, 179, 181, 184, 187, 191, 193, 195, 197, 198, 200, 201, 203, 204, 206, 207, 209, 210, 212, 213, 215, 217, 219, 221, 222, 224, 225, 227, 228, 229, 231, 232, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255, 257, 259, 261, 263, 265, 267, 269, 271, 273, 275, 277, 279, 281, 283, 286, 289, 292, 295, 298, 301, 304, 307, 310, 312, 314, 316, 318, 320}\n\nfunc (i token) String() string {\n\tidx := int(i) - 0\n\tif i < 0 || idx >= len(_token_index)-1 {\n\t\treturn \"token(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _token_name[_token_index[idx]:_token_index[idx+1]]\n}\n"
  },
  {
    "path": "syntax/tokens.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\n//go:generate go tool stringer -type token -linecomment -trimprefix _\n\ntype token uint32\n\n// The list of all possible tokens.\nconst (\n\tillegalTok token = iota\n\n\t_EOF\n\t_Newl\n\t_Lit\n\t_LitWord\n\t_LitRedir\n\n\t// Token values beyond this point stringify as exact source.\n\t_realTokenBoundary\n\n\tsglQuote // '\n\tdblQuote // \"\n\tbckQuote // `\n\n\tand     // &\n\tandAnd  // &&\n\torOr    // ||\n\tor      // |\n\torAnd   // |&\n\tandPipe // &|\n\tandBang // &!\n\n\tdollar       // $\n\tdollSglQuote // $'\n\tdollDblQuote // $\"\n\tdollBrace    // ${\n\tdollBrack    // $[\n\tdollParen    // $(\n\tdollDblParen // $((\n\tleftBrace    // {\n\tleftBrack    // [\n\tdblLeftBrack // [[\n\tleftParen    // (\n\tdblLeftParen // ((\n\n\trightBrace    // }\n\trightBrack    // ]\n\tdblRightBrack // ]]\n\trightParen    // )\n\tdblRightParen // ))\n\tsemicolon     // ;\n\n\tdblSemicolon // ;;\n\tsemiAnd      // ;&\n\tdblSemiAnd   // ;;&\n\tsemiOr       // ;|\n\n\texclMark // !\n\ttilde    // ~\n\taddAdd   // ++\n\tsubSub   // --\n\tstar     // *\n\tpower    // **\n\tequal    // ==\n\tnequal   // !=\n\tlequal   // <=\n\tgequal   // >=\n\n\taddAssgn     // +=\n\tsubAssgn     // -=\n\tmulAssgn     // *=\n\tquoAssgn     // /=\n\tremAssgn     // %=\n\tandAssgn     // &=\n\torAssgn      // |=\n\txorAssgn     // ^=\n\tshlAssgn     // <<=\n\tshrAssgn     // >>=\n\tandBoolAssgn // &&=\n\torBoolAssgn  // ||=\n\txorBoolAssgn // ^^=\n\tpowAssgn     // **=\n\n\trdrOut     // >\n\tappOut     // >>\n\trdrIn      // <\n\trdrInOut   // <>\n\tdplIn      // <&\n\tdplOut     // >&\n\trdrClob    // >|\n\tappClob    // >>|\n\thdoc       // <<\n\tdashHdoc   // <<-\n\twordHdoc   // <<<\n\trdrAll     // &>\n\trdrAllClob // &>|\n\tappAll     // &>>\n\tappAllClob // &>>|\n\n\tcmdIn      // <(\n\tassgnParen // =(\n\tcmdOut     // >(\n\n\tplus     // +\n\tcolPlus  // :+\n\tminus    // -\n\tcolMinus // :-\n\tquest    // ?\n\tcolQuest // :?\n\tassgn    // =\n\tcolAssgn // :=\n\tperc     // %\n\tdblPerc  // %%\n\thash     // #\n\tdblHash  // ##\n\tcolHash  // :#\n\tcolPipe  // :|\n\tcolStar  // :*\n\tcaret    // ^\n\tdblCaret // ^^\n\tcomma    // ,\n\tdblComma // ,,\n\tat       // @\n\tslash    // /\n\tdblSlash // //\n\tperiod   // .\n\tcolon    // :\n\n\ttsExists  // -e\n\ttsRegFile // -f\n\ttsDirect  // -d\n\ttsCharSp  // -c\n\ttsBlckSp  // -b\n\ttsNmPipe  // -p\n\ttsSocket  // -S\n\ttsSmbLink // -L\n\ttsSticky  // -k\n\ttsGIDSet  // -g\n\ttsUIDSet  // -u\n\ttsGrpOwn  // -G\n\ttsUsrOwn  // -O\n\ttsModif   // -N\n\ttsRead    // -r\n\ttsWrite   // -w\n\ttsExec    // -x\n\ttsNoEmpty // -s\n\ttsFdTerm  // -t\n\ttsEmpStr  // -z\n\ttsNempStr // -n\n\ttsOptSet  // -o\n\ttsVarSet  // -v\n\ttsRefVar  // -R\n\n\ttsReMatch // =~\n\ttsNewer   // -nt\n\ttsOlder   // -ot\n\ttsDevIno  // -ef\n\ttsEql     // -eq\n\ttsNeq     // -ne\n\ttsLeq     // -le\n\ttsGeq     // -ge\n\ttsLss     // -lt\n\ttsGtr     // -gt\n\n\tglobQuest // ?(\n\tglobStar  // *(\n\tglobPlus  // +(\n\tglobAt    // @(\n\tglobExcl  // !(\n)\n\nfunc (t token) isLit() bool {\n\treturn t == _Lit || t == _LitWord || t == _LitRedir\n}\n\ntype RedirOperator token\n\nconst (\n\tRdrOut     = RedirOperator(rdrOut) + iota // >\n\tAppOut                                    // >>\n\tRdrIn                                     // <\n\tRdrInOut                                  // <>\n\tDplIn                                     // <&\n\tDplOut                                    // >&\n\tRdrClob                                   // >|\n\tAppClob                                   // >>| with [LangZsh]\n\tHdoc                                      // <<\n\tDashHdoc                                  // <<-\n\tWordHdoc                                  // <<<\n\tRdrAll                                    // &>\n\tRdrAllClob                                // &>| with [LangZsh]\n\tAppAll                                    // &>>\n\tAppAllClob                                // &>>| with [LangZsh]\n\n\t// Deprecated: use [RdrClob]\n\t//\n\t//go:fix inline\n\tClbOut = RdrClob\n)\n\ntype ProcOperator token\n\nconst (\n\tCmdIn     = ProcOperator(cmdIn) + iota // <(\n\tCmdInTemp                              // =(\n\tCmdOut                                 // >(\n)\n\ntype GlobOperator token\n\nconst (\n\tGlobZeroOrOne  = GlobOperator(globQuest) + iota // ?(\n\tGlobZeroOrMore                                  // *(\n\tGlobOneOrMore                                   // +(\n\tGlobOne                                         // @(\n\tGlobExcept                                      // !(\n)\n\ntype BinCmdOperator token\n\nconst (\n\tAndStmt = BinCmdOperator(andAnd) + iota // &&\n\tOrStmt                                  // ||\n\tPipe                                    // |\n\tPipeAll                                 // |&\n)\n\ntype CaseOperator token\n\nconst (\n\tBreak       = CaseOperator(dblSemicolon) + iota // ;;\n\tFallthrough                                     // ;&\n\tResume                                          // ;;&\n\tResumeKorn                                      // ;|\n)\n\ntype ParNamesOperator token\n\nconst (\n\tNamesPrefix      = ParNamesOperator(star) // *\n\tNamesPrefixWords = ParNamesOperator(at)   // @\n)\n\ntype ParExpOperator token\n\nconst (\n\tAlternateUnset       = ParExpOperator(plus) + iota // +\n\tAlternateUnsetOrNull                               // :+\n\tDefaultUnset                                       // -\n\tDefaultUnsetOrNull                                 // :-\n\tErrorUnset                                         // ?\n\tErrorUnsetOrNull                                   // :?\n\tAssignUnset                                        // =\n\tAssignUnsetOrNull                                  // :=\n\tRemSmallSuffix                                     // %\n\tRemLargeSuffix                                     // %%\n\tRemSmallPrefix                                     // #\n\tRemLargePrefix                                     // ##\n\tMatchEmpty                                         // :# with [LangZsh]\n\tArrayExclude                                       // :| with [LangZsh]\n\tArrayIntersect                                     // :* with [LangZsh]\n\tUpperFirst                                         // ^\n\tUpperAll                                           // ^^\n\tLowerFirst                                         // ,\n\tLowerAll                                           // ,,\n\tOtherParamOps                                      // @\n)\n\ntype UnAritOperator token\n\nconst (\n\tNot         = UnAritOperator(exclMark) + iota // !\n\tBitNegation                                   // ~\n\tInc                                           // ++\n\tDec                                           // --\n\tPlus        = UnAritOperator(plus)            // +\n\tMinus       = UnAritOperator(minus)           // -\n)\n\ntype BinAritOperator token\n\nconst (\n\tAdd = BinAritOperator(plus)   // +\n\tSub = BinAritOperator(minus)  // -\n\tMul = BinAritOperator(star)   // *\n\tQuo = BinAritOperator(slash)  // /\n\tRem = BinAritOperator(perc)   // %\n\tPow = BinAritOperator(power)  // **\n\tEql = BinAritOperator(equal)  // ==\n\tGtr = BinAritOperator(rdrOut) // >\n\tLss = BinAritOperator(rdrIn)  // <\n\tNeq = BinAritOperator(nequal) // !=\n\tLeq = BinAritOperator(lequal) // <=\n\tGeq = BinAritOperator(gequal) // >=\n\tAnd = BinAritOperator(and)    // &\n\tOr  = BinAritOperator(or)     // |\n\tXor = BinAritOperator(caret)  // ^\n\tShr = BinAritOperator(appOut) // >>\n\tShl = BinAritOperator(hdoc)   // <<\n\n\t// TODO: use \"Bool\" consistently for logical operators like AndArit and OrArit; use //go:fix inline?\n\n\tAndArit   = BinAritOperator(andAnd)   // &&\n\tOrArit    = BinAritOperator(orOr)     // ||\n\tXorBool   = BinAritOperator(dblCaret) // ^^\n\tComma     = BinAritOperator(comma)    // ,\n\tTernQuest = BinAritOperator(quest)    // ?\n\tTernColon = BinAritOperator(colon)    // :\n\n\tAssgn        = BinAritOperator(assgn)        // =\n\tAddAssgn     = BinAritOperator(addAssgn)     // +=\n\tSubAssgn     = BinAritOperator(subAssgn)     // -=\n\tMulAssgn     = BinAritOperator(mulAssgn)     // *=\n\tQuoAssgn     = BinAritOperator(quoAssgn)     // /=\n\tRemAssgn     = BinAritOperator(remAssgn)     // %=\n\tAndAssgn     = BinAritOperator(andAssgn)     // &=\n\tOrAssgn      = BinAritOperator(orAssgn)      // |=\n\tXorAssgn     = BinAritOperator(xorAssgn)     // ^=\n\tShlAssgn     = BinAritOperator(shlAssgn)     // <<=\n\tShrAssgn     = BinAritOperator(shrAssgn)     // >>=\n\tAndBoolAssgn = BinAritOperator(andBoolAssgn) // &&=\n\tOrBoolAssgn  = BinAritOperator(orBoolAssgn)  // ||=\n\tXorBoolAssgn = BinAritOperator(xorBoolAssgn) // ^^=\n\tPowAssgn     = BinAritOperator(powAssgn)     // **=\n)\n\ntype UnTestOperator token\n\nconst (\n\tTsExists  = UnTestOperator(tsExists) + iota // -e\n\tTsRegFile                                   // -f\n\tTsDirect                                    // -d\n\tTsCharSp                                    // -c\n\tTsBlckSp                                    // -b\n\tTsNmPipe                                    // -p\n\tTsSocket                                    // -S\n\tTsSmbLink                                   // -L\n\tTsSticky                                    // -k\n\tTsGIDSet                                    // -g\n\tTsUIDSet                                    // -u\n\tTsGrpOwn                                    // -G\n\tTsUsrOwn                                    // -O\n\tTsModif                                     // -N\n\tTsRead                                      // -r\n\tTsWrite                                     // -w\n\tTsExec                                      // -x\n\tTsNoEmpty                                   // -s\n\tTsFdTerm                                    // -t\n\tTsEmpStr                                    // -z\n\tTsNempStr                                   // -n\n\tTsOptSet                                    // -o\n\tTsVarSet                                    // -v\n\tTsRefVar                                    // -R\n\tTsNot     = UnTestOperator(exclMark)        // !\n\tTsParen   = UnTestOperator(leftParen)       // (\n)\n\ntype BinTestOperator token\n\nconst (\n\tTsReMatch    = BinTestOperator(tsReMatch) + iota // =~\n\tTsNewer                                          // -nt\n\tTsOlder                                          // -ot\n\tTsDevIno                                         // -ef\n\tTsEql                                            // -eq\n\tTsNeq                                            // -ne\n\tTsLeq                                            // -le\n\tTsGeq                                            // -ge\n\tTsLss                                            // -lt\n\tTsGtr                                            // -gt\n\tAndTest      = BinTestOperator(andAnd)           // &&\n\tOrTest       = BinTestOperator(orOr)             // ||\n\tTsMatchShort = BinTestOperator(assgn)            // =\n\tTsMatch      = BinTestOperator(equal)            // ==\n\tTsNoMatch    = BinTestOperator(nequal)           // !=\n\tTsBefore     = BinTestOperator(rdrIn)            // <\n\tTsAfter      = BinTestOperator(rdrOut)           // >\n)\n\nfunc (o RedirOperator) String() string    { return token(o).String() }\nfunc (o ProcOperator) String() string     { return token(o).String() }\nfunc (o GlobOperator) String() string     { return token(o).String() }\nfunc (o BinCmdOperator) String() string   { return token(o).String() }\nfunc (o CaseOperator) String() string     { return token(o).String() }\nfunc (o ParNamesOperator) String() string { return token(o).String() }\nfunc (o ParExpOperator) String() string   { return token(o).String() }\nfunc (o UnAritOperator) String() string   { return token(o).String() }\nfunc (o BinAritOperator) String() string  { return token(o).String() }\nfunc (o UnTestOperator) String() string   { return token(o).String() }\nfunc (o BinTestOperator) String() string  { return token(o).String() }\n"
  },
  {
    "path": "syntax/typedjson/json.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package typedjson allows encoding and decoding shell syntax trees as JSON.\n// The decoding process needs to know what syntax node types to decode into,\n// so the \"typed JSON\" requires \"Type\" keys in some syntax tree node objects:\n//\n//   - The root node\n//   - Any node represented as an interface field in the parent Go type\n//\n// The types of all other nodes can be inferred from context alone.\n//\n// For the sake of efficiency and simplicity, the \"Type\" key\n// described above must be first in each JSON object.\npackage typedjson\n\n// TODO: encoding and decoding nodes other than File is untested.\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n)\n\n// Encode is a shortcut for [EncodeOptions.Encode] with the default options.\nfunc Encode(w io.Writer, node syntax.Node) error {\n\treturn EncodeOptions{}.Encode(w, node)\n}\n\n// EncodeOptions allows configuring how syntax nodes are encoded.\ntype EncodeOptions struct {\n\tIndent string // e.g. \"\\t\"\n\n\t// Allows us to add options later.\n}\n\n// Encode writes node to w in its typed JSON form,\n// as described in the package documentation.\nfunc (opts EncodeOptions) Encode(w io.Writer, node syntax.Node) error {\n\tval := reflect.ValueOf(node)\n\tencVal, tname := encodeValue(val)\n\tif tname == \"\" {\n\t\tpanic(\"node did not contain a named type?\")\n\t}\n\tencVal.Elem().Field(0).SetString(tname)\n\tenc := json.NewEncoder(w)\n\tif opts.Indent != \"\" {\n\t\tenc.SetIndent(\"\", opts.Indent)\n\t}\n\treturn enc.Encode(encVal.Interface())\n}\n\nfunc encodeValue(val reflect.Value) (reflect.Value, string) {\n\tswitch val.Kind() {\n\tcase reflect.Pointer:\n\t\tif val.IsNil() {\n\t\t\tbreak\n\t\t}\n\t\treturn encodeValue(val.Elem())\n\tcase reflect.Interface:\n\t\tif val.IsNil() {\n\t\t\tbreak\n\t\t}\n\t\tenc, tname := encodeValue(val.Elem())\n\t\tif tname == \"\" {\n\t\t\tpanic(\"interface did not contain a named type?\")\n\t\t}\n\t\tenc.Elem().Field(0).SetString(tname)\n\t\treturn enc, \"\"\n\tcase reflect.Struct:\n\t\t// Construct a new struct with an optional Type, Pos and End,\n\t\t// and then all the visible fields which aren't positions.\n\t\ttyp := val.Type()\n\t\tfields := []reflect.StructField{typeField, posField, endField}\n\t\tfor i := range typ.NumField() {\n\t\t\tfield := typ.Field(i)\n\t\t\ttyp := anyType\n\t\t\tif field.Type == posType {\n\t\t\t\ttyp = exportedPosType\n\t\t\t}\n\t\t\tfields = append(fields, reflect.StructField{\n\t\t\t\tName: field.Name,\n\t\t\t\tType: typ,\n\t\t\t\tTag:  `json:\",omitempty\"`,\n\t\t\t})\n\t\t}\n\t\tencTyp := reflect.StructOf(fields)\n\t\tenc := reflect.New(encTyp).Elem()\n\n\t\t// Node methods are defined on struct pointer receivers.\n\t\tif node, _ := val.Addr().Interface().(syntax.Node); node != nil {\n\t\t\tencodePos(enc.Field(1), node.Pos()) // posField\n\t\t\tencodePos(enc.Field(2), node.End()) // endField\n\t\t}\n\t\t// Do the rest of the fields.\n\t\tfor i := 3; i < encTyp.NumField(); i++ {\n\t\t\tftyp := encTyp.Field(i)\n\t\t\tfval := val.FieldByName(ftyp.Name)\n\t\t\tif ftyp.Type == exportedPosType {\n\t\t\t\tencodePos(enc.Field(i), fval.Interface().(syntax.Pos))\n\t\t\t} else {\n\t\t\t\tencElem, _ := encodeValue(fval)\n\t\t\t\tif encElem.IsValid() {\n\t\t\t\t\tenc.Field(i).Set(encElem)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Addr helps prevent an allocation as we use any fields.\n\t\treturn enc.Addr(), typ.Name()\n\tcase reflect.Slice:\n\t\tn := val.Len()\n\t\tif n == 0 {\n\t\t\tbreak\n\t\t}\n\t\tenc := reflect.MakeSlice(anySliceType, n, n)\n\t\tfor i := range n {\n\t\t\telem := val.Index(i)\n\t\t\tencElem, _ := encodeValue(elem)\n\t\t\tenc.Index(i).Set(encElem)\n\t\t}\n\t\treturn enc, \"\"\n\tcase reflect.Bool:\n\t\tif val.Bool() {\n\t\t\treturn val, \"\"\n\t\t}\n\tcase reflect.String:\n\t\tif val.String() != \"\" {\n\t\t\treturn val, \"\"\n\t\t}\n\tcase reflect.Uint32:\n\t\tif val.Uint() != 0 {\n\t\t\treturn val, \"\"\n\t\t}\n\tdefault:\n\t\tpanic(val.Kind().String())\n\t}\n\treturn noValue, \"\"\n}\n\nvar (\n\tnoValue reflect.Value\n\n\tanyType         = reflect.TypeFor[any]()\n\tanySliceType    = reflect.TypeFor[[]any]()\n\tposType         = reflect.TypeFor[syntax.Pos]()\n\texportedPosType = reflect.TypeFor[*exportedPos]()\n\n\t// TODO(v4): derived fields like Type, Pos, and End should have clearly\n\t// different names to prevent confusion. For example: _type, _pos, _end.\n\ttypeField = reflect.StructField{\n\t\tName: \"Type\",\n\t\tType: reflect.TypeFor[string](),\n\t\tTag:  `json:\",omitempty\"`,\n\t}\n\tposField = reflect.StructField{\n\t\tName: \"Pos\",\n\t\tType: exportedPosType,\n\t\tTag:  `json:\",omitempty\"`,\n\t}\n\tendField = reflect.StructField{\n\t\tName: \"End\",\n\t\tType: exportedPosType,\n\t\tTag:  `json:\",omitempty\"`,\n\t}\n)\n\ntype exportedPos struct {\n\tOffset, Line, Col uint\n}\n\nfunc encodePos(encPtr reflect.Value, val syntax.Pos) {\n\t// TODO: perhaps we should encode recovered positions, as that is still useful information.\n\tif !val.IsValid() {\n\t\treturn\n\t}\n\tenc := reflect.New(exportedPosType.Elem())\n\tencPtr.Set(enc)\n\tenc = enc.Elem()\n\n\tenc.Field(0).SetUint(uint64(val.Offset()))\n\tenc.Field(1).SetUint(uint64(val.Line()))\n\tenc.Field(2).SetUint(uint64(val.Col()))\n}\n\nfunc decodePos(val reflect.Value, enc map[string]any) {\n\toffset := uint(enc[\"Offset\"].(float64))\n\tline := uint(enc[\"Line\"].(float64))\n\tcolumn := uint(enc[\"Col\"].(float64))\n\tval.Set(reflect.ValueOf(syntax.NewPos(offset, line, column)))\n}\n\n// Decode is a shortcut for [DecodeOptions.Decode] with the default options.\nfunc Decode(r io.Reader) (syntax.Node, error) {\n\treturn DecodeOptions{}.Decode(r)\n}\n\n// DecodeOptions allows configuring how syntax nodes are encoded.\ntype DecodeOptions struct {\n\t// Empty for now; allows us to add options later.\n}\n\n// Decode writes node to w in its typed JSON form,\n// as described in the package documentation.\nfunc (opts DecodeOptions) Decode(r io.Reader) (syntax.Node, error) {\n\tvar enc any\n\tif err := json.NewDecoder(r).Decode(&enc); err != nil {\n\t\treturn nil, err\n\t}\n\tnode := new(syntax.Node)\n\tif err := decodeValue(reflect.ValueOf(node).Elem(), enc); err != nil {\n\t\treturn nil, err\n\t}\n\treturn *node, nil\n}\n\nvar nodeByName = map[string]reflect.Type{\n\t\"File\": reflect.TypeFor[syntax.File](),\n\t\"Word\": reflect.TypeFor[syntax.Word](),\n\n\t\"Lit\":       reflect.TypeFor[syntax.Lit](),\n\t\"SglQuoted\": reflect.TypeFor[syntax.SglQuoted](),\n\t\"DblQuoted\": reflect.TypeFor[syntax.DblQuoted](),\n\t\"ParamExp\":  reflect.TypeFor[syntax.ParamExp](),\n\t\"CmdSubst\":  reflect.TypeFor[syntax.CmdSubst](),\n\t\"CallExpr\":  reflect.TypeFor[syntax.CallExpr](),\n\t\"ArithmExp\": reflect.TypeFor[syntax.ArithmExp](),\n\t\"ProcSubst\": reflect.TypeFor[syntax.ProcSubst](),\n\t\"ExtGlob\":   reflect.TypeFor[syntax.ExtGlob](),\n\t\"BraceExp\":  reflect.TypeFor[syntax.BraceExp](),\n\n\t\"ArithmCmd\":    reflect.TypeFor[syntax.ArithmCmd](),\n\t\"BinaryCmd\":    reflect.TypeFor[syntax.BinaryCmd](),\n\t\"IfClause\":     reflect.TypeFor[syntax.IfClause](),\n\t\"ForClause\":    reflect.TypeFor[syntax.ForClause](),\n\t\"WhileClause\":  reflect.TypeFor[syntax.WhileClause](),\n\t\"CaseClause\":   reflect.TypeFor[syntax.CaseClause](),\n\t\"Block\":        reflect.TypeFor[syntax.Block](),\n\t\"Subshell\":     reflect.TypeFor[syntax.Subshell](),\n\t\"FuncDecl\":     reflect.TypeFor[syntax.FuncDecl](),\n\t\"TestClause\":   reflect.TypeFor[syntax.TestClause](),\n\t\"DeclClause\":   reflect.TypeFor[syntax.DeclClause](),\n\t\"LetClause\":    reflect.TypeFor[syntax.LetClause](),\n\t\"TimeClause\":   reflect.TypeFor[syntax.TimeClause](),\n\t\"CoprocClause\": reflect.TypeFor[syntax.CoprocClause](),\n\t\"TestDecl\":     reflect.TypeFor[syntax.TestDecl](),\n\n\t\"UnaryArithm\":  reflect.TypeFor[syntax.UnaryArithm](),\n\t\"BinaryArithm\": reflect.TypeFor[syntax.BinaryArithm](),\n\t\"ParenArithm\":  reflect.TypeFor[syntax.ParenArithm](),\n\n\t\"UnaryTest\":  reflect.TypeFor[syntax.UnaryTest](),\n\t\"BinaryTest\": reflect.TypeFor[syntax.BinaryTest](),\n\t\"ParenTest\":  reflect.TypeFor[syntax.ParenTest](),\n\n\t\"WordIter\":   reflect.TypeFor[syntax.WordIter](),\n\t\"CStyleLoop\": reflect.TypeFor[syntax.CStyleLoop](),\n}\n\nfunc decodeValue(val reflect.Value, enc any) error {\n\tswitch enc := enc.(type) {\n\tcase map[string]any:\n\t\tif val.Kind() == reflect.Pointer && val.IsNil() {\n\t\t\tval.Set(reflect.New(val.Type().Elem()))\n\t\t}\n\t\tif typeName, _ := enc[\"Type\"].(string); typeName != \"\" {\n\t\t\ttyp := nodeByName[typeName]\n\t\t\tif typ == nil {\n\t\t\t\treturn fmt.Errorf(\"unknown type: %q\", typeName)\n\t\t\t}\n\t\t\tval.Set(reflect.New(typ))\n\t\t}\n\t\tfor val.Kind() == reflect.Pointer || val.Kind() == reflect.Interface {\n\t\t\tval = val.Elem()\n\t\t}\n\t\tfor name, fv := range enc {\n\t\t\tfval := val.FieldByName(name)\n\t\t\tswitch name {\n\t\t\tcase \"Type\", \"Pos\", \"End\":\n\t\t\t\t// Type is already used above. Pos and End came from method calls.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !fval.IsValid() {\n\t\t\t\treturn fmt.Errorf(\"unknown field for %s: %q\", val.Type(), name)\n\t\t\t}\n\t\t\tif fval.Type() == posType {\n\t\t\t\t// TODO: don't panic on bad input\n\t\t\t\tdecodePos(fval, fv.(map[string]any))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := decodeValue(fval, fv); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase []any:\n\t\tfor _, encElem := range enc {\n\t\t\telem := reflect.New(val.Type().Elem()).Elem()\n\t\t\tif err := decodeValue(elem, encElem); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tval.Set(reflect.Append(val, elem))\n\t\t}\n\tcase float64:\n\t\t// Tokens and thus operators are uint32, but encoding/json defaults to float64.\n\t\t// TODO: reject invalid operators.\n\t\tu := uint64(enc)\n\t\tval.SetUint(u)\n\tdefault:\n\t\tif enc != nil {\n\t\t\tval.Set(reflect.ValueOf(enc))\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "syntax/typedjson/json_test.go",
    "content": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage typedjson_test\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-quicktest/qt\"\n\n\t\"mvdan.cc/sh/v3/syntax\"\n\t\"mvdan.cc/sh/v3/syntax/typedjson\"\n)\n\nvar update = flag.Bool(\"u\", false, \"update output files\")\n\nfunc TestRoundtrip(t *testing.T) {\n\tt.Parallel()\n\n\tdir := filepath.Join(\"testdata\", \"roundtrip\")\n\tshellPaths, err := filepath.Glob(filepath.Join(dir, \"*.sh\"))\n\tqt.Assert(t, qt.IsNil(err))\n\tfor _, shellPath := range shellPaths {\n\t\tname := strings.TrimSuffix(filepath.Base(shellPath), \".sh\")\n\t\tjsonPath := filepath.Join(dir, name+\".json\")\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tshellInput, err := os.ReadFile(shellPath)\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\tjsonInput, err := os.ReadFile(jsonPath)\n\t\t\tif !*update { // allow it to not exist\n\t\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\t}\n\t\t\tsb := new(strings.Builder)\n\n\t\t\t// Parse the shell source and check that it is well formatted.\n\t\t\tparser := syntax.NewParser(syntax.KeepComments(true))\n\t\t\tnode, err := parser.Parse(bytes.NewReader(shellInput), \"\")\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\n\t\t\tprinter := syntax.NewPrinter()\n\t\t\tsb.Reset()\n\t\t\terr = printer.Print(sb, node)\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\tqt.Assert(t, qt.Equals(sb.String(), string(shellInput)))\n\n\t\t\t// Validate writing the pretty JSON.\n\t\t\tsb.Reset()\n\t\t\tencOpts := typedjson.EncodeOptions{Indent: \"\\t\"}\n\t\t\terr = encOpts.Encode(sb, node)\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\tgot := sb.String()\n\t\t\tif *update {\n\t\t\t\terr := os.WriteFile(jsonPath, []byte(got), 0o666)\n\t\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\t} else {\n\t\t\t\tqt.Assert(t, qt.Equals(got, string(jsonInput)))\n\t\t\t}\n\n\t\t\t// Ensure we don't use the originally parsed node again.\n\t\t\tnode = nil\n\n\t\t\t// Validate reading the pretty JSON and check that it formats the same.\n\t\t\tnode2, err := typedjson.Decode(bytes.NewReader(jsonInput))\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\n\t\t\tsb.Reset()\n\t\t\terr = printer.Print(sb, node2)\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\tqt.Assert(t, qt.Equals(sb.String(), string(shellInput)))\n\n\t\t\t// Validate that emitting the JSON again produces the same result.\n\t\t\tsb.Reset()\n\t\t\terr = encOpts.Encode(sb, node2)\n\t\t\tqt.Assert(t, qt.IsNil(err))\n\t\t\tgot = sb.String()\n\t\t\tqt.Assert(t, qt.Equals(got, string(jsonInput)))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "syntax/typedjson/testdata/roundtrip/file.json",
    "content": "{\n\t\"Type\": \"File\",\n\t\"Pos\": {\n\t\t\"Offset\": 0,\n\t\t\"Line\": 1,\n\t\t\"Col\": 1\n\t},\n\t\"End\": {\n\t\t\"Offset\": 368,\n\t\t\"Line\": 30,\n\t\t\"Col\": 4\n\t},\n\t\"Stmts\": [\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 0,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 3,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 4\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\"Col\": 4\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 0,\n\t\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 3,\n\t\t\t\t\t\t\t\t\t\"Line\": 1,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 0,\n\t\t\t\t\"Line\": 1,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 4,\n\t\t\t\t\"Line\": 2,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 9,\n\t\t\t\t\"Line\": 2,\n\t\t\t\t\"Col\": 6\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 6,\n\t\t\t\t\t\"Line\": 2,\n\t\t\t\t\t\"Col\": 3\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 9,\n\t\t\t\t\t\"Line\": 2,\n\t\t\t\t\t\"Col\": 6\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 6,\n\t\t\t\t\t\t\t\"Line\": 2,\n\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 9,\n\t\t\t\t\t\t\t\"Line\": 2,\n\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 6,\n\t\t\t\t\t\t\t\t\t\"Line\": 2,\n\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 9,\n\t\t\t\t\t\t\t\t\t\"Line\": 2,\n\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 6,\n\t\t\t\t\t\t\t\t\t\"Line\": 2,\n\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 9,\n\t\t\t\t\t\t\t\t\t\"Line\": 2,\n\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 4,\n\t\t\t\t\"Line\": 2,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"Negated\": true\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 10,\n\t\t\t\t\"Line\": 3,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 15,\n\t\t\t\t\"Line\": 3,\n\t\t\t\t\"Col\": 6\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 10,\n\t\t\t\t\t\"Line\": 3,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 13,\n\t\t\t\t\t\"Line\": 3,\n\t\t\t\t\t\"Col\": 4\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 10,\n\t\t\t\t\t\t\t\"Line\": 3,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 13,\n\t\t\t\t\t\t\t\"Line\": 3,\n\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 10,\n\t\t\t\t\t\t\t\t\t\"Line\": 3,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 13,\n\t\t\t\t\t\t\t\t\t\"Line\": 3,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 10,\n\t\t\t\t\t\t\t\t\t\"Line\": 3,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 13,\n\t\t\t\t\t\t\t\t\t\"Line\": 3,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 10,\n\t\t\t\t\"Line\": 3,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"Semicolon\": {\n\t\t\t\t\"Offset\": 14,\n\t\t\t\t\"Line\": 3,\n\t\t\t\t\"Col\": 5\n\t\t\t},\n\t\t\t\"Background\": true\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 16,\n\t\t\t\t\"Line\": 4,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 27,\n\t\t\t\t\"Line\": 4,\n\t\t\t\t\"Col\": 12\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 16,\n\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 27,\n\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\"Col\": 12\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 16,\n\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 21,\n\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"SglQuoted\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 16,\n\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 21,\n\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Left\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 16,\n\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Right\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 20,\n\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 22,\n\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 27,\n\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"DblQuoted\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 22,\n\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 27,\n\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Left\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 22,\n\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Right\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 26,\n\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 23,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 23,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 4,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 16,\n\t\t\t\t\"Line\": 4,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 28,\n\t\t\t\t\"Line\": 5,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 50,\n\t\t\t\t\"Line\": 5,\n\t\t\t\t\"Col\": 23\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 28,\n\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 50,\n\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\"Col\": 23\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 28,\n\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 34,\n\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"ParamExp\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 28,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 34,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Dollar\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 28,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Rbrace\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 33,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Param\": {\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 30,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 33,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 30,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 33,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 35,\n\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 41,\n\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"CmdSubst\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 35,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 41,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Left\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 35,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Right\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 40,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Stmts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 37,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 40,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 37,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 40,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 37,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 40,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 37,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 40,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 37,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 40,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 37,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 42,\n\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 50,\n\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\"Col\": 23\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"ArithmExp\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 42,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 50,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 23\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Left\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 42,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Right\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 48,\n\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\"Col\": 21\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 45,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 48,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 21\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 45,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 48,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 21\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 45,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 48,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 5,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 21\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"baz\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 28,\n\t\t\t\t\"Line\": 5,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 51,\n\t\t\t\t\"Line\": 6,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 67,\n\t\t\t\t\"Line\": 6,\n\t\t\t\t\"Col\": 17\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 51,\n\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 67,\n\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\"Col\": 17\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 51,\n\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 57,\n\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"ExtGlob\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 51,\n\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 57,\n\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 51,\n\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Op\": 142,\n\t\t\t\t\t\t\t\t\"Pattern\": {\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 53,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 56,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 53,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 56,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 58,\n\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 67,\n\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 58,\n\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 67,\n\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 58,\n\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 67,\n\t\t\t\t\t\t\t\t\t\"Line\": 6,\n\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Value\": \"{bar,baz}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 51,\n\t\t\t\t\"Line\": 6,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 69,\n\t\t\t\t\"Line\": 8,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 86,\n\t\t\t\t\"Line\": 8,\n\t\t\t\t\"Col\": 18\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"BinaryCmd\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 86,\n\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\"Col\": 18\n\t\t\t\t},\n\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\"Offset\": 80,\n\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\"Col\": 12\n\t\t\t\t},\n\t\t\t\t\"Op\": 12,\n\t\t\t\t\"X\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 79,\n\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t},\n\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\"Type\": \"BinaryCmd\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 79,\n\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\"Offset\": 73,\n\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Op\": 11,\n\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 72,\n\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 72,\n\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 72,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 72,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 72,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Y\": {\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 76,\n\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 79,\n\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 76,\n\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 79,\n\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 76,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 79,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 76,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 79,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 76,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 79,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\t\"Offset\": 76,\n\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\"Offset\": 69,\n\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"Y\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 83,\n\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 86,\n\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t},\n\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 83,\n\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 86,\n\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 83,\n\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 86,\n\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 83,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 86,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 83,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 86,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"baz\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\"Offset\": 83,\n\t\t\t\t\t\t\"Line\": 8,\n\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 69,\n\t\t\t\t\"Line\": 8,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 87,\n\t\t\t\t\"Line\": 9,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 103,\n\t\t\t\t\"Line\": 9,\n\t\t\t\t\"Col\": 17\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"BinaryCmd\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 103,\n\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\"Col\": 17\n\t\t\t\t},\n\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\"Offset\": 97,\n\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\"Col\": 11\n\t\t\t\t},\n\t\t\t\t\"Op\": 14,\n\t\t\t\t\"X\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 96,\n\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t},\n\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\"Type\": \"BinaryCmd\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 96,\n\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\"Offset\": 91,\n\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Op\": 13,\n\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 90,\n\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 90,\n\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 90,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 90,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 90,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Y\": {\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 93,\n\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 96,\n\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 93,\n\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 96,\n\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 93,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 96,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 93,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 96,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 93,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 96,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\t\"Offset\": 93,\n\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\"Offset\": 87,\n\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"Y\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 100,\n\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 103,\n\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t},\n\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 100,\n\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 103,\n\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 100,\n\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 103,\n\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 100,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 103,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 100,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 103,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"baz\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\"Offset\": 100,\n\t\t\t\t\t\t\"Line\": 9,\n\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 87,\n\t\t\t\t\"Line\": 9,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 105,\n\t\t\t\t\"Line\": 11,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 125,\n\t\t\t\t\"Line\": 11,\n\t\t\t\t\"Col\": 21\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"IfClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 105,\n\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 125,\n\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\"Col\": 21\n\t\t\t\t},\n\t\t\t\t\"Position\": {\n\t\t\t\t\t\"Offset\": 105,\n\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"ThenPos\": {\n\t\t\t\t\t\"Offset\": 113,\n\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\"Col\": 9\n\t\t\t\t},\n\t\t\t\t\"FiPos\": {\n\t\t\t\t\t\"Offset\": 123,\n\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\"Col\": 19\n\t\t\t\t},\n\t\t\t\t\"Cond\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 108,\n\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 112,\n\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 108,\n\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 111,\n\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 108,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 111,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 108,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 111,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 108,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 111,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\"Offset\": 108,\n\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Semicolon\": {\n\t\t\t\t\t\t\t\"Offset\": 111,\n\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t\t\"Then\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 118,\n\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 122,\n\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 118,\n\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 121,\n\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 118,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 121,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 118,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 121,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 118,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 121,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\"Offset\": 118,\n\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Semicolon\": {\n\t\t\t\t\t\t\t\"Offset\": 121,\n\t\t\t\t\t\t\t\"Line\": 11,\n\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 105,\n\t\t\t\t\"Line\": 11,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 126,\n\t\t\t\t\"Line\": 12,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 154,\n\t\t\t\t\"Line\": 12,\n\t\t\t\t\"Col\": 29\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"ForClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 126,\n\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 154,\n\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\"Col\": 29\n\t\t\t\t},\n\t\t\t\t\"ForPos\": {\n\t\t\t\t\t\"Offset\": 126,\n\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"DoPos\": {\n\t\t\t\t\t\"Offset\": 142,\n\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\"Col\": 17\n\t\t\t\t},\n\t\t\t\t\"DonePos\": {\n\t\t\t\t\t\"Offset\": 150,\n\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\"Col\": 25\n\t\t\t\t},\n\t\t\t\t\"Loop\": {\n\t\t\t\t\t\"Type\": \"WordIter\",\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 130,\n\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 140,\n\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t},\n\t\t\t\t\t\"Name\": {\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 130,\n\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 131,\n\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\"Offset\": 130,\n\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\"Offset\": 131,\n\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Value\": \"i\"\n\t\t\t\t\t},\n\t\t\t\t\t\"InPos\": {\n\t\t\t\t\t\t\"Offset\": 132,\n\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t},\n\t\t\t\t\t\"Items\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 135,\n\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 136,\n\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 135,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 136,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 135,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 136,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"1\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 137,\n\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 138,\n\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 137,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 138,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 137,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 138,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"2\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 139,\n\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 140,\n\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 139,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 140,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 139,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 140,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"3\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"Do\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 145,\n\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\"Col\": 20\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 149,\n\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\"Col\": 24\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 145,\n\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\"Col\": 20\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 148,\n\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\"Col\": 23\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 145,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 20\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 148,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 23\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 145,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 20\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 148,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 23\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 145,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 20\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 148,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 23\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\"Offset\": 145,\n\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\"Col\": 20\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Semicolon\": {\n\t\t\t\t\t\t\t\"Offset\": 148,\n\t\t\t\t\t\t\t\"Line\": 12,\n\t\t\t\t\t\t\t\"Col\": 23\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 126,\n\t\t\t\t\"Line\": 12,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 155,\n\t\t\t\t\"Line\": 13,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 194,\n\t\t\t\t\"Line\": 13,\n\t\t\t\t\"Col\": 40\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"ForClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 155,\n\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 194,\n\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\"Col\": 40\n\t\t\t\t},\n\t\t\t\t\"ForPos\": {\n\t\t\t\t\t\"Offset\": 155,\n\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"DoPos\": {\n\t\t\t\t\t\"Offset\": 182,\n\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\"Col\": 28\n\t\t\t\t},\n\t\t\t\t\"DonePos\": {\n\t\t\t\t\t\"Offset\": 190,\n\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\"Col\": 36\n\t\t\t\t},\n\t\t\t\t\"Loop\": {\n\t\t\t\t\t\"Type\": \"CStyleLoop\",\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 159,\n\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 180,\n\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\"Col\": 26\n\t\t\t\t\t},\n\t\t\t\t\t\"Lparen\": {\n\t\t\t\t\t\t\"Offset\": 159,\n\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t},\n\t\t\t\t\t\"Rparen\": {\n\t\t\t\t\t\t\"Offset\": 178,\n\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\"Col\": 24\n\t\t\t\t\t},\n\t\t\t\t\t\"Init\": {\n\t\t\t\t\t\t\"Type\": \"BinaryArithm\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 161,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 166,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\"Offset\": 163,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Op\": 87,\n\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 161,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 162,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 161,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 162,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 161,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 162,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"i\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Y\": {\n\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 165,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 166,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 165,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 166,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 165,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 166,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"0\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"Cond\": {\n\t\t\t\t\t\t\"Type\": \"BinaryArithm\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 168,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 173,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\"Offset\": 170,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Op\": 65,\n\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 168,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 169,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 168,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 169,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 168,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 169,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"i\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Y\": {\n\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 172,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 173,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 172,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 173,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 172,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 173,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"3\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"Post\": {\n\t\t\t\t\t\t\"Type\": \"UnaryArithm\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 175,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 21\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 178,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 24\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\"Offset\": 176,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 22\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Op\": 41,\n\t\t\t\t\t\t\"Post\": true,\n\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 175,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 21\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 176,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 22\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 175,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 21\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 176,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 22\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 175,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 21\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 176,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 22\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"i\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"Do\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 185,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 31\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 189,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 35\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 185,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 31\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 188,\n\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\"Col\": 34\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 185,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 31\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 188,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 34\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 185,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 31\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 188,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 34\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 185,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 31\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 188,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 34\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\"Offset\": 185,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 31\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Semicolon\": {\n\t\t\t\t\t\t\t\"Offset\": 188,\n\t\t\t\t\t\t\t\"Line\": 13,\n\t\t\t\t\t\t\t\"Col\": 34\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 155,\n\t\t\t\t\"Line\": 13,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 195,\n\t\t\t\t\"Line\": 14,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 218,\n\t\t\t\t\"Line\": 14,\n\t\t\t\t\"Col\": 24\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"WhileClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 195,\n\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 218,\n\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\"Col\": 24\n\t\t\t\t},\n\t\t\t\t\"WhilePos\": {\n\t\t\t\t\t\"Offset\": 195,\n\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"DoPos\": {\n\t\t\t\t\t\"Offset\": 206,\n\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\"Col\": 12\n\t\t\t\t},\n\t\t\t\t\"DonePos\": {\n\t\t\t\t\t\"Offset\": 214,\n\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\"Col\": 20\n\t\t\t\t},\n\t\t\t\t\"Cond\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 201,\n\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 205,\n\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 201,\n\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 204,\n\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 201,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 204,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 201,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 204,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 201,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 204,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\"Offset\": 201,\n\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Semicolon\": {\n\t\t\t\t\t\t\t\"Offset\": 204,\n\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t\t\"Do\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 209,\n\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 213,\n\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 209,\n\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 212,\n\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 209,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 212,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 209,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 212,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 209,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 212,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\"Offset\": 209,\n\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Semicolon\": {\n\t\t\t\t\t\t\t\"Offset\": 212,\n\t\t\t\t\t\t\t\"Line\": 14,\n\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 195,\n\t\t\t\t\"Line\": 14,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 219,\n\t\t\t\t\"Line\": 15,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 245,\n\t\t\t\t\"Line\": 15,\n\t\t\t\t\"Col\": 27\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CaseClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 219,\n\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 245,\n\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\"Col\": 27\n\t\t\t\t},\n\t\t\t\t\"Case\": {\n\t\t\t\t\t\"Offset\": 219,\n\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"In\": {\n\t\t\t\t\t\"Offset\": 226,\n\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\"Col\": 8\n\t\t\t\t},\n\t\t\t\t\"Esac\": {\n\t\t\t\t\t\"Offset\": 241,\n\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\"Col\": 23\n\t\t\t\t},\n\t\t\t\t\"Word\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 224,\n\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 225,\n\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t},\n\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 224,\n\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 225,\n\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 224,\n\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\"Offset\": 225,\n\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Value\": \"i\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t\"Items\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 229,\n\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 240,\n\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\"Col\": 22\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Op\": 35,\n\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\"Offset\": 238,\n\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\"Col\": 20\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Patterns\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 229,\n\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 232,\n\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 229,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 232,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 229,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 232,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"Stmts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 234,\n\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 237,\n\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 234,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 237,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 234,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 237,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 234,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 237,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 234,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 237,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 19\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 234,\n\t\t\t\t\t\t\t\t\t\"Line\": 15,\n\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 219,\n\t\t\t\t\"Line\": 15,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 247,\n\t\t\t\t\"Line\": 17,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 255,\n\t\t\t\t\"Line\": 17,\n\t\t\t\t\"Col\": 9\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"Block\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 247,\n\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 255,\n\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\"Col\": 9\n\t\t\t\t},\n\t\t\t\t\"Lbrace\": {\n\t\t\t\t\t\"Offset\": 247,\n\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Rbrace\": {\n\t\t\t\t\t\"Offset\": 254,\n\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\"Col\": 8\n\t\t\t\t},\n\t\t\t\t\"Stmts\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 249,\n\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 253,\n\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 249,\n\t\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 252,\n\t\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 249,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 252,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 249,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 252,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 249,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 252,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\"Offset\": 249,\n\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Semicolon\": {\n\t\t\t\t\t\t\t\"Offset\": 252,\n\t\t\t\t\t\t\t\"Line\": 17,\n\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 247,\n\t\t\t\t\"Line\": 17,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 256,\n\t\t\t\t\"Line\": 18,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 261,\n\t\t\t\t\"Line\": 18,\n\t\t\t\t\"Col\": 6\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"Subshell\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 256,\n\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 261,\n\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\"Col\": 6\n\t\t\t\t},\n\t\t\t\t\"Lparen\": {\n\t\t\t\t\t\"Offset\": 256,\n\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Rparen\": {\n\t\t\t\t\t\"Offset\": 260,\n\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\"Col\": 5\n\t\t\t\t},\n\t\t\t\t\"Stmts\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 257,\n\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\"Col\": 2\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 260,\n\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 257,\n\t\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\t\"Col\": 2\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 260,\n\t\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 257,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 2\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 260,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 257,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 2\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 260,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 257,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 2\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 260,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\"Offset\": 257,\n\t\t\t\t\t\t\t\"Line\": 18,\n\t\t\t\t\t\t\t\"Col\": 2\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 256,\n\t\t\t\t\"Line\": 18,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 262,\n\t\t\t\t\"Line\": 19,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 276,\n\t\t\t\t\"Line\": 19,\n\t\t\t\t\"Col\": 15\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"FuncDecl\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 262,\n\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 276,\n\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\"Col\": 15\n\t\t\t\t},\n\t\t\t\t\"Position\": {\n\t\t\t\t\t\"Offset\": 262,\n\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Parens\": true,\n\t\t\t\t\"Name\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 262,\n\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 265,\n\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t},\n\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\"Offset\": 262,\n\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\"Offset\": 265,\n\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t},\n\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t},\n\t\t\t\t\"Body\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 268,\n\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 276,\n\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t},\n\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\"Type\": \"Block\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 268,\n\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 276,\n\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Lbrace\": {\n\t\t\t\t\t\t\t\"Offset\": 268,\n\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Rbrace\": {\n\t\t\t\t\t\t\t\"Offset\": 275,\n\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Stmts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 270,\n\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 274,\n\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 270,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 273,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 270,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 273,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 270,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 273,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 270,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 273,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 270,\n\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Semicolon\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 273,\n\t\t\t\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\"Offset\": 268,\n\t\t\t\t\t\t\"Line\": 19,\n\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 262,\n\t\t\t\t\"Line\": 19,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 277,\n\t\t\t\t\"Line\": 20,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 288,\n\t\t\t\t\"Line\": 20,\n\t\t\t\t\"Col\": 12\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"DeclClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 277,\n\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 288,\n\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\"Col\": 12\n\t\t\t\t},\n\t\t\t\t\"Variant\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 277,\n\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 284,\n\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t},\n\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\"Offset\": 277,\n\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\"Offset\": 284,\n\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t},\n\t\t\t\t\t\"Value\": \"declare\"\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 285,\n\t\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 288,\n\t\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Naked\": true,\n\t\t\t\t\t\t\"Name\": {\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 285,\n\t\t\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 288,\n\t\t\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 285,\n\t\t\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\"Offset\": 288,\n\t\t\t\t\t\t\t\t\"Line\": 20,\n\t\t\t\t\t\t\t\t\"Col\": 12\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 277,\n\t\t\t\t\"Line\": 20,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 289,\n\t\t\t\t\"Line\": 21,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 304,\n\t\t\t\t\"Line\": 21,\n\t\t\t\t\"Col\": 16\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"LetClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 289,\n\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 304,\n\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\"Col\": 16\n\t\t\t\t},\n\t\t\t\t\"Let\": {\n\t\t\t\t\t\"Offset\": 289,\n\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Exprs\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Type\": \"BinaryArithm\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 293,\n\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 304,\n\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\"Offset\": 296,\n\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Op\": 87,\n\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 293,\n\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 296,\n\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 293,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 296,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 293,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 5\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 296,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Y\": {\n\t\t\t\t\t\t\t\"Type\": \"BinaryArithm\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 297,\n\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 304,\n\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 302,\n\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Op\": 81,\n\t\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\t\"Type\": \"ParenArithm\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 297,\n\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 302,\n\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Lparen\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 297,\n\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Rparen\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 301,\n\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 298,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\"Offset\": 301,\n\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 298,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 301,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 298,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 301,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\t\t\"Col\": 13\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Y\": {\n\t\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 303,\n\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 304,\n\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 303,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 304,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 303,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 15\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 304,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 21,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 16\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"3\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 289,\n\t\t\t\t\"Line\": 21,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 305,\n\t\t\t\t\"Line\": 22,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 313,\n\t\t\t\t\"Line\": 22,\n\t\t\t\t\"Col\": 9\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"TimeClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 305,\n\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 313,\n\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\"Col\": 9\n\t\t\t\t},\n\t\t\t\t\"Time\": {\n\t\t\t\t\t\"Offset\": 305,\n\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Stmt\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 310,\n\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 313,\n\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t},\n\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 310,\n\t\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 313,\n\t\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 310,\n\t\t\t\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 313,\n\t\t\t\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 310,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 313,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 310,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 313,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 9\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\"Offset\": 310,\n\t\t\t\t\t\t\"Line\": 22,\n\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 305,\n\t\t\t\t\"Line\": 22,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 314,\n\t\t\t\t\"Line\": 23,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 324,\n\t\t\t\t\"Line\": 23,\n\t\t\t\t\"Col\": 11\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CoprocClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 314,\n\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 324,\n\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\"Col\": 11\n\t\t\t\t},\n\t\t\t\t\"Coproc\": {\n\t\t\t\t\t\"Offset\": 314,\n\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Stmt\": {\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 321,\n\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 324,\n\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t},\n\t\t\t\t\t\"Cmd\": {\n\t\t\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 321,\n\t\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 324,\n\t\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Args\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 321,\n\t\t\t\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 324,\n\t\t\t\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 321,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 324,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 321,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 324,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t},\n\t\t\t\t\t\"Position\": {\n\t\t\t\t\t\t\"Offset\": 321,\n\t\t\t\t\t\t\"Line\": 23,\n\t\t\t\t\t\t\"Col\": 8\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 314,\n\t\t\t\t\"Line\": 23,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 325,\n\t\t\t\t\"Line\": 24,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 330,\n\t\t\t\t\"Line\": 24,\n\t\t\t\t\"Col\": 6\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"ArithmCmd\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 325,\n\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 330,\n\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\"Col\": 6\n\t\t\t\t},\n\t\t\t\t\"Left\": {\n\t\t\t\t\t\"Offset\": 325,\n\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Right\": {\n\t\t\t\t\t\"Offset\": 328,\n\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\"Col\": 4\n\t\t\t\t},\n\t\t\t\t\"X\": {\n\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 327,\n\t\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 328,\n\t\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t},\n\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 327,\n\t\t\t\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 328,\n\t\t\t\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 327,\n\t\t\t\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\"Offset\": 328,\n\t\t\t\t\t\t\t\t\"Line\": 24,\n\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Value\": \"2\"\n\t\t\t\t\t\t}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 325,\n\t\t\t\t\"Line\": 24,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 332,\n\t\t\t\t\"Line\": 26,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 352,\n\t\t\t\t\"Line\": 26,\n\t\t\t\t\"Col\": 21\n\t\t\t},\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"TestClause\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 332,\n\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 352,\n\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\"Col\": 21\n\t\t\t\t},\n\t\t\t\t\"Left\": {\n\t\t\t\t\t\"Offset\": 332,\n\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"Right\": {\n\t\t\t\t\t\"Offset\": 350,\n\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\"Col\": 19\n\t\t\t\t},\n\t\t\t\t\"X\": {\n\t\t\t\t\t\"Type\": \"UnaryTest\",\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 335,\n\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 349,\n\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t},\n\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\"Offset\": 335,\n\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t},\n\t\t\t\t\t\"Op\": 39,\n\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\"Type\": \"ParenTest\",\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 337,\n\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 349,\n\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\"Col\": 18\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Lparen\": {\n\t\t\t\t\t\t\t\"Offset\": 337,\n\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\"Col\": 6\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Rparen\": {\n\t\t\t\t\t\t\t\"Offset\": 348,\n\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\"Type\": \"BinaryTest\",\n\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 338,\n\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\"Offset\": 348,\n\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\t\"Offset\": 342,\n\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\"Col\": 11\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Op\": 11,\n\t\t\t\t\t\t\t\"X\": {\n\t\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 338,\n\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 341,\n\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 338,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 341,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 338,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 7\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 341,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"foo\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"Y\": {\n\t\t\t\t\t\t\t\t\"Type\": \"Word\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 345,\n\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 348,\n\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\"Type\": \"Lit\",\n\t\t\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 345,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 348,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValuePos\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 345,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 14\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"ValueEnd\": {\n\t\t\t\t\t\t\t\t\t\t\t\"Offset\": 348,\n\t\t\t\t\t\t\t\t\t\t\t\"Line\": 26,\n\t\t\t\t\t\t\t\t\t\t\t\"Col\": 17\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\"Value\": \"bar\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 332,\n\t\t\t\t\"Line\": 26,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"Pos\": {\n\t\t\t\t\"Offset\": 365,\n\t\t\t\t\"Line\": 30,\n\t\t\t\t\"Col\": 1\n\t\t\t},\n\t\t\t\"End\": {\n\t\t\t\t\"Offset\": 368,\n\t\t\t\t\"Line\": 30,\n\t\t\t\t\"Col\": 4\n\t\t\t},\n\t\t\t\"Comments\": [\n\t\t\t\t{\n\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\"Offset\": 354,\n\t\t\t\t\t\t\"Line\": 28,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\"Offset\": 363,\n\t\t\t\t\t\t\"Line\": 28,\n\t\t\t\t\t\t\"Col\": 10\n\t\t\t\t\t},\n\t\t\t\t\t\"Hash\": {\n\t\t\t\t\t\t\"Offset\": 354,\n\t\t\t\t\t\t\"Line\": 28,\n\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t},\n\t\t\t\t\t\"Text\": \" comment\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"Cmd\": {\n\t\t\t\t\"Type\": \"CallExpr\",\n\t\t\t\t\"Pos\": {\n\t\t\t\t\t\"Offset\": 365,\n\t\t\t\t\t\"Line\": 30,\n\t\t\t\t\t\"Col\": 1\n\t\t\t\t},\n\t\t\t\t\"End\": {\n\t\t\t\t\t\"Offset\": 368,\n\t\t\t\t\t\"Line\": 30,\n\t\t\t\t\t\"Col\": 4\n\t\t\t\t},\n\t\t\t\t\"Args\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\"Offset\": 365,\n\t\t\t\t\t\t\t\"Line\": 30,\n\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\"Offset\": 368,\n\t\t\t\t\t\t\t\"Line\": 30,\n\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Parts\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"Type\": \"ProcSubst\",\n\t\t\t\t\t\t\t\t\"Pos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 365,\n\t\t\t\t\t\t\t\t\t\"Line\": 30,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"End\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 368,\n\t\t\t\t\t\t\t\t\t\"Line\": 30,\n\t\t\t\t\t\t\t\t\t\"Col\": 4\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"OpPos\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 365,\n\t\t\t\t\t\t\t\t\t\"Line\": 30,\n\t\t\t\t\t\t\t\t\t\"Col\": 1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Rparen\": {\n\t\t\t\t\t\t\t\t\t\"Offset\": 367,\n\t\t\t\t\t\t\t\t\t\"Line\": 30,\n\t\t\t\t\t\t\t\t\t\"Col\": 3\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"Op\": 78\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"Position\": {\n\t\t\t\t\"Offset\": 365,\n\t\t\t\t\"Line\": 30,\n\t\t\t\t\"Col\": 1\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "syntax/typedjson/testdata/roundtrip/file.sh",
    "content": "foo\n! foo\nfoo &\n'foo' \"bar\"\n${foo} $(bar) $((baz))\n@(foo) {bar,baz}\n\nfoo && bar || baz\nfoo | bar |& baz\n\nif foo; then bar; fi\nfor i in 1 2 3; do bar; done\nfor ((i = 0; i < 3; i++)); do bar; done\nwhile foo; do bar; done\ncase i in foo) bar ;; esac\n\n{ foo; }\n(foo)\nfoo() { bar; }\ndeclare foo\nlet foo=(bar)+3\ntime foo\ncoproc foo\n((2))\n\n[[ ! (foo && bar) ]]\n\n# comment\n\n<()\n"
  },
  {
    "path": "syntax/walk.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n)\n\n// Walk traverses a syntax tree in depth-first order: It starts by calling\n// f(node); node must not be nil. If f returns true, Walk invokes f\n// recursively for each of the non-nil children of node, followed by\n// f(nil).\nfunc Walk(node Node, f func(Node) bool) {\n\tif !f(node) {\n\t\treturn\n\t}\n\n\tswitch node := node.(type) {\n\tcase *File:\n\t\twalkList(node.Stmts, f)\n\t\twalkComments(node.Last, f)\n\tcase *Comment:\n\tcase *Stmt:\n\t\tfor _, c := range node.Comments {\n\t\t\tif !node.End().After(c.Pos()) {\n\t\t\t\tdefer Walk(&c, f)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tWalk(&c, f)\n\t\t}\n\t\tif node.Cmd != nil {\n\t\t\tWalk(node.Cmd, f)\n\t\t}\n\t\twalkList(node.Redirs, f)\n\tcase *Assign:\n\t\twalkNilable(node.Name, f)\n\t\twalkNilable(node.Value, f)\n\t\twalkNilable(node.Index, f)\n\t\twalkNilable(node.Array, f)\n\tcase *Redirect:\n\t\twalkNilable(node.N, f)\n\t\tWalk(node.Word, f)\n\t\twalkNilable(node.Hdoc, f)\n\tcase *CallExpr:\n\t\twalkList(node.Assigns, f)\n\t\twalkList(node.Args, f)\n\tcase *Subshell:\n\t\twalkList(node.Stmts, f)\n\t\twalkComments(node.Last, f)\n\tcase *Block:\n\t\twalkList(node.Stmts, f)\n\t\twalkComments(node.Last, f)\n\tcase *IfClause:\n\t\twalkList(node.Cond, f)\n\t\twalkComments(node.CondLast, f)\n\t\twalkList(node.Then, f)\n\t\twalkComments(node.ThenLast, f)\n\t\twalkNilable(node.Else, f)\n\tcase *WhileClause:\n\t\twalkList(node.Cond, f)\n\t\twalkComments(node.CondLast, f)\n\t\twalkList(node.Do, f)\n\t\twalkComments(node.DoLast, f)\n\tcase *ForClause:\n\t\tWalk(node.Loop, f)\n\t\twalkList(node.Do, f)\n\t\twalkComments(node.DoLast, f)\n\tcase *WordIter:\n\t\tWalk(node.Name, f)\n\t\twalkList(node.Items, f)\n\tcase *CStyleLoop:\n\t\twalkNilable(node.Init, f)\n\t\twalkNilable(node.Cond, f)\n\t\twalkNilable(node.Post, f)\n\tcase *BinaryCmd:\n\t\tWalk(node.X, f)\n\t\tWalk(node.Y, f)\n\tcase *FuncDecl:\n\t\twalkNilable(node.Name, f)\n\t\twalkList(node.Names, f)\n\t\tWalk(node.Body, f)\n\tcase *Word:\n\t\twalkList(node.Parts, f)\n\tcase *Lit:\n\tcase *SglQuoted:\n\tcase *DblQuoted:\n\t\twalkList(node.Parts, f)\n\tcase *CmdSubst:\n\t\twalkList(node.Stmts, f)\n\t\twalkComments(node.Last, f)\n\tcase *ParamExp:\n\t\twalkNilable(node.Flags, f)\n\t\twalkNilable(node.Param, f)\n\t\twalkNilable(node.NestedParam, f)\n\t\twalkNilable(node.Index, f)\n\t\tif node.Slice != nil {\n\t\t\twalkNilable(node.Slice.Offset, f)\n\t\t\twalkNilable(node.Slice.Length, f)\n\t\t}\n\t\tif node.Repl != nil {\n\t\t\twalkNilable(node.Repl.Orig, f)\n\t\t\twalkNilable(node.Repl.With, f)\n\t\t}\n\t\tif node.Exp != nil {\n\t\t\twalkNilable(node.Exp.Word, f)\n\t\t}\n\tcase *ArithmExp:\n\t\tWalk(node.X, f)\n\tcase *ArithmCmd:\n\t\tWalk(node.X, f)\n\tcase *BinaryArithm:\n\t\tWalk(node.X, f)\n\t\tWalk(node.Y, f)\n\tcase *BinaryTest:\n\t\tWalk(node.X, f)\n\t\tWalk(node.Y, f)\n\tcase *UnaryArithm:\n\t\tWalk(node.X, f)\n\tcase *UnaryTest:\n\t\tWalk(node.X, f)\n\tcase *ParenArithm:\n\t\tWalk(node.X, f)\n\tcase *FlagsArithm:\n\t\tWalk(node.Flags, f)\n\t\tif node.X != nil {\n\t\t\tWalk(node.X, f)\n\t\t}\n\tcase *ParenTest:\n\t\tWalk(node.X, f)\n\tcase *CaseClause:\n\t\tWalk(node.Word, f)\n\t\twalkList(node.Items, f)\n\t\twalkComments(node.Last, f)\n\tcase *CaseItem:\n\t\tfor _, c := range node.Comments {\n\t\t\tif c.Pos().After(node.Pos()) {\n\t\t\t\tdefer Walk(&c, f)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tWalk(&c, f)\n\t\t}\n\t\twalkList(node.Patterns, f)\n\t\twalkList(node.Stmts, f)\n\t\twalkComments(node.Last, f)\n\tcase *TestClause:\n\t\tWalk(node.X, f)\n\tcase *DeclClause:\n\t\twalkList(node.Args, f)\n\tcase *ArrayExpr:\n\t\twalkList(node.Elems, f)\n\t\twalkComments(node.Last, f)\n\tcase *ArrayElem:\n\t\tfor _, c := range node.Comments {\n\t\t\tif c.Pos().After(node.Pos()) {\n\t\t\t\tdefer Walk(&c, f)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tWalk(&c, f)\n\t\t}\n\t\twalkNilable(node.Index, f)\n\t\twalkNilable(node.Value, f)\n\tcase *ExtGlob:\n\t\tWalk(node.Pattern, f)\n\tcase *ProcSubst:\n\t\twalkList(node.Stmts, f)\n\t\twalkComments(node.Last, f)\n\tcase *TimeClause:\n\t\twalkNilable(node.Stmt, f)\n\tcase *CoprocClause:\n\t\twalkNilable(node.Name, f)\n\t\tWalk(node.Stmt, f)\n\tcase *LetClause:\n\t\twalkList(node.Exprs, f)\n\tcase *TestDecl:\n\t\tWalk(node.Description, f)\n\t\tWalk(node.Body, f)\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"syntax.Walk: unexpected node type %T\", node))\n\t}\n\n\tf(nil)\n}\n\ntype nilableNode interface {\n\tNode\n\tcomparable // pointer nodes, which can be compared to nil\n}\n\nfunc walkNilable[N nilableNode](node N, f func(Node) bool) {\n\tvar zero N // nil\n\tif node != zero {\n\t\tWalk(node, f)\n\t}\n}\n\nfunc walkList[N Node](list []N, f func(Node) bool) {\n\tfor _, node := range list {\n\t\tWalk(node, f)\n\t}\n}\n\nfunc walkComments(list []Comment, f func(Node) bool) {\n\t// Note that []Comment does not satisfy the generic constraint []Node.\n\tfor i := range list {\n\t\tWalk(&list[i], f)\n\t}\n}\n\n// DebugPrint prints the provided syntax tree, spanning multiple lines and with\n// indentation. Can be useful to investigate the content of a syntax tree.\nfunc DebugPrint(w io.Writer, node Node) error {\n\tp := debugPrinter{out: w}\n\tp.print(reflect.ValueOf(node))\n\tp.printf(\"\\n\")\n\treturn p.err\n}\n\ntype debugPrinter struct {\n\tout   io.Writer\n\tlevel int\n\terr   error\n}\n\nfunc (p *debugPrinter) printf(format string, args ...any) {\n\t_, err := fmt.Fprintf(p.out, format, args...)\n\tif err != nil && p.err == nil {\n\t\tp.err = err\n\t}\n}\n\nfunc (p *debugPrinter) newline() {\n\tp.printf(\"\\n\")\n\tfor range p.level {\n\t\tp.printf(\".  \")\n\t}\n}\n\nfunc (p *debugPrinter) print(x reflect.Value) {\n\tswitch x.Kind() {\n\tcase reflect.Interface:\n\t\tif x.IsNil() {\n\t\t\tp.printf(\"nil\")\n\t\t\treturn\n\t\t}\n\t\tp.print(x.Elem())\n\tcase reflect.Pointer:\n\t\tif x.IsNil() {\n\t\t\tp.printf(\"nil\")\n\t\t\treturn\n\t\t}\n\t\tp.printf(\"*\")\n\t\tp.print(x.Elem())\n\tcase reflect.Slice:\n\t\tp.printf(\"%s (len = %d) {\", x.Type(), x.Len())\n\t\tif x.Len() > 0 {\n\t\t\tp.level++\n\t\t\tp.newline()\n\t\t\tfor i := range x.Len() {\n\t\t\t\tp.printf(\"%d: \", i)\n\t\t\t\tp.print(x.Index(i))\n\t\t\t\tif i == x.Len()-1 {\n\t\t\t\t\tp.level--\n\t\t\t\t}\n\t\t\t\tp.newline()\n\t\t\t}\n\t\t}\n\t\tp.printf(\"}\")\n\n\tcase reflect.Struct:\n\t\tif v, ok := x.Interface().(Pos); ok {\n\t\t\tif v.IsRecovered() {\n\t\t\t\tp.printf(\"<recovered>\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tp.printf(\"%v:%v\", v.Line(), v.Col())\n\t\t\treturn\n\t\t}\n\t\tt := x.Type()\n\t\tp.printf(\"%s {\", t)\n\t\tp.level++\n\t\tp.newline()\n\t\tfor i := range t.NumField() {\n\t\t\tp.printf(\"%s: \", t.Field(i).Name)\n\t\t\tp.print(x.Field(i))\n\t\t\tif i == x.NumField()-1 {\n\t\t\t\tp.level--\n\t\t\t}\n\t\t\tp.newline()\n\t\t}\n\t\tp.printf(\"}\")\n\tdefault:\n\t\tif s, ok := x.Interface().(fmt.Stringer); ok && !x.IsZero() {\n\t\t\tp.printf(\"%#v (%s)\", x.Interface(), s)\n\t\t} else {\n\t\t\tp.printf(\"%#v\", x.Interface())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "syntax/walk_test.go",
    "content": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestWalk(t *testing.T) {\n\tt.Parallel()\n\tseen := map[string]bool{\n\t\t\"*syntax.File\":         false,\n\t\t\"*syntax.Comment\":      false,\n\t\t\"*syntax.Stmt\":         false,\n\t\t\"*syntax.Assign\":       false,\n\t\t\"*syntax.Redirect\":     false,\n\t\t\"*syntax.CallExpr\":     false,\n\t\t\"*syntax.Subshell\":     false,\n\t\t\"*syntax.Block\":        false,\n\t\t\"*syntax.IfClause\":     false,\n\t\t\"*syntax.WhileClause\":  false,\n\t\t\"*syntax.ForClause\":    false,\n\t\t\"*syntax.WordIter\":     false,\n\t\t\"*syntax.CStyleLoop\":   false,\n\t\t\"*syntax.BinaryCmd\":    false,\n\t\t\"*syntax.FuncDecl\":     false,\n\t\t\"*syntax.Word\":         false,\n\t\t\"*syntax.Lit\":          false,\n\t\t\"*syntax.SglQuoted\":    false,\n\t\t\"*syntax.DblQuoted\":    false,\n\t\t\"*syntax.CmdSubst\":     false,\n\t\t\"*syntax.ParamExp\":     false,\n\t\t\"*syntax.ArithmExp\":    false,\n\t\t\"*syntax.ArithmCmd\":    false,\n\t\t\"*syntax.BinaryArithm\": false,\n\t\t\"*syntax.UnaryArithm\":  false,\n\t\t\"*syntax.ParenArithm\":  false,\n\t\t\"*syntax.CaseClause\":   false,\n\t\t\"*syntax.CaseItem\":     false,\n\t\t\"*syntax.TestClause\":   false,\n\t\t\"*syntax.BinaryTest\":   false,\n\t\t\"*syntax.UnaryTest\":    false,\n\t\t\"*syntax.ParenTest\":    false,\n\t\t\"*syntax.DeclClause\":   false,\n\t\t\"*syntax.ArrayExpr\":    false,\n\t\t\"*syntax.ArrayElem\":    false,\n\t\t\"*syntax.ExtGlob\":      false,\n\t\t\"*syntax.ProcSubst\":    false,\n\t\t\"*syntax.TimeClause\":   false,\n\t\t\"*syntax.CoprocClause\": false,\n\t\t\"*syntax.LetClause\":    false,\n\t}\n\tparser := NewParser(KeepComments(true))\n\tvar allStrs []string\n\tfor _, c := range fileTests {\n\t\tallStrs = append(allStrs, c.inputs[0])\n\t}\n\tfor _, c := range printTests {\n\t\tallStrs = append(allStrs, c.in)\n\t}\n\tcountRan := 0\n\tfor _, in := range allStrs {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tt.Logf(\"input: %s\", in)\n\t\t\tcountRan++\n\t\t\tprog, err := parser.Parse(strings.NewReader(in), \"\")\n\t\t\tif err != nil {\n\t\t\t\t// good enough for now, as the bash parser\n\t\t\t\t// ignoring errors covers what we need.\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlastOffs := uint(0)\n\t\t\tWalk(prog, func(node Node) bool {\n\t\t\t\tif node == nil {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\ttstr := reflect.TypeOf(node).String()\n\t\t\t\tif _, ok := seen[tstr]; !ok {\n\t\t\t\t\tt.Errorf(\"unexpected type: %s\", tstr)\n\t\t\t\t} else {\n\t\t\t\t\tseen[tstr] = true\n\t\t\t\t}\n\t\t\t\tswitch node.(type) {\n\t\t\t\tcase *Lit:\n\t\t\t\t\treturn false\n\t\t\t\tcase *Comment:\n\t\t\t\tdefault:\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\toffs := node.Pos().Offset()\n\t\t\t\tif offs >= lastOffs {\n\t\t\t\t\tlastOffs = offs\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"comment offset goes back\")\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t})\n\t}\n\t// If we're running a subset of the tests,\n\t// we can't expect to have seen all node types.\n\tif countRan == len(allStrs) {\n\t\tfor tstr, tseen := range seen {\n\t\t\tif !tseen {\n\t\t\t\tt.Errorf(\"type not seen: %s\", tstr)\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype newNode struct{}\n\nfunc (newNode) Pos() Pos { return Pos{} }\nfunc (newNode) End() Pos { return Pos{} }\n\nfunc TestWalkUnexpectedType(t *testing.T) {\n\tt.Parallel()\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"did not panic\")\n\t\t}\n\t}()\n\tWalk(newNode{}, func(node Node) bool {\n\t\treturn true\n\t})\n}\n"
  }
]