Full Code of mvdan/sh for AI

master 781257adb402 cached
107 files
991.3 KB
345.3k tokens
1385 symbols
1 requests
Download .txt
Showing preview only (1,033K chars total). Download the full file or copy to clipboard to get everything.
Repository: mvdan/sh
Branch: master
Commit: 781257adb402
Files: 107
Total size: 991.3 KB

Directory structure:
gitextract_0uf_kch0/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cmd/
│   ├── gosh/
│   │   ├── main.go
│   │   └── main_test.go
│   └── shfmt/
│       ├── Dockerfile
│       ├── docker-entrypoint.sh
│       ├── main.go
│       ├── main_test.go
│       ├── shfmt.1.scd
│       └── testdata/
│           └── script/
│               ├── atomic.txtar
│               ├── basic.txtar
│               ├── diff.txtar
│               ├── editorconfig.txtar
│               ├── flags.txtar
│               ├── simplify.txtar
│               ├── tojson.txtar
│               └── walk.txtar
├── expand/
│   ├── arith.go
│   ├── braces.go
│   ├── braces_test.go
│   ├── doc.go
│   ├── environ.go
│   ├── environ_test.go
│   ├── expand.go
│   ├── expand_nonwindows.go
│   ├── expand_test.go
│   ├── expand_windows.go
│   ├── param.go
│   └── valuekind_string.go
├── fileutil/
│   ├── file.go
│   └── file_test.go
├── go.mod
├── go.sum
├── internal/
│   ├── pattern.go
│   └── testing.go
├── interp/
│   ├── api.go
│   ├── builtin.go
│   ├── example_test.go
│   ├── handler.go
│   ├── handler_test.go
│   ├── interp_test.go
│   ├── os_notunix.go
│   ├── os_unix.go
│   ├── runner.go
│   ├── test.go
│   ├── test_classic.go
│   ├── trace.go
│   ├── unexported_test.go
│   ├── unix_test.go
│   ├── vars.go
│   └── windows_test.go
├── moreinterp/
│   ├── coreutils/
│   │   ├── coreutils.go
│   │   ├── coreutils_test.go
│   │   └── error.go
│   ├── go.mod
│   └── go.sum
├── pattern/
│   ├── example_test.go
│   ├── pattern.go
│   └── pattern_test.go
├── shell/
│   ├── doc.go
│   ├── example_test.go
│   ├── expand.go
│   └── expand_test.go
└── syntax/
    ├── bench_test.go
    ├── braces.go
    ├── canonical.sh
    ├── doc.go
    ├── example_test.go
    ├── filetests_test.go
    ├── fuzz_test.go
    ├── lexer.go
    ├── nodes.go
    ├── parser.go
    ├── parser_arithm.go
    ├── parser_linux_test.go
    ├── parser_other_test.go
    ├── parser_test.go
    ├── printer.go
    ├── printer_test.go
    ├── quote.go
    ├── quote_test.go
    ├── simplify.go
    ├── simplify_test.go
    ├── testdata/
    │   └── fuzz/
    │       ├── FuzzParsePrint/
    │       │   ├── 293db3718a4ab7a5
    │       │   ├── 6d0dc226922dc40c
    │       │   └── cb6d714b0a2d2315
    │       └── FuzzQuote/
    │           ├── 23cf0175e40438e8033b11cdd1441a2d2893a99144c4ac0f2b5f4caa113c9edd
    │           ├── 25f36feab4af00bc4dfc3cf56da02b842b62ba8c5ac44862b5b3b776a0d519b4
    │           ├── 2788bd30d386289e06a1024a030ad5ab7f363c703bea8a5d035de174491029bf
    │           ├── 39d5fdf93d52b2cd50fb9582b27c82d159de0575623865538ced2a7780499fa6
    │           ├── 6fcce067200fb8ae6d4c2b1b7c1f55d3f7e4b38f4ee4f05e50e496a7c399f2d8
    │           ├── b26cd471412059c6ab6aa27b6153d42d2d00cbb00ad11d3cd88a192a7dfd2cdf
    │           ├── df6b5d69da50c7d58ca13f6dde15e2a7224a53ce7bd72a02d49893e580b6775b
    │           └── ea14da9b0299f4463c20659e2a51808fef8d5fb0de6324f0de64153511d4b1f8
    ├── token_string.go
    ├── tokens.go
    ├── typedjson/
    │   ├── json.go
    │   ├── json_test.go
    │   └── testdata/
    │       └── roundtrip/
    │           ├── file.json
    │           └── file.sh
    ├── walk.go
    └── walk_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# To prevent CRLF breakages on Windows for fragile files, like testdata.
* -text


================================================
FILE: .github/FUNDING.yml
================================================
github: mvdan


================================================
FILE: .github/workflows/test.yml
================================================
on: [push, pull_request]
name: Test
jobs:
  test:
    strategy:
      matrix:
        go-version: [1.25.x, 1.26.x]
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    timeout-minutes: 10
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-go@v5
      with:
        go-version: ${{ matrix.go-version }}
        cache: false

    - run: go test ./...
    - run: cd moreinterp && go test ./...

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

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

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

  test-linux-alpine:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
    - uses: actions/checkout@v5
    - name: Test as root, without cgo, and with busybox
      run: docker run -v="$PWD:/pwd" -w=/pwd --user=1000 -e=GOCACHE=/tmp -e=CGO_ENABLED=0 golang:1.26.0-alpine go test ./...

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

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

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

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

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


================================================
FILE: .gitignore
================================================
*.a
*.zip

# Don't store any of this in the master branch.
suppressions/
crashers/
corpus/
vendor/


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## [3.12.0] - 2025-07-06

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

## [3.11.0] - 2025-03-05

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

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

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

## [3.10.0] - 2024-10-20

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

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

## [3.9.0] - 2024-08-16

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

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

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

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

## [3.8.0] - 2024-02-11

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

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

## [3.7.0] - 2023-06-18

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

## [3.6.0] - 2022-12-11

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

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

## [3.5.1] - 2022-05-23

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

## [3.5.0] - 2022-05-11

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

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

## [3.4.3] - 2022-02-19

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

## [3.4.2] - 2021-12-24

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

## [3.4.1] - 2021-11-23

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

## [3.4.0] - 2021-10-01

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

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

## [3.3.1] - 2021-08-01

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

## [3.3.0] - 2021-05-17

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

## [3.2.4] - 2021-03-08

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

## [3.2.2] - 2021-01-29

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

## [3.2.1] - 2020-12-02

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

## [3.2.0] - 2020-10-29

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

## [3.1.2] - 2020-06-26

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

## [3.1.1] - 2020-05-04

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

## [3.1.0] - 2020-04-07

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

## [3.0.2] - 2020-02-22

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

## [3.0.1] - 2020-01-11

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

## [3.0.0] - 2019-12-16

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

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

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

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

## [2.6.4] - 2019-03-10

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

## [2.6.3] - 2019-01-19

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

## [2.6.2] - 2018-12-08

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

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

## [2.6.1] - 2018-11-17

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

## [2.6.0] - 2018-11-10

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

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

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

## [2.5.1] - 2018-08-03

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

## [2.5.0] - 2018-07-13

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

## [2.4.0] - 2018-05-16

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

## [2.3.0] - 2018-03-07

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

## [2.2.1] - 2018-01-25

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

## [2.2.0] - 2018-01-18

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

## [2.1.0] - 2017-11-25

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

## [2.0.0] - 2017-08-30

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

## [1.3.1] - 2017-05-26

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

## [1.3.0] - 2017-04-24

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

## [1.2.0] - 2017-02-22

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

## [1.1.0] - 2017-01-05

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

## [1.0.0] - 2016-12-13

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

## [0.6.0] - 2016-12-05

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

## [0.5.0] - 2016-11-24

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

## [0.4.0] - 2016-11-08

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

## [0.3.0] - 2016-10-26

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

## [0.2.0] - 2016-10-13

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

## [0.1.0] - 2016-09-20

Initial release.

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

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

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

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


================================================
FILE: LICENSE
================================================
Copyright (c) 2016, Daniel Martí. All rights reserved.

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

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

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


================================================
FILE: README.md
================================================
# sh

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

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

### Quick start

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

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

To interpret or run shell scripts,
see the [interp package](https://pkg.go.dev/mvdan.cc/sh/v3/interp).

### shfmt

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

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

	shfmt -l -w script.sh

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

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

### gosh

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

Proof of concept shell that uses the `interp` package.

### Fuzzing

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

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

### Caveats

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

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

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

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

* `export`, `let`, and `declare` are parsed as keywords.
  This allows statically building their syntax tree,
  as opposed to keeping the arguments as a slice of words.
  It is also required to support `declare foo=(bar)`.

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

### Formatting FAQs

* The formatter cannot be disabled for ranges of lines; most users wanting this
  are working around a bug or they don't like how a piece of code is formatted.
  Instead, search the issue tracker and file a new issue if necessary.
  Formatting of partial files leads to lots of edge cases and complexity
  which this project has no resources for, nor interest in, getting into.

* We avoid adding more formatting options where possible. Each added flag interacts
  with all others, multiplying the human cost of development, maintenance, testing,
  and properly documenting the behavior for end users.

* The true value in a formatter is consistency, especially for teams of developers.
  We do not aim to satisfy every developer's personal choice of optimal formatting.

### JavaScript

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

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

### Docker

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

To build a Docker image, run:

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

To use a Docker image, run:

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

### Related projects

The following editor integrations wrap `shfmt`:

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

Other noteworthy integrations include:

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

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


================================================
FILE: cmd/gosh/main.go
================================================
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

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

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

	"golang.org/x/term"

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

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

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

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

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

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

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

func runInteractive(r *interp.Runner, stdin io.Reader, stdout, stderr io.Writer) error {
	parser := syntax.NewParser()
	fmt.Fprintf(stdout, "$ ")
	for stmts, err := range parser.InteractiveSeq(stdin) {
		if err != nil {
			return err // stop at the first error
		}
		if parser.Incomplete() {
			fmt.Fprintf(stdout, "> ")
			continue
		}
		ctx := context.Background()
		for _, stmt := range stmts {
			err := r.Run(ctx, stmt)
			if r.Exited() {
				return err
			}
		}
		fmt.Fprintf(stdout, "$ ")
	}
	return nil
}


================================================
FILE: cmd/gosh/main_test.go
================================================
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package main

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

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

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

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

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

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

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

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

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

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

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

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

// readString will keep reading from a reader until all bytes from the supplied
// string are read.
func readString(r io.Reader, want string) error {
	p := make([]byte, len(want))
	_, err := io.ReadFull(r, p)
	if err != nil {
		return err
	}
	got := string(p)
	if got != want {
		return fmt.Errorf("ReadString: read %q, wanted %q", got, want)
	}
	return nil
}


================================================
FILE: cmd/shfmt/Dockerfile
================================================
FROM golang:1.26.1-alpine AS build

WORKDIR /src
RUN apk add --no-cache git
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-w -s -extldflags '-static'" ./cmd/shfmt

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

FROM scratch
COPY --from=build /src/shfmt /bin/shfmt
ENTRYPOINT ["/bin/shfmt"]
CMD ["-h"]


================================================
FILE: cmd/shfmt/docker-entrypoint.sh
================================================
#!/bin/sh
# SPDX-License-Identifier: BSD-3-Clause
#
# Copyright (C) 2019 Olliver Schinagl <oliver@schinagl.nl>
#
# A beginning user should be able to docker run image bash (or sh) without
# needing to learn about --entrypoint
# https://github.com/docker-library/official-images#consistency

set -eu

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

exit 0


================================================
FILE: cmd/shfmt/main.go
================================================
// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// shfmt formats shell programs.
package main

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

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

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

type boolStringValue string

func (b *boolStringValue) Set(val string) error {
	*b = boolStringValue(val)
	return nil
}
func (b *boolStringValue) String() string {
	return string(*b)
}
func (*boolStringValue) IsBoolFlag() bool { return true }

func boolStringVar(p *string, name string, value string, usage string) {
	*p = value
	flag.Var((*boolStringValue)(p), name, usage)
}

func langVariantVar(p *syntax.LangVariant, name string, value syntax.LangVariant, usage string) {
	*p = value
	flag.Var(p, name, usage)
}

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

func flagVal[T any](short, long string, val T, register func(*T, string, T, string)) *multiFlag[T] {
	f := &multiFlag[T]{short, long, val}
	if short != "" {
		register(&f.val, short, val, "")
	}
	if long != "" {
		register(&f.val, long, val, "")
	}
	return f
}

var (
	// Generic flags.
	versionFlag = flagVal("", "version", false, flag.BoolVar)
	list        = flagVal("l", "list", "false", boolStringVar)
	write       = flagVal("w", "write", false, flag.BoolVar)
	diff        = flagVal("d", "diff", false, flag.BoolVar)
	applyIgnore = flagVal("", "apply-ignore", false, flag.BoolVar)
	filename    = flagVal("", "filename", "", flag.StringVar)

	// Parser flags.
	lang     = flagVal("ln", "language-dialect", syntax.LangAuto, langVariantVar)
	posix    = flagVal("p", "posix", false, flag.BoolVar)
	simplify = flagVal("s", "simplify", false, flag.BoolVar)
	// TODO: when promoting exp.recover to a stable flag, add it as an EditorConfig knob too, and perhaps rename to recover-errors
	expRecover = flagVal("", "exp.recover", 0, flag.IntVar)

	// Printer flags.
	indent      = flagVal("i", "indent", 0, flag.UintVar)
	binNext     = flagVal("bn", "binary-next-line", false, flag.BoolVar)
	caseIndent  = flagVal("ci", "case-indent", false, flag.BoolVar)
	spaceRedirs = flagVal("sr", "space-redirects", false, flag.BoolVar)
	keepPadding = flagVal("kp", "keep-padding", false, flag.BoolVar)
	funcNext    = flagVal("fn", "func-next-line", false, flag.BoolVar)
	minify      = flagVal("mn", "minify", false, flag.BoolVar)

	// Utility flags.
	find     = flagVal("f", "find", "false", boolStringVar)
	toJSON   = flagVal("tojson", "to-json", false, flag.BoolVar) // TODO(v4): remove "tojson" for consistency
	fromJSON = flagVal("", "from-json", false, flag.BoolVar)

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

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

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

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

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

  --version  show version and exit

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

Parser options:

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

Printer options:

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

Utilities:

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

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

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

	syntax.RecoverErrors(expRecover.val)(parser)

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

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

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

var errFormattingDiffers = fmt.Errorf("")

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

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

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

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

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

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

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

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

	return lang, langErr == nil
}

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

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

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

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

const (
	terminalGreen = "\u001b[32m"
	terminalRed   = "\u001b[31m"
	terminalCyan  = "\u001b[36m"
	terminalReset = "\u001b[0m"
	terminalBold  = "\u001b[1m"
)


================================================
FILE: cmd/shfmt/main_test.go
================================================
// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package main

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

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

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

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

func TestScript(t *testing.T) {
	t.Parallel()
	testscript.Run(t, testscript.Params{
		Dir:                 filepath.Join("testdata", "script"),
		UpdateScripts:       *update,
		RequireExplicitExec: true,
	})
}


================================================
FILE: cmd/shfmt/shfmt.1.scd
================================================
shfmt(1)

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

# NAME

shfmt - Format shell programs

# SYNOPSIS

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

# DESCRIPTION

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

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

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

# OPTIONS

## Generic flags

*--version*
	Show version and exit.

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

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

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

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

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

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

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

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

## Parser flags

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

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

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

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

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

## Printer flags

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

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

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

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

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

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

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

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

## Utility flags

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

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

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

# EXAMPLES

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

	shfmt -l -w .

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

	shfmt -d .

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

	shfmt -i 2 -ci -bn

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

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

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

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

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

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

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

# AUTHORS

Maintained by Daniel Martí <mvdan@mvdan.cc>, who is assisted by other open
source contributors. For more information and development, see
<https://github.com/mvdan/sh>.


================================================
FILE: cmd/shfmt/testdata/script/atomic.txtar
================================================
[windows] skip 'atomic writes aren''t supported on Windows'
[!exec:sh] skip 'sh is required to run this test'

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

cmp input.sh input.sh.golden

-- input.sh --
echo foo
  shfmt -l -w input.sh
echo bar
-- input.sh.golden --
echo foo
shfmt -l -w input.sh
echo bar
-- stdout.golden --
foo
input.sh
bar


================================================
FILE: cmd/shfmt/testdata/script/basic.txtar
================================================
cp input.sh input.sh.orig

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

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

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

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

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

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

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

-- input.sh --
 foo
-- input.sh.golden --
foo


================================================
FILE: cmd/shfmt/testdata/script/diff.txtar
================================================
stdin input.sh
! exec shfmt -d
cmp stdout input.sh.stdindiff
! stderr .

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

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

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

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

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

-- input.sh --
 foo


bar
-- input.sh.golden --
foo

bar
-- input.sh.stdindiff --
diff <standard input>.orig <standard input>
--- <standard input>.orig
+++ <standard input>
@@ -1,4 +1,3 @@
- foo
-
+foo
 
 bar
-- input.sh.filediff --
diff input.sh.orig input.sh
--- input.sh.orig
+++ input.sh
@@ -1,4 +1,3 @@
- foo
-
+foo
 
 bar


================================================
FILE: cmd/shfmt/testdata/script/editorconfig.txtar
================================================
cp input.sh input.sh.orig

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

-- .editorconfig --
root = true

[*]
indent_style = space
indent_size = 3

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

[*_bash.sh]
shell_variant = bash

[*_mksh.sh]
shell_variant = mksh

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

[binary_next_line.sh]
binary_next_line = true

[switch_case_indent.sh]
switch_case_indent = true

[space_redirects.sh]
space_redirects = true

[keep_padding.sh]
keep_padding = true

[function_next_line.sh]
function_next_line = true

[simplify.sh]
simplify = true

[minify.sh]
minify = true

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

[third_party/**]
ignore = true

[1_lone_ignored.sh]
ignore = true

[2_dir_ignored]
ignore = true

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

[*]
indent_style = space
indent_size = 1

[[shell]]
indent_size = 2

[[bash]]
indent_size = 4

[[zsh]]
indent_size = 5

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

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

{
    indented
}
array=(elem)
-- shebang/binzsh --
#!/bin/zsh

{
     indented
}
array=(elem)
-- multiconfig/space_redirects/.editorconfig --
[*]
space_redirects = true
-- multiconfig/space_redirects/f.sh --
foo > bar
foo &&
   bar
-- multiconfig/binary_next_line/.editorconfig --
[*]
binary_next_line = true
-- multiconfig/binary_next_line/f.sh --
foo >bar
foo \
   && bar


================================================
FILE: cmd/shfmt/testdata/script/flags.txtar
================================================
exec shfmt -h
! stderr 'flag provided but not defined'
stderr 'usage: shfmt'
stderr 'Utilities' # definitely includes our help text
! stderr 'help requested' # don't duplicate usage output
! stderr '-test\.' # don't show the test binary's usage func

exec shfmt --help
stderr 'usage: shfmt'

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

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

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

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

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

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

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

# Check all the -ln variations.

stdin input-posix
! exec shfmt

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

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

stdin input-posix
! exec shfmt -ln=mksh

stdin input-posix
! exec shfmt -ln=bash

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

stdin input-mksh
! exec shfmt

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	case $i in
	j)
		z
		;;
	esac

	space >redirs

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

  case $i in
  j)
    z
    ;;
  esac

  space >redirs

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

	case $i in
	j)
		z
		;;
	esac

	space >redirs

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

	case $i in
		j)
			z
			;;
	esac

	space >redirs

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

	case $i in
	j)
		z
		;;
	esac

	space > redirs

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

	case $i in
	j)
		z
		;;
	esac

	space >redirs

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

	case $i in
	j)
		z
		;;
	esac

	space >redirs

	keep padding
}


================================================
FILE: cmd/shfmt/testdata/script/simplify.txtar
================================================
exec shfmt -s input.sh
cmp stdout input.sh.simplify-golden

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

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

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

-- input.sh --
foo() {
	(( $bar ))
}
-- input.sh.simplify-golden --
foo() {
	((bar))
}
-- input.sh.minify-golden --
foo(){
((bar))
}


================================================
FILE: cmd/shfmt/testdata/script/tojson.txtar
================================================
stdin empty.sh
exec shfmt -tojson # old flag name
cmp stdout empty.sh.json
! stderr .

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

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

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

-- empty.sh --
-- empty.sh.json --
{
	"Type": "File"
}
-- simple.sh --
foo
-- simple.sh.json --
{
	"Type": "File",
	"Pos": {
		"Offset": 0,
		"Line": 1,
		"Col": 1
	},
	"End": {
		"Offset": 3,
		"Line": 1,
		"Col": 4
	},
	"Stmts": [
		{
			"Pos": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			},
			"End": {
				"Offset": 3,
				"Line": 1,
				"Col": 4
			},
			"Cmd": {
				"Type": "CallExpr",
				"Pos": {
					"Offset": 0,
					"Line": 1,
					"Col": 1
				},
				"End": {
					"Offset": 3,
					"Line": 1,
					"Col": 4
				},
				"Args": [
					{
						"Pos": {
							"Offset": 0,
							"Line": 1,
							"Col": 1
						},
						"End": {
							"Offset": 3,
							"Line": 1,
							"Col": 4
						},
						"Parts": [
							{
								"Type": "Lit",
								"Pos": {
									"Offset": 0,
									"Line": 1,
									"Col": 1
								},
								"End": {
									"Offset": 3,
									"Line": 1,
									"Col": 4
								},
								"ValuePos": {
									"Offset": 0,
									"Line": 1,
									"Col": 1
								},
								"ValueEnd": {
									"Offset": 3,
									"Line": 1,
									"Col": 4
								},
								"Value": "foo"
							}
						]
					}
				]
			},
			"Position": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			}
		}
	]
}
-- arithmetic.sh --
((2))
-- arithmetic.sh.json --
{
	"Type": "File",
	"Pos": {
		"Offset": 0,
		"Line": 1,
		"Col": 1
	},
	"End": {
		"Offset": 5,
		"Line": 1,
		"Col": 6
	},
	"Stmts": [
		{
			"Pos": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			},
			"End": {
				"Offset": 5,
				"Line": 1,
				"Col": 6
			},
			"Cmd": {
				"Type": "ArithmCmd",
				"Pos": {
					"Offset": 0,
					"Line": 1,
					"Col": 1
				},
				"End": {
					"Offset": 5,
					"Line": 1,
					"Col": 6
				},
				"Left": {
					"Offset": 0,
					"Line": 1,
					"Col": 1
				},
				"Right": {
					"Offset": 3,
					"Line": 1,
					"Col": 4
				},
				"X": {
					"Type": "Word",
					"Pos": {
						"Offset": 2,
						"Line": 1,
						"Col": 3
					},
					"End": {
						"Offset": 3,
						"Line": 1,
						"Col": 4
					},
					"Parts": [
						{
							"Type": "Lit",
							"Pos": {
								"Offset": 2,
								"Line": 1,
								"Col": 3
							},
							"End": {
								"Offset": 3,
								"Line": 1,
								"Col": 4
							},
							"ValuePos": {
								"Offset": 2,
								"Line": 1,
								"Col": 3
							},
							"ValueEnd": {
								"Offset": 3,
								"Line": 1,
								"Col": 4
							},
							"Value": "2"
						}
					]
				}
			},
			"Position": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			}
		}
	]
}
-- comment.sh --
#
-- comment.sh.json --
{
	"Type": "File",
	"Pos": {
		"Offset": 0,
		"Line": 1,
		"Col": 1
	},
	"End": {
		"Offset": 1,
		"Line": 1,
		"Col": 2
	},
	"Last": [
		{
			"Pos": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			},
			"End": {
				"Offset": 1,
				"Line": 1,
				"Col": 2
			},
			"Hash": {
				"Offset": 0,
				"Line": 1,
				"Col": 1
			}
		}
	]
}


================================================
FILE: cmd/shfmt/testdata/script/walk.txtar
================================================
mkdir symlink/subdir
# Remember that the symlink target is relative to the symlink directory.
[symlink] symlink symlink/subdir/symlink-file -> ../../modify/ext-shebang.sh
[symlink] symlink symlink/symlink-shebang.sh -> ../modify/ext-shebang.sh
[symlink] cp modify/ext.bash symlink/target-ext-bash # a copy that won't be formatted on its own
[symlink] symlink symlink/symlink-ext.bash -> target-ext-bash
[symlink] symlink symlink/symlink-dir -> subdir
[symlink] symlink symlink/symlink-none -> nonexistent

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

-- skip/.git/ext.sh --
foo
-- skip/.svn/ext.sh --
foo
-- skip/.hg/ext.sh --
foo

-- error/parse-error.sh --
foo(


================================================
FILE: expand/arith.go
================================================
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"fmt"
	"strconv"
	"strings"

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

// TODO(v4): the arithmetic APIs should return int64 for portability with 32-bit systems,
// even if Bash only supports native int sizes.

func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) {
	switch expr := expr.(type) {
	case *syntax.Word:
		str, err := Literal(cfg, expr)
		if err != nil {
			return 0, err
		}
		// recursively fetch vars
		i := 0
		for syntax.ValidName(str) {
			val := cfg.envGet(str)
			if val == "" {
				break
			}
			if i++; i >= maxNameRefDepth {
				break
			}
			str = val
		}
		// default to 0
		return int(atoi(str)), nil
	case *syntax.ParenArithm:
		return Arithm(cfg, expr.X)
	case *syntax.UnaryArithm:
		switch expr.Op {
		case syntax.Inc, syntax.Dec:
			name := expr.X.(*syntax.Word).Lit()
			old := atoi(cfg.envGet(name))
			val := old
			if expr.Op == syntax.Inc {
				val++
			} else {
				val--
			}
			if err := cfg.envSet(name, strconv.FormatInt(val, 10)); err != nil {
				return 0, err
			}
			if expr.Post {
				return int(old), nil
			}
			return int(val), nil
		}
		val, err := Arithm(cfg, expr.X)
		if err != nil {
			return 0, err
		}
		switch expr.Op {
		case syntax.Not:
			return oneIf(val == 0), nil
		case syntax.BitNegation:
			return ^val, nil
		case syntax.Plus:
			return val, nil
		case syntax.Minus:
			return -val, nil
		default:
			return 0, fmt.Errorf("unsupported unary arithmetic operator: %q", expr.Op)
		}
	case *syntax.BinaryArithm:
		switch expr.Op {
		case syntax.Assgn, syntax.AddAssgn, syntax.SubAssgn,
			syntax.MulAssgn, syntax.QuoAssgn, syntax.RemAssgn,
			syntax.AndAssgn, syntax.OrAssgn, syntax.XorAssgn,
			syntax.ShlAssgn, syntax.ShrAssgn:
			return cfg.assgnArit(expr)
		case syntax.TernQuest: // TernColon can't happen here
			cond, err := Arithm(cfg, expr.X)
			if err != nil {
				return 0, err
			}
			b2 := expr.Y.(*syntax.BinaryArithm) // must have Op==TernColon
			if cond == 1 {
				return Arithm(cfg, b2.X)
			}
			return Arithm(cfg, b2.Y)
		}
		left, err := Arithm(cfg, expr.X)
		if err != nil {
			return 0, err
		}
		right, err := Arithm(cfg, expr.Y)
		if err != nil {
			return 0, err
		}
		return binArit(expr.Op, left, right)
	default:
		panic(fmt.Sprintf("unexpected arithm expr: %T", expr))
	}
}

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

// atoi is like [strconv.ParseInt](s, 10, 64), but it ignores errors and trims whitespace.
func atoi(s string) int64 {
	s = strings.TrimSpace(s)
	n, _ := strconv.ParseInt(s, 10, 64)
	return n
}

func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) {
	name := b.X.(*syntax.Word).Lit()
	val := atoi(cfg.envGet(name))
	arg_, err := Arithm(cfg, b.Y)
	if err != nil {
		return 0, err
	}
	arg := int64(arg_)
	switch b.Op {
	case syntax.Assgn:
		val = arg
	case syntax.AddAssgn:
		val += arg
	case syntax.SubAssgn:
		val -= arg
	case syntax.MulAssgn:
		val *= arg
	case syntax.QuoAssgn:
		if arg == 0 {
			return 0, fmt.Errorf("division by zero")
		}
		val /= arg
	case syntax.RemAssgn:
		if arg == 0 {
			return 0, fmt.Errorf("division by zero")
		}
		val %= arg
	case syntax.AndAssgn:
		val &= arg
	case syntax.OrAssgn:
		val |= arg
	case syntax.XorAssgn:
		val ^= arg
	case syntax.ShlAssgn:
		val <<= uint(arg)
	case syntax.ShrAssgn:
		val >>= uint(arg)
	}
	if err := cfg.envSet(name, strconv.FormatInt(val, 10)); err != nil {
		return 0, err
	}
	return int(val), nil
}

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

func binArit(op syntax.BinAritOperator, x, y int) (int, error) {
	switch op {
	case syntax.Add:
		return x + y, nil
	case syntax.Sub:
		return x - y, nil
	case syntax.Mul:
		return x * y, nil
	case syntax.Quo:
		if y == 0 {
			return 0, fmt.Errorf("division by zero")
		}
		return x / y, nil
	case syntax.Rem:
		if y == 0 {
			return 0, fmt.Errorf("division by zero")
		}
		return x % y, nil
	case syntax.Pow:
		return intPow(x, y), nil
	case syntax.Eql:
		return oneIf(x == y), nil
	case syntax.Gtr:
		return oneIf(x > y), nil
	case syntax.Lss:
		return oneIf(x < y), nil
	case syntax.Neq:
		return oneIf(x != y), nil
	case syntax.Leq:
		return oneIf(x <= y), nil
	case syntax.Geq:
		return oneIf(x >= y), nil
	case syntax.And:
		return x & y, nil
	case syntax.Or:
		return x | y, nil
	case syntax.Xor:
		return x ^ y, nil
	case syntax.Shr:
		return x >> uint(y), nil
	case syntax.Shl:
		return x << uint(y), nil
	case syntax.AndArit:
		return oneIf(x != 0 && y != 0), nil
	case syntax.OrArit:
		return oneIf(x != 0 || y != 0), nil
	case syntax.Comma:
		// x is executed but its result discarded
		return y, nil
	default:
		return 0, fmt.Errorf("unsupported binary arithmetic operator: %q", op)
	}
}


================================================
FILE: expand/braces.go
================================================
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"strconv"
	"strings"

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

// Braces performs brace expansion on a word, given that it contains any
// [syntax.BraceExp] parts. For example, the word with a brace expansion
// "foo{bar,baz}" will return two literal words, "foobar" and "foobaz".
//
// Note that the resulting words may share word parts.
func Braces(word *syntax.Word) []*syntax.Word {
	var all []*syntax.Word
	var left []syntax.WordPart
	for i, wp := range word.Parts {
		br, ok := wp.(*syntax.BraceExp)
		if !ok {
			left = append(left, wp)
			continue
		}
		if br.Sequence {
			chars := false

			fromLit := br.Elems[0].Lit()
			toLit := br.Elems[1].Lit()
			zeros := max(extraLeadingZeros(fromLit), extraLeadingZeros(toLit))

			from, err1 := strconv.Atoi(fromLit)
			to, err2 := strconv.Atoi(toLit)
			if err1 != nil || err2 != nil {
				chars = true
				from = int(br.Elems[0].Lit()[0])
				to = int(br.Elems[1].Lit()[0])
			}
			upward := from <= to
			incr := 1
			if !upward {
				incr = -1
			}
			if len(br.Elems) > 2 {
				n, _ := strconv.Atoi(br.Elems[2].Lit())
				if n != 0 && n > 0 == upward {
					incr = n
				}
			}
			n := from
			for {
				if upward && n > to {
					break
				}
				if !upward && n < to {
					break
				}
				next := *word
				next.Parts = next.Parts[i+1:]
				lit := &syntax.Lit{}
				if chars {
					lit.Value = string(rune(n))
				} else {
					lit.Value = strings.Repeat("0", zeros) + strconv.Itoa(n)
				}
				next.Parts = append([]syntax.WordPart{lit}, next.Parts...)
				exp := Braces(&next)
				for _, w := range exp {
					w.Parts = append(left, w.Parts...)
				}
				all = append(all, exp...)
				n += incr
			}
			return all
		}
		for _, elem := range br.Elems {
			next := *word
			next.Parts = next.Parts[i+1:]
			next.Parts = append(elem.Parts, next.Parts...)
			exp := Braces(&next)
			for _, w := range exp {
				w.Parts = append(left, w.Parts...)
			}
			all = append(all, exp...)
		}
		return all
	}
	return []*syntax.Word{{Parts: left}}
}

func extraLeadingZeros(s string) int {
	for i, r := range s {
		if r != '0' {
			return i
		}
	}
	return 0 // "0" has no extra leading zeros
}


================================================
FILE: expand/braces_test.go
================================================
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"bytes"
	"testing"

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

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

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

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

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

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

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

func printWords(words ...*syntax.Word) string {
	p := syntax.NewPrinter()
	var buf bytes.Buffer
	call := &syntax.CallExpr{Args: words}
	p.Print(&buf, call)
	return buf.String()
}


================================================
FILE: expand/doc.go
================================================
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package expand contains code to perform various shell expansions.
package expand


================================================
FILE: expand/environ.go
================================================
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

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

// Environ is the base interface for a shell's environment, allowing it to fetch
// variables by name and to iterate over all the currently set variables.
type Environ interface {
	// Get retrieves a variable by its name. To check if the variable is
	// set, use Variable.IsSet.
	Get(name string) Variable

	// TODO(v4): make Each below a func that returns an iterator.

	// Each iterates over all the currently set variables, calling the
	// supplied function on each variable. Iteration is stopped if the
	// function returns false.
	//
	// The names used in the calls aren't required to be unique or sorted.
	// If a variable name appears twice, the latest occurrence takes
	// priority.
	//
	// Each is required to forward exported variables when executing
	// programs.
	Each(func(name string, vr Variable) bool)
}

// TODO(v4): [WriteEnviron.Set] below is overloaded to the point that correctly
// implementing both sides of the interface is tricky. In particular, some operations
// such as `export foo` or `readonly foo` alter the attributes but not the value,
// and `foo=bar` or `foo=[3]=baz` alter the value but not the attributes.

// WriteEnviron is an extension on Environ that supports modifying and deleting
// variables.
type WriteEnviron interface {
	Environ
	// Set sets a variable by name. If !vr.IsSet(), the variable is being
	// unset; otherwise, the variable is being replaced.
	//
	// The given variable can have the kind [KeepValue] to replace an existing
	// variable's attributes without changing its value at all.
	// This is helpful to implement `readonly foo=bar; export foo`,
	// as the second declaration needs to clearly signal that the value is not modified.
	//
	// An error may be returned if the operation is invalid, such as if the
	// name is empty or if we're trying to overwrite a read-only variable.
	Set(name string, vr Variable) error
}

//go:generate go tool stringer -type=ValueKind

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

const (
	// Unknown is used for unset variables which do not have a kind yet.
	Unknown ValueKind = iota
	// String describes plain string variables, such as `foo=bar`.
	String
	// NameRef describes variables which reference another by name, such as `declare -n foo=foo2`.
	NameRef
	// Indexed describes indexed array variables, such as `foo=(bar baz)`.
	Indexed
	// Associative describes associative array variables, such as `foo=([bar]=x [baz]=y)`.
	Associative

	// KeepValue is used by [WriteEnviron.Set] to signal that we are changing attributes
	// about a variable, such as exporting it, without changing its value at all.
	KeepValue

	// Deprecated: use [Unknown], as tracking whether or not a variable is set
	// is now done via [Variable.Set].
	// Otherwise it was impossible to describe an unset variable with a known kind
	// such as `declare -A foo`.
	Unset = Unknown
)

// Variable describes a shell variable, which can have a number of attributes
// and a value.
type Variable struct {
	// Set is true when the variable has been set to a value,
	// which may be empty.
	Set bool

	Local    bool
	Exported bool
	ReadOnly bool

	// Kind defines which of the value fields below should be used.
	Kind ValueKind

	Str  string            // Used when Kind is String or NameRef.
	List []string          // Used when Kind is Indexed.
	Map  map[string]string // Used when Kind is Associative.
}

// IsSet reports whether the variable has been set to a value.
// The zero value of a Variable is unset.
func (v Variable) IsSet() bool {
	return v.Set
}

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

// Flags returns the variable's attribute flags in the order used by bash's
// declare builtin and ${var@a}: type (a/A/n), readonly (r), exported (x).
func (v Variable) Flags() string {
	var flags []byte
	switch v.Kind {
	case Indexed:
		flags = append(flags, 'a')
	case Associative:
		flags = append(flags, 'A')
	case NameRef:
		flags = append(flags, 'n')
	}
	if v.ReadOnly {
		flags = append(flags, 'r')
	}
	if v.Exported {
		flags = append(flags, 'x')
	}
	return string(flags)
}

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

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

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

// FuncEnviron wraps a function mapping variable names to their string values,
// and implements [Environ]. Empty strings returned by the function will be
// treated as unset variables. All variables will be exported.
//
// Note that the returned Environ's Each method will be a no-op.
func FuncEnviron(fn func(string) string) Environ {
	return funcEnviron(fn)
}

type funcEnviron func(string) string

func (f funcEnviron) Get(name string) Variable {
	value := f(name)
	if value == "" {
		return Variable{}
	}
	return Variable{Set: true, Exported: true, Kind: String, Str: value}
}

func (f funcEnviron) Each(func(name string, vr Variable) bool) {}

// ListEnviron returns an [Environ] with the supplied variables, in the form
// "key=value". All variables will be exported. The last value in pairs is used
// if multiple values are present.
//
// On Windows, where environment variable names are case-insensitive, the
// resulting variable names will all be uppercase.
func ListEnviron(pairs ...string) Environ {
	return listEnviron_(runtime.GOOS == "windows", pairs...)
}

// listEnviron_ implements [ListEnviron], but letting the tests specify
// whether to uppercase all names or not.
func listEnviron_(caseInsensitive bool, pairs ...string) Environ {
	list := slices.Clone(pairs)
	env := listEnviron{caseInsensitive: caseInsensitive}
	slices.SortStableFunc(list, func(a, b string) int {
		isep := strings.IndexByte(a, '=')
		jsep := strings.IndexByte(b, '=')
		if isep < 0 {
			isep = 0
		} else {
			isep += 1
		}
		if jsep < 0 {
			jsep = 0
		} else {
			jsep += 1
		}
		return env.compare(a[:isep], b[:jsep])
	})

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

// listEnviron is a sorted list of "name=value" strings.
type listEnviron struct {
	caseInsensitive bool
	pairs           []string
}

func (l listEnviron) compare(a, b string) int {
	if l.caseInsensitive {
		// This is not particularly efficient, but it does the job.
		// If we had a cmp-compatible version of [strings.EqualFold], we'd use it.
		a = strings.ToUpper(a)
		b = strings.ToUpper(b)
	}
	return strings.Compare(a, b)
}

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

func (l listEnviron) Each(fn func(name string, vr Variable) bool) {
	for _, pair := range l.pairs {
		name, value, ok := strings.Cut(pair, "=")
		if !ok {
			// should never happen; see listEnvironWithUpper
			panic("expand.listEnviron: did not expect malformed name-value pair: " + pair)
		}
		if !fn(name, Variable{Set: true, Exported: true, Kind: String, Str: value}) {
			return
		}
	}
}


================================================
FILE: expand/environ_test.go
================================================
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"reflect"
	"testing"
)

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

func TestGetWithSameSubPrefix(t *testing.T) {
	gotEnv := ListEnviron("GREETING=text1", "GREETING2=text2")
	got := gotEnv.Get("GREETING2").String()
	if got != "text2" {
		t.Fatalf("ListEnviron.Get(GREETING2) wanted text2, got %q", got)
	}
	got = gotEnv.Get("GREETING").String()
	if got != "text1" {
		t.Fatalf("ListEnviron.Get(GREETING) wanted text1, got %q", got)
	}
}


================================================
FILE: expand/expand.go
================================================
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

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

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

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

	// CmdSubst expands a command substitution node, writing its standard
	// output to the provided [io.Writer].
	//
	// If nil, encountering a command substitution will result in an
	// UnexpectedCommandError.
	CmdSubst func(io.Writer, *syntax.CmdSubst) error

	// ProcSubst expands a process substitution node.
	ProcSubst func(*syntax.ProcSubst) (string, error)

	// TODO(v4): replace ReadDir with ReadDir2.

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

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

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

	// DotGlob corresponds to the shell option which allows filenames beginning
	// with a dot to be matched by a pattern which does not begin with a dot.
	DotGlob bool

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

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

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

	// ExtGlob corresponds to the shell option which allows using extended
	// pattern matching features when performing pathname expansion (globbing).
	ExtGlob bool

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

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

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

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

var zeroConfig = &Config{}

// TODO: note that prepareConfig is modifying the user's config in place,
// which doesn't feel right - we should make a copy.

func prepareConfig(cfg *Config) *Config {
	cfg = cmp.Or(cfg, zeroConfig)
	cfg.Env = cmp.Or(cfg.Env, FuncEnviron(func(string) string { return "" }))

	cfg.ifs = " \t\n"
	if vr := cfg.Env.Get("IFS"); vr.IsSet() {
		cfg.ifs = vr.String()
	}

	if cfg.ReadDir != nil && cfg.ReadDir2 == nil {
		cfg.ReadDir2 = func(path string) ([]fs.DirEntry, error) {
			infos, err := cfg.ReadDir(path)
			if err != nil {
				return nil, err
			}
			entries := make([]fs.DirEntry, len(infos))
			for i, info := range infos {
				entries[i] = fs.FileInfoToDirEntry(info)
			}
			return entries, nil
		}
	}
	return cfg
}

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

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

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

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

func (cfg *Config) envSet(name, value string) error {
	wenv, ok := cfg.Env.(WriteEnviron)
	if !ok {
		return fmt.Errorf("environment is read-only")
	}
	return wenv.Set(name, Variable{Set: true, Kind: String, Str: value})
}

// Literal expands a single shell word. It is similar to [Fields], but the result
// is a single string. This is the behavior when a word is used as the value in
// a shell variable assignment, for example.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Literal(cfg *Config, word *syntax.Word) (string, error) {
	if word == nil {
		return "", nil
	}
	cfg = prepareConfig(cfg)
	field, err := cfg.wordField(word.Parts, quoteNone)
	if err != nil {
		return "", err
	}
	return cfg.fieldJoin(field), nil
}

// Document expands a single shell word as if it were a here-document body.
// It is similar to [Literal], but without brace expansion, tilde expansion, and
// globbing.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Document(cfg *Config, word *syntax.Word) (string, error) {
	if word == nil {
		return "", nil
	}
	cfg = prepareConfig(cfg)
	field, err := cfg.wordField(word.Parts, quoteHeredoc)
	if err != nil {
		return "", err
	}
	return cfg.fieldJoin(field), nil
}

// Pattern expands a single shell word as a pattern, using [pattern.QuoteMeta]
// on any non-quoted parts of the input word. The result can be used on
// [pattern.Regexp] directly.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Pattern(cfg *Config, word *syntax.Word) (string, error) {
	if word == nil {
		return "", nil
	}
	cfg = prepareConfig(cfg)
	field, err := cfg.wordField(word.Parts, quoteNone)
	if err != nil {
		return "", err
	}
	sb := cfg.strBuilder()
	for _, part := range field {
		if part.quote > quoteNone {
			sb.WriteString(pattern.QuoteMeta(part.val, 0))
		} else {
			sb.WriteString(part.val)
		}
	}
	return sb.String(), nil
}

// Format expands a format string with a number of arguments, following the
// shell's format specifications. These include printf(1), among others.
//
// The resulting string is returned, along with the number of arguments used.
// Note that the resulting string may contain null bytes, for example
// if the format string used `\x00`. The caller should terminate the string
// at the first null byte if needed, such as when expanding for `$'foo\x00bar'`.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Format(cfg *Config, format string, args []string) (string, int, error) {
	cfg = prepareConfig(cfg)
	sb := cfg.strBuilder()

	consumed, err := formatInto(sb, format, args)
	if err != nil {
		return "", 0, err
	}

	return sb.String(), consumed, err
}

func formatInto(sb *strings.Builder, format string, args []string) (int, error) {
	var fmts []byte
	initialArgs := len(args)

	for i := 0; i < len(format); i++ {
		// readDigits reads from 0 to max digits, either octal or
		// hexadecimal.
		readDigits := func(max int, hex bool) string {
			j := 0
			for ; j < max && i+j < len(format); j++ {
				c := format[i+j]
				if (c >= '0' && c <= '9') ||
					(hex && c >= 'a' && c <= 'f') ||
					(hex && c >= 'A' && c <= 'F') {
					// valid octal or hex char
				} else {
					break
				}
			}
			digits := format[i : i+j]
			i += j - 1 // -1 since the outer loop does i++
			return digits
		}
		c := format[i]
		switch {
		case c == '\\': // escaped
			i++
			if i >= len(format) {
				sb.WriteByte('\\')
				break
			}
			switch c = format[i]; c {
			case 'a': // bell
				sb.WriteByte('\a')
			case 'b': // backspace
				sb.WriteByte('\b')
			case 'e', 'E': // escape
				sb.WriteByte('\x1b')
			case 'f': // form feed
				sb.WriteByte('\f')
			case 'n': // new line
				sb.WriteByte('\n')
			case 'r': // carriage return
				sb.WriteByte('\r')
			case 't': // horizontal tab
				sb.WriteByte('\t')
			case 'v': // vertical tab
				sb.WriteByte('\v')
			case '\\', '\'', '"', '?': // just the character
				sb.WriteByte(c)
			case '0', '1', '2', '3', '4', '5', '6', '7':
				digits := readDigits(3, false)
				// if digits don't fit in 8 bits, 0xff via strconv
				n, _ := strconv.ParseUint(digits, 8, 8)
				sb.WriteByte(byte(n))
			case 'x', 'u', 'U':
				i++
				max := 2
				switch c {
				case 'u':
					max = 4
				case 'U':
					max = 8
				}
				digits := readDigits(max, true)
				if len(digits) > 0 {
					// can't error
					n, _ := strconv.ParseUint(digits, 16, 32)
					if c == 'x' {
						// always as a single byte
						sb.WriteByte(byte(n))
					} else {
						sb.WriteRune(rune(n))
					}
					break
				}
				fallthrough
			default: // no escape sequence
				sb.WriteByte('\\')
				sb.WriteByte(c)
			}
		case len(fmts) > 0:
			switch c {
			case '%':
				sb.WriteByte('%')
				fmts = nil
			case 'c':
				var b byte
				if len(args) > 0 {
					arg := ""
					arg, args = args[0], args[1:]
					if len(arg) > 0 {
						b = arg[0]
					}
				}
				sb.WriteByte(b)
				fmts = nil
			case '+', '-', ' ':
				if len(fmts) > 1 {
					return 0, fmt.Errorf("invalid format char: %c", c)
				}
				fmts = append(fmts, c)
			case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
				fmts = append(fmts, c)
			case 's', 'b', 'd', 'i', 'u', 'o', 'x':
				arg := ""
				if len(args) > 0 {
					arg, args = args[0], args[1:]
				}
				var farg any
				if c == 'b' {
					// Passing in nil for args ensures that % format
					// strings aren't processed; only escape sequences
					// will be handled.
					_, err := formatInto(sb, arg, nil)
					if err != nil {
						return 0, err
					}
				} else if c != 's' {
					n, _ := strconv.ParseInt(arg, 0, 0)
					if c == 'i' || c == 'd' {
						farg = int(n)
					} else {
						farg = uint(n)
					}
					if c == 'i' || c == 'u' {
						c = 'd'
					}
				} else {
					farg = arg
				}
				if farg != nil {
					fmts = append(fmts, c)
					fmt.Fprintf(sb, string(fmts), farg)
				}
				fmts = nil
			default:
				return 0, fmt.Errorf("invalid format char: %c", c)
			}
		case args != nil && c == '%':
			// if args == nil, we are not doing format
			// arguments
			fmts = []byte{c}
		default:
			sb.WriteByte(c)
		}
	}
	if len(fmts) > 0 {
		return 0, fmt.Errorf("missing format char")
	}
	return initialArgs - len(args), nil
}

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

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

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

// FieldsSeq expands a number of words as if they were arguments in a shell
// command. This includes brace expansion, tilde expansion, parameter expansion,
// command substitution, arithmetic expansion, quote removal, and globbing.
func FieldsSeq(cfg *Config, words ...*syntax.Word) iter.Seq2[string, error] {
	cfg = prepareConfig(cfg)
	dir := cfg.envGet("PWD")
	return func(yield func(string, error) bool) {
		for _, word := range words {
			word := *word // make a copy, since SplitBraces replaces the Parts slice
			afterBraces := []*syntax.Word{&word}
			if syntax.SplitBraces(&word) {
				afterBraces = Braces(&word)
			}
			for _, word2 := range afterBraces {
				wfields, err := cfg.wordFields(word2.Parts)
				if err != nil {
					yield("", err)
					return
				}
				for _, field := range wfields {
					path, doGlob := cfg.escapedGlobField(field)
					if doGlob && cfg.ReadDir2 != nil {
						// Note that globbing requires keeping a slice state, so it doesn't
						// really benefit from using an iterator.
						matches, err := cfg.glob(dir, path)
						if err != nil {
							// We avoid [errors.As] as it allocates,
							// and we know that [Config.glob] returns [pattern.Regexp] errors without wrapping.
							if _, ok := err.(*pattern.SyntaxError); !ok {
								yield("", err)
								return
							}
						} else if len(matches) > 0 || cfg.NullGlob {
							for _, m := range matches {
								if !yield(m, nil) {
									return
								}
							}
							continue
						}
					}
					if !yield(cfg.fieldJoin(field), nil) {
						return
					}
				}
			}
		}
	}
}

type fieldPart struct {
	val   string
	quote quoteLevel
}

type quoteLevel uint

const (
	quoteNone quoteLevel = iota
	quoteDouble
	quoteHeredoc
	quoteSingle
)

func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) {
	var field []fieldPart
	for i, wp := range wps {
		switch wp := wp.(type) {
		case *syntax.Lit:
			s := wp.Value
			if i == 0 && ql == quoteNone {
				if prefix, rest := cfg.expandUser(s, len(wps) > 1); prefix != "" {
					// TODO: return two separate fieldParts,
					// like in wordFields?
					s = prefix + rest
				}
			}
			if (ql == quoteDouble || ql == quoteHeredoc) && strings.Contains(s, "\\") {
				sb := cfg.strBuilder()
				for i := 0; i < len(s); i++ {
					b := s[i]
					if b == '\\' && i+1 < len(s) {
						switch s[i+1] {
						case '"':
							if ql != quoteDouble {
								break
							}
							fallthrough
						case '\\', '$', '`': // special chars
							i++
							b = s[i] // write the special char, skipping the backslash
						}
					}
					sb.WriteByte(b)
				}
				s = sb.String()
			}
			s, _, _ = strings.Cut(s, "\x00") // TODO: why is this needed?
			field = append(field, fieldPart{val: s})
		case *syntax.SglQuoted:
			fp := fieldPart{quote: quoteSingle, val: wp.Value}
			if wp.Dollar {
				fp.val, _, _ = Format(cfg, fp.val, nil)
				fp.val, _, _ = strings.Cut(fp.val, "\x00") // cut the string if format included \x00
			}
			field = append(field, fp)
		case *syntax.DblQuoted:
			wfield, err := cfg.wordField(wp.Parts, quoteDouble)
			if err != nil {
				return nil, err
			}
			for _, part := range wfield {
				part.quote = quoteDouble
				field = append(field, part)
			}
		case *syntax.ParamExp:
			val, err := cfg.paramExp(wp)
			if err != nil {
				return nil, err
			}
			field = append(field, fieldPart{val: val})
		case *syntax.CmdSubst:
			val, err := cfg.cmdSubst(wp)
			if err != nil {
				return nil, err
			}
			field = append(field, fieldPart{val: val})
		case *syntax.ArithmExp:
			n, err := Arithm(cfg, wp.X)
			if err != nil {
				return nil, err
			}
			field = append(field, fieldPart{val: strconv.Itoa(n)})
		case *syntax.ProcSubst:
			path, err := cfg.ProcSubst(wp)
			if err != nil {
				return nil, err
			}
			field = append(field, fieldPart{val: path})
		case *syntax.ExtGlob:
			// Like how [Config.wordFields] deals with [syntax.ExtGlob],
			// except that we allow these through even when [Config.ExtGlob]
			// is false, as it only applies to pathname expansion.
			field = append(field, fieldPart{val: wp.Op.String() + wp.Pattern.Value + ")"})
		default:
			panic(fmt.Sprintf("unhandled word part: %T", wp))
		}
	}
	return field, nil
}

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

func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
	fields := cfg.fieldsAlloc[:0]
	curField := cfg.fieldAlloc[:0]
	allowEmpty := false
	flush := func() {
		if len(curField) == 0 {
			return
		}
		fields = append(fields, curField)
		curField = nil
	}
	splitAdd := func(val string) {
		fieldStart := -1
		for i, r := range val {
			if cfg.ifsRune(r) {
				if fieldStart >= 0 { // ending a field
					curField = append(curField, fieldPart{val: val[fieldStart:i]})
					fieldStart = -1
				}
				flush()
			} else {
				if fieldStart < 0 { // starting a new field
					fieldStart = i
				}
			}
		}
		if fieldStart >= 0 { // ending a field without IFS
			curField = append(curField, fieldPart{val: val[fieldStart:]})
		}
	}
	for i, wp := range wps {
		switch wp := wp.(type) {
		case *syntax.Lit:
			s := wp.Value
			if i == 0 {
				prefix, rest := cfg.expandUser(s, len(wps) > 1)
				curField = append(curField, fieldPart{
					quote: quoteSingle,
					val:   prefix,
				})
				s = rest
			}
			if strings.Contains(s, "\\") {
				sb := cfg.strBuilder()
				for i := 0; i < len(s); i++ {
					b := s[i]
					if b == '\\' {
						if i++; i >= len(s) {
							break
						}
						b = s[i]
					}
					sb.WriteByte(b)
				}
				s = sb.String()
			}
			curField = append(curField, fieldPart{val: s})
		case *syntax.SglQuoted:
			allowEmpty = true
			fp := fieldPart{quote: quoteSingle, val: wp.Value}
			if wp.Dollar {
				fp.val, _, _ = Format(cfg, fp.val, nil)
				fp.val, _, _ = strings.Cut(fp.val, "\x00") // cut the string if format included \x00
			}
			curField = append(curField, fp)
		case *syntax.DblQuoted:
			if len(wp.Parts) == 1 {
				pe, _ := wp.Parts[0].(*syntax.ParamExp)
				if elems := cfg.quotedElemFields(pe); elems != nil {
					for i, elem := range elems {
						if i > 0 {
							flush()
						}
						curField = append(curField, fieldPart{
							quote: quoteDouble,
							val:   elem,
						})
					}
					continue
				}
			}
			allowEmpty = true
			wfield, err := cfg.wordField(wp.Parts, quoteDouble)
			if err != nil {
				return nil, err
			}
			for _, part := range wfield {
				part.quote = quoteDouble
				curField = append(curField, part)
			}
		case *syntax.ParamExp:
			val, err := cfg.paramExp(wp)
			if err != nil {
				return nil, err
			}
			splitAdd(val)
		case *syntax.CmdSubst:
			val, err := cfg.cmdSubst(wp)
			if err != nil {
				return nil, err
			}
			splitAdd(val)
		case *syntax.ArithmExp:
			n, err := Arithm(cfg, wp.X)
			if err != nil {
				return nil, err
			}
			curField = append(curField, fieldPart{val: strconv.Itoa(n)})
		case *syntax.ProcSubst:
			path, err := cfg.ProcSubst(wp)
			if err != nil {
				return nil, err
			}
			splitAdd(path)
		case *syntax.ExtGlob:
			if !cfg.ExtGlob {
				return nil, fmt.Errorf("extended globbing operator used without the \"extglob\" option set")
			}
			// We don't translate or interpret the pattern here in any way;
			// that's done later when globbing takes place via [pattern.Regexp].
			// Here, all we do is keep the extended globbing expression in string form.
			//
			// TODO(v4): perhaps the syntax parser should keep extended globbing expressions
			// as plain literal strings, because a custom node is not particularly helpful.
			// It's not like other globbing operators like `*` or `**` get their own nodes.
			curField = append(curField, fieldPart{val: wp.Op.String() + wp.Pattern.Value + ")"})
		default:
			panic(fmt.Sprintf("unhandled word part: %T", wp))
		}
	}
	flush()
	if allowEmpty && len(fields) == 0 {
		fields = append(fields, curField)
	}
	return fields, nil
}

// quotedElemFields returns the list of elements resulting from a quoted
// parameter expansion that should be treated especially, like "${foo[@]}".
func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string {
	if pe == nil || pe.Length || pe.Width || pe.IsSet {
		return nil
	}
	name := pe.Param.Value
	if pe.Excl {
		switch pe.Names {
		case syntax.NamesPrefixWords: // "${!prefix@}"
			return cfg.namesByPrefix(pe.Param.Value)
		case syntax.NamesPrefix: // "${!prefix*}"
			return nil
		}
		switch nodeLit(pe.Index) {
		case "@": // "${!name[@]}"
			switch vr := cfg.Env.Get(name); vr.Kind {
			case Indexed:
				// TODO: if an indexed array only has elements 0 and 10,
				// we should not return all indices in between those.
				keys := make([]string, 0, len(vr.List))
				for key := range vr.List {
					keys = append(keys, strconv.Itoa(key))
				}
				return keys
			case Associative:
				return slices.Collect(maps.Keys(vr.Map))
			}
		}
		return nil
	}
	switch name {
	case "*": // "${*}" or "${*:offset:length}"
		return []string{cfg.ifsJoin(cfg.sliceElems(pe, cfg.Env.Get(name).List, true))}
	case "@": // "${@}" or "${@:offset:length}"
		return cfg.sliceElems(pe, cfg.Env.Get(name).List, true)
	}
	switch nodeLit(pe.Index) {
	case "@": // "${name[@]}"
		vr := cfg.Env.Get(name)
		switch vr.Kind {
		case Indexed:
			return cfg.sliceElems(pe, vr.List, false)
		case Associative:
			return slices.Collect(maps.Values(vr.Map))
		case Unknown:
			if !vr.IsSet() {
				// An unset variable expanded as "${name[@]}" produces
				// zero fields, just like an empty array.
				return []string{}
			}
		}
	case "*": // "${name[*]}"
		if vr := cfg.Env.Get(name); vr.Kind == Indexed {
			return []string{cfg.ifsJoin(cfg.sliceElems(pe, vr.List, false))}
		}
	}
	return nil
}

// sliceElems applies ${var:offset:length} slicing to a list of elements.
// When positional is true, $0 is prepended to the list before slicing.
// In bash, positional parameter offsets ($@ and $*) are 1-based and
// offset 0 includes $0 (the shell or script name). Negative offsets
// count from $# + 1, so $0 is reachable via large enough negative values.
func (cfg *Config) sliceElems(pe *syntax.ParamExp, elems []string, positional bool) []string {
	if pe.Slice == nil {
		return elems
	}
	if positional {
		elems = append([]string{cfg.Env.Get("0").Str}, elems...)
	}
	slicePos := func(n int) int {
		if n < 0 {
			n = len(elems) + n
			if n < 0 {
				n = len(elems)
			}
		} else if n > len(elems) {
			n = len(elems)
		}
		return n
	}
	if pe.Slice.Offset != nil {
		offset, err := Arithm(cfg, pe.Slice.Offset)
		if err != nil {
			return elems
		}
		elems = elems[slicePos(offset):]
	}
	if pe.Slice.Length != nil {
		length, err := Arithm(cfg, pe.Slice.Length)
		if err != nil {
			return elems
		}
		elems = elems[:slicePos(length)]
	}
	return elems
}

func (cfg *Config) expandUser(field string, moreFields bool) (prefix, rest string) {
	name, ok := strings.CutPrefix(field, "~")
	if !ok {
		// No tilde prefix to expand, e.g. "foo".
		return "", field
	}
	i := strings.IndexByte(name, '/')
	if i < 0 && moreFields {
		// There is a tilde prefix, but followed by more fields, e.g. "~'foo'".
		// We only proceed if an unquoted slash was found in this field, e.g. "~/'foo'".
		return "", field
	}
	if i >= 0 {
		rest = name[i:]
		name = name[:i]
	}
	if name == "" {
		// Current user; try via "HOME", otherwise fall back to the
		// system's appropriate home dir env var. Don't use os/user, as
		// that's overkill. We can't use [os.UserHomeDir], because we want
		// to use cfg.Env, and we always want to check "HOME" first.

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

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

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

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

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

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

var (
	rxGlobStar        = regexp.MustCompile(`^[^/.][^/]*$`)
	rxGlobStarDotGlob = regexp.MustCompile(`^[^/]*$`)
)

// pathJoin2 is a simpler version of [filepath.Join] without cleaning the result,
// since that's needed for globbing.
func pathJoin2(elem1, elem2 string) string {
	if elem1 == "" {
		return elem2
	}
	if strings.HasSuffix(elem1, string(filepath.Separator)) {
		return elem1 + elem2
	}
	return elem1 + string(filepath.Separator) + elem2
}

// pathSplit splits a file path into its elements, retaining empty ones. Before
// splitting, slashes are replaced with [filepath.Separator], so that splitting
// Unix paths on Windows works as well.
func pathSplit(path string) []string {
	path = filepath.FromSlash(path)
	return strings.Split(path, string(filepath.Separator))
}

func (cfg *Config) glob(base, pat string) ([]string, error) {
	parts := pathSplit(pat)
	matches := []string{""}
	if filepath.IsAbs(pat) {
		if parts[0] == "" {
			// unix-like
			matches[0] = string(filepath.Separator)
		} else {
			// windows (for some reason it won't work without the
			// trailing separator)
			matches[0] = parts[0] + string(filepath.Separator)
		}
		parts = parts[1:]
	}
	// TODO: as an optimization, we could do chunks of the path all at once,
	// like doing a single stat for "/foo/bar" in "/foo/bar/*".

	// TODO: Another optimization would be to reduce the number of ReadDir2 calls.
	// For example, /foo/* can end up doing one duplicate call:
	//
	//    ReadDir2("/foo") to ensure that "/foo/" exists and only matches a directory
	//    ReadDir2("/foo") glob "*"

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

		wantDir := i < len(parts)-1
		switch {
		case part == "", part == ".", part == "..":
			for i, dir := range matches {
				matches[i] = pathJoin2(dir, part)
			}
			continue
		case !pattern.HasMeta(part, 0):
			var newMatches []string
			for _, dir := range matches {
				match := dir
				if !filepath.IsAbs(match) {
					match = filepath.Join(base, match)
				}
				match = pathJoin2(match, part)
				// We can't use [Config.ReadDir2] on the parent and match the directory
				// entry by name, because short paths on Windows break that.
				// Our only option is to [Config.ReadDir2] on the directory entry itself,
				// which can be wasteful if we only want to see if it exists,
				// but at least it's correct in all scenarios.
				if _, err := cfg.ReadDir2(match); err != nil {
					if isWindowsErrPathNotFound(err) {
						// Unfortunately, [os.File.Readdir] on a regular file on
						// Windows returns an error that satisfies [fs.ErrNotExist].
						// Luckily, it returns a special "path not found" rather
						// than the normal "file not found" for missing files,
						// so we can use that knowledge to work around the bug.
						// See https://github.com/golang/go/issues/46734.
						// TODO: remove when the Go issue above is resolved.
					} else if errors.Is(err, fs.ErrNotExist) {
						continue // simply doesn't exist
					}
					if wantDir {
						continue // exists but not a directory
					}
				}
				newMatches = append(newMatches, pathJoin2(dir, part))
			}
			matches = newMatches
			continue
		case part == "**" && cfg.GlobStar:
			// Find all recursive matches for "**".
			// Note that we need the results to be in depth-first order,
			// and to avoid recursion, we use a slice as a stack.
			// Since we pop from the back, we populate the stack backwards.
			stack := make([]string, 0, len(matches))
			for _, match := range slices.Backward(matches) {
				// "a/**" should match "a/ a/b a/b/cfg ...";
				// note how the zero-match case there has a trailing separator.
				stack = append(stack, pathJoin2(match, ""))
			}
			matches = matches[:0]
			var newMatches []string // to reuse its capacity
			for len(stack) > 0 {
				dir := stack[len(stack)-1]
				stack = stack[:len(stack)-1]
				matches = append(matches, dir)

				// If dir is not a directory, we keep the stack as-is and continue.
				newMatches = newMatches[:0]
				rx := rxGlobStar.MatchString
				if cfg.DotGlob {
					rx = rxGlobStarDotGlob.MatchString
				}
				newMatches, _ = cfg.globDir(base, dir, rx, wantDir, newMatches)
				for _, match := range slices.Backward(newMatches) {
					stack = append(stack, match)
				}
			}
			continue
		}
		mode := pattern.Filenames | pattern.EntireString | pattern.NoGlobStar
		if cfg.NoCaseGlob {
			mode |= pattern.NoGlobCase
		}
		if cfg.DotGlob {
			mode |= pattern.GlobLeadingDot
		}
		if cfg.ExtGlob {
			mode |= pattern.ExtendedOperators
		}
		matcher, err := internal.ExtendedPatternMatcher(part, mode)
		if err != nil {
			return nil, err
		}
		var newMatches []string
		for _, dir := range matches {
			newMatches, err = cfg.globDir(base, dir, matcher, wantDir, newMatches)
			if err != nil {
				return nil, err
			}
		}
		matches = newMatches
	}
	// Note that the results need to be sorted.
	// TODO: above we do a BFS; if we did a DFS, the matches would already be sorted.
	slices.Sort(matches)
	// Remove any empty matches left behind from "**".
	if len(matches) > 0 && matches[0] == "" {
		matches = matches[1:]
	}
	return matches, nil
}

func (cfg *Config) globDir(base, dir string, matcher func(string) bool, wantDir bool, matches []string) ([]string, error) {
	fullDir := dir
	if !filepath.IsAbs(dir) {
		fullDir = filepath.Join(base, dir)
	}
	infos, err := cfg.ReadDir2(fullDir)
	if err != nil {
		// We still want to return matches, for the sake of reusing slices.
		return matches, err
	}
	for _, info := range infos {
		name := info.Name()
		if !wantDir {
			// No filtering.
		} else if mode := info.Type(); mode&os.ModeSymlink != 0 {
			// We need to know if the symlink points to a directory.
			// This requires an extra syscall, as [Config.ReadDir] on the parent directory
			// does not follow symlinks for each of the directory entries.
			// ReadDir is somewhat wasteful here, as we only want its error result,
			// but we could try to reuse its result as per the TODO in [Config.glob].
			if _, err := cfg.ReadDir2(filepath.Join(fullDir, info.Name())); err != nil {
				continue
			}
		} else if !mode.IsDir() {
			// Not a symlink nor a directory.
			continue
		}
		if matcher(name) {
			matches = append(matches, pathJoin2(dir, name))
		}
	}
	return matches, nil
}

// ReadFields splits and returns n fields from s, like the "read" shell builtin.
// If raw is set, backslash escape sequences are not interpreted.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func ReadFields(cfg *Config, s string, n int, raw bool) []string {
	cfg = prepareConfig(cfg)
	type pos struct {
		start, end int
	}
	var fpos []pos

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

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

	fields := make([]string, len(fpos))
	for i, p := range fpos {
		fields[i] = string(runes[p.start:p.end])
	}
	return fields
}


================================================
FILE: expand/expand_nonwindows.go
================================================
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

//go:build !windows

package expand

func isWindowsErrPathNotFound(error) bool { return false }


================================================
FILE: expand/expand_test.go
================================================
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

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

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

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

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

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

func Test_glob(t *testing.T) {
	cfg := &Config{
		ReadDir2: func(string) ([]fs.DirEntry, error) {
			return []fs.DirEntry{
				// The filenames here are sorted, just like [io/fs.ReadDirFS].
				&mockFileInfo{name: "A"},
				&mockFileInfo{name: "AB"},
				&mockFileInfo{name: "a"},
				&mockFileInfo{name: "ab"},
			}, nil
		},
	}

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

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

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

func (fi *mockFileInfo) Name() string      { return fi.name }
func (fi *mockFileInfo) Type() fs.FileMode { return fi.typ }


================================================
FILE: expand/expand_windows.go
================================================
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

import (
	"errors"
	"os"
	"syscall"
)

func isWindowsErrPathNotFound(err error) bool {
	var pathErr *os.PathError
	return errors.As(err, &pathErr) && pathErr.Err == syscall.ERROR_PATH_NOT_FOUND
}


================================================
FILE: expand/param.go
================================================
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package expand

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

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

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

// UnsetParameterError is returned when a parameter expansion encounters an
// unset variable and [Config.NoUnset] has been set.
type UnsetParameterError struct {
	Node    *syntax.ParamExp
	Message string
}

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

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

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

	name := pe.Param.Value
	index := pe.Index
	switch name {
	case "@", "*":
		index = &syntax.Word{Parts: []syntax.WordPart{
			&syntax.Lit{Value: name},
		}}
	}
	var vr Variable
	switch name {
	case "LINENO":
		// This is the only parameter expansion that the environment
		// interface cannot satisfy.
		line := uint64(cfg.curParam.Pos().Line())
		vr = Variable{Set: true, Kind: String, Str: strconv.FormatUint(line, 10)}
	default:
		vr = cfg.Env.Get(name)
	}
	orig := vr
	_, vr = vr.Resolve(cfg.Env)
	if cfg.NoUnset && !vr.IsSet() && !overridingUnset(pe) {
		return "", UnsetParameterError{
			Node:    pe,
			Message: "unbound variable",
		}
	}

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

	var (
		str   string
		elems []string

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

	switch nodeLit(index) {
	case "@", "*":
		switch vr.Kind {
		case Unknown:
			elems = nil
			indexAllElements = true
		case Indexed:
			indexAllElements = true
			callVarInd = false
			elems = cfg.sliceElems(pe, vr.List, name == "@" || name == "*")
			str = strings.Join(elems, " ")
		}
	}
	if callVarInd {
		var err error
		str, err = cfg.varInd(vr, index)
		if err != nil {
			return "", err
		}
	}
	if !indexAllElements {
		elems = []string{str}
	}

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

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

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

			for i, elem := range elems {
				rs := []rune(elem)
				for ri, r := range rs {
					if rx.MatchString(string(r)) {
						rs[ri] = caseFunc(r)
						if !all {
							break
						}
					}
				}
				elems[i] = string(rs)
			}
			str = strings.Join(elems, " ")
		case syntax.OtherParamOps:
			switch arg {
			case "Q":
				str, err = syntax.Quote(str, syntax.LangBash)
				if err != nil {
					// Is this even possible? If a user runs into this panic,
					// it's most likely a bug we need to fix.
					panic(err)
				}
			case "E":
				tail := str
				var rns []rune
				for tail != "" {
					var rn rune
					rn, _, tail, _ = strconv.UnquoteChar(tail, 0)
					rns = append(rns, rn)
				}
				str = string(rns)
			case "a":
				// ${var@a} returns variable attribute flags.
				// We use orig (before nameref resolve) for the attributes.
				str = orig.Flags()
			case "A":
				// ${var@A} returns a declare statement that recreates the variable.
				flags := orig.Flags()
				quoted, err := syntax.Quote(str, syntax.LangBash)
				if err != nil {
					return "", err
				}
				if flags == "" {
					str = fmt.Sprintf("%s=%s", name, quoted)
				} else {
					str = fmt.Sprintf("declare -%s %s=%s", flags, name, quoted)
				}
			case "P":
				// TODO: implement prompt expansion (\u, \h, \w, etc.).
			default:
				panic(fmt.Sprintf("unexpected @%s param expansion", arg))
			}
		}
	}
	return str, nil
}

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

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

func (cfg *Config) namesByPrefix(prefix string) []string {
	var names []string
	for name := range cfg.Env.Each {
		if strings.HasPrefix(name, prefix) {
			names = append(names, name)
		}
	}
	return names
}


================================================
FILE: expand/valuekind_string.go
================================================
// Code generated by "stringer -type=ValueKind"; DO NOT EDIT.

package expand

import "strconv"

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

const _ValueKind_name = "UnknownStringNameRefIndexedAssociativeKeepValue"

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

func (i ValueKind) String() string {
	idx := int(i) - 0
	if i < 0 || idx >= len(_ValueKind_index)-1 {
		return "ValueKind(" + strconv.FormatInt(int64(i), 10) + ")"
	}
	return _ValueKind_name[_ValueKind_index[idx]:_ValueKind_index[idx+1]]
}


================================================
FILE: fileutil/file.go
================================================
// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

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

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

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

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

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

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

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

const (
	// ConfNotScript describes files which are definitely not shell scripts,
	// such as non-regular files or files with a non-shell extension.
	ConfNotScript ScriptConfidence = iota

	// ConfIfShebang describes files which might be shell scripts, depending
	// on the shebang line in the file's contents. Since [CouldBeScript] only
	// works on [fs.FileInfo], the answer in this case can't be final.
	ConfIfShebang

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

// CouldBeScript is a shortcut for CouldBeScript2(fs.FileInfoToDirEntry(info)).
//
// Deprecated: prefer [CouldBeScript2], which usually requires fewer syscalls.
func CouldBeScript(info fs.FileInfo) ScriptConfidence {
	return CouldBeScript2(fs.FileInfoToDirEntry(info))
}

// CouldBeScript2 reports how likely a directory entry is to be a shell script.
// It discards directories and other non-regular files like symbolic links,
// filenames beginning with '.', and files with non-shell extensions.
func CouldBeScript2(entry fs.DirEntry) ScriptConfidence {
	name := entry.Name()
	switch {
	case name[0] == '.':
		return ConfNotScript // '.' prefix (hidden file)
	case !entry.Type().IsRegular():
		return ConfNotScript // dir, symlink, named pipes, etc
	case extRe.MatchString(name):
		return ConfIsScript // shell extension
	case strings.IndexByte(name, '.') > 0:
		return ConfNotScript // non-shell extension
	default:
		return ConfIfShebang // no extension; read and look for a shebang
	}
}


================================================
FILE: fileutil/file_test.go
================================================
// Copyright (c) 2025, Ville Skyttä <ville.skytta@iki.fi>
// See LICENSE for licensing information

package fileutil

import (
	"strings"
	"testing"
)

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

	for _, test := range tests {
		name := strings.ReplaceAll(strings.ReplaceAll(string(test.in), "\f", "\\f"), "\t", "\\t")
		t.Run(name, func(t *testing.T) {
			if got := Shebang(test.in); got != test.want {
				t.Fatalf("want %q, got %q", test.want, got)
			}
		})
	}
}


================================================
FILE: go.mod
================================================
module mvdan.cc/sh/v3

go 1.25.0

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

require (
	github.com/kr/pretty v0.3.1 // indirect
	github.com/kr/text v0.2.0 // indirect
	golang.org/x/mod v0.29.0 // indirect
	golang.org/x/sync v0.17.0 // indirect
	golang.org/x/tools v0.38.0 // indirect
)

tool golang.org/x/tools/cmd/stringer


================================================
FILE: go.sum
================================================
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw=
github.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
mvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=
mvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=


================================================
FILE: internal/pattern.go
================================================
// Copyright (c) 2026, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package internal

import (
	"errors"
	"fmt"
	"regexp"
	"strings"

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

// ExtendedPatternMatcher returns a [regexp.Regexp.MatchString]-like function
// to support !(pattern-list) extended patterns where possible.
// It can be used instead of [pattern.Regexp] for narrow use cases.
func ExtendedPatternMatcher(pat string, mode pattern.Mode) (func(string) bool, error) {
	if mode&pattern.ExtendedOperators != 0 && mode&pattern.EntireString == 0 {
		// In the future we could try to support !(pattern) without matching
		// the entire input, ensuring we add enough test cases.
		panic("ExtendedOperators is only supported with EntireString")
	}

	// Extended pattern matching operators are always on outside of pathname expansion.
	expr, err := pattern.Regexp(pat, mode)
	if err != nil {
		// Handle !(pattern-list) negation: when Regexp returns NegExtglobError,
		// match the inner pattern and negate the result.
		var negErr *pattern.NegExtGlobError
		if !errors.As(err, &negErr) {
			return nil, err
		}
		return extNegatedMatcher(pat, negErr.Groups)
	}
	rx := regexp.MustCompile(expr)
	return rx.MatchString, nil
}

// extNegatedMatcher handles !(pattern-list) extglob negation.
// Only a single !(...) group with fixed-string prefix and suffix is supported.
func extNegatedMatcher(pat string, groups []pattern.NegExtGlobGroup) (func(string) bool, error) {
	if len(groups) != 1 {
		return nil, fmt.Errorf("multiple extglob !(...) groups are not supported yet")
	}
	g := groups[0]
	prefix := pat[:g.Start]
	suffix := pat[g.End:]

	if pattern.HasMeta(prefix, 0) || pattern.HasMeta(suffix, 0) {
		return nil, fmt.Errorf("extglob !(...) is only supported with a fixed prefix and suffix")
	}

	// Use @(inner) to compile the pattern list, then negate the match.
	inner := pat[g.Start+len("!(") : g.End-len(")")]
	expr, err := pattern.Regexp("@("+inner+")", pattern.EntireString|pattern.ExtendedOperators)
	if err != nil {
		return nil, err
	}
	rx := regexp.MustCompile(expr)

	return func(name string) bool {
		if !strings.HasPrefix(name, prefix) {
			return false
		}
		if !strings.HasSuffix(name, suffix) {
			return false
		}
		end := len(name) - len(suffix)
		if end < len(prefix) {
			return false // prefix and suffix overlap in name
		}
		middle := name[len(prefix):end]

		return !rx.MatchString(middle)
	}, nil
}


================================================
FILE: internal/testing.go
================================================
// Copyright (c) 2026, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package internal

import (
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// TestMainSetup is used by the integration tests running shell scripts
// either via our interpreter or via real shells,
// to ensure a reasonably clean and consistent environment.
func TestMainSetup() {
	// Set the locale to computer-friendly English and UTF-8.
	// Some systems like macOS miss C.UTF8, so fall back to the US English locale.
	if out, _ := exec.Command("locale", "-a").Output(); strings.Contains(
		strings.ToLower(string(out)), "c.utf",
	) {
		os.Setenv("LANGUAGE", "C.UTF-8")
		os.Setenv("LC_ALL", "C.UTF-8")
	} else {
		os.Setenv("LANGUAGE", "en_US.UTF-8")
		os.Setenv("LC_ALL", "en_US.UTF-8")
	}

	// Bash prints the pwd after changing directories when CDPATH is set.
	os.Unsetenv("CDPATH")

	pathDir, err := os.MkdirTemp("", "interp-bin-")
	if err != nil {
		panic(err)
	}

	// These short names are commonly used as variables.
	// Ensure they are unset as env vars.
	// We can't easily remove names from $PATH,
	// so do the next best thing: override each name with a failing script.
	for _, s := range []string{
		"a", "b", "c", "d", "e", "f", "foo", "bar",
	} {
		os.Unsetenv(s)
		pathFile := filepath.Join(pathDir, s)
		if err := os.WriteFile(pathFile, []byte("#!/bin/sh\necho NO_SUCH_COMMAND; exit 1"), 0o777); err != nil {
			panic(err)
		}
	}

	os.Setenv("PATH", pathDir+string(os.PathListSeparator)+os.Getenv("PATH"))
}


================================================
FILE: interp/api.go
================================================
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

// Package interp implements an interpreter to execute shell programs
// parsed by the [syntax] package as either [syntax.LangBash]
// or [syntax.LangPOSIX], behaving like Bash as a result.
//
// The interpreter currently aims to behave like a non-interactive shell,
// which is how most shells run scripts, and is more useful to machines.
// In the future, it may gain an option to behave like an interactive shell.
package interp

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

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

// A Runner interprets shell programs. It can be reused, but it is not safe for
// concurrent use. Use [New] to build a new Runner.
//
// Note that writes to Stdout and Stderr may be concurrent if background
// commands are used. If you plan on using an [io.Writer] implementation that
// isn't safe for concurrent use, consider a workaround like hiding writes
// behind a mutex.
//
// Runner's exported fields are meant to be configured via [RunnerOption];
// once a Runner has been created, the fields should be treated as read-only.
type Runner struct {
	// Env specifies the initial environment for the interpreter, which must
	// not be nil. It can only be set via [Env].
	//
	// If it includes a TMPDIR variable describing an absolute directory,
	// it is used as the directory in which to create temporary files needed
	// for the interpreter's use, such as named pipes for process substitutions.
	// Otherwise, [os.TempDir] is used.
	Env expand.Environ

	// writeEnv overlays [Runner.Env] so that we can write environment variables
	// as an overlay.
	writeEnv expand.WriteEnviron

	// Dir specifies the working directory of the command, which must be an
	// absolute path. It can only be set via [Dir].
	Dir string

	// tempDir is either $TMPDIR from [Runner.Env], or [os.TempDir].
	tempDir string

	// Params are the current shell parameters, e.g. from running a shell
	// file or calling a function. Accessible via the $@/$* family of vars.
	// It can only be set via [Params].
	Params []string

	// Separate maps - note that bash allows a name to be both a var and a
	// func simultaneously.
	// Vars is mostly superseded by Env at this point.
	// TODO(v4): remove these

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

	alias map[string]alias

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

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

	// execMiddlewares grows with calls to [ExecHandlers],
	// and is used to construct execHandler when Reset is first called.
	// The slice is needed to preserve the relative order of middlewares.
	execMiddlewares []func(ExecHandlerFunc) ExecHandlerFunc

	// openHandler is a function responsible for opening files. It must not be nil.
	openHandler OpenHandlerFunc

	// readDirHandler is a function responsible for reading directories during
	// glob expansion. It must be non-nil.
	readDirHandler ReadDirHandlerFunc2

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

	stdin  *os.File // e.g. the read end of a pipe
	stdout io.Writer
	stderr io.Writer

	ecfg *expand.Config
	ectx context.Context // just so that Runner.Subshell can use it again

	// didReset remembers whether the runner has ever been reset. This is
	// used so that Reset is automatically called when running any program
	// or node for the first time on a Runner.
	didReset bool

	usedNew bool

	filename string // only if Node was a File

	// >0 to break or continue out of N enclosing loops
	breakEnclosing, contnEnclosing int

	inLoop       bool
	inFunc       bool
	inSource     bool
	handlingTrap bool // whether we're currently in a trap callback

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

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

	// The current and last exit statuses. They can only be different if
	// the interpreter is in the middle of running a statement. In that
	// scenario, 'exit' is the status for the current statement being run,
	// and 'lastExit' corresponds to the previous statement that was run.
	exit     exitStatus
	lastExit exitStatus

	lastExpandExit exitStatus // used to surface exit statuses while expanding fields

	// bgProcs holds all background shells spawned by this runner.
	// Their PIDs are 1-indexed, from 1 to len(bgProcs), with a "g" prefix
	// to distinguish them from real PIDs on the host operating system.
	//
	// Note that each shell only tracks its direct children;
	// subshells do not share nor inherit the background PIDs they can wait for.
	bgProcs []bgProc

	opts runnerOpts

	origDir    string
	origParams []string
	origOpts   runnerOpts
	origStdin  *os.File
	origStdout io.Writer
	origStderr io.Writer

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

	optState getopts

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

	// Fake signal callbacks
	callbackErr  string
	callbackExit string
}

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

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

	// err holds the error information for a non-zero exit status code or fatal error.
	// Used so that running a single statement with a custom handler
	// which returns a non-fatal Go error, such as a Go error wrapping [NewExitStatus],
	// can be returned by [Runner.Run] without being lost entirely.
	err error
}

// clear sets the exit status code and error to zero, as long as the exit status
// was not set by `return`, `exit`, or a fatal error.
func (e *exitStatus) clear() {
	if e.returning || e.exiting || e.fatalExit {
		return
	}
	e.code = 0
	e.err = nil
}

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

// oneIf sets the exit status code to 1 if b is true.
// Note that it assumes the exit status hasn't been set yet,
// meaning that [exitStatus.code] and [exitStatus.err] are zero values.
func (e *exitStatus) oneIf(b bool) {
	if b {
		e.code = 1
	}
}

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

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

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

	exit *exitStatus
}

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

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

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

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

// RunnerOption can be passed to [New] to alter a [Runner]'s behaviour.
// It can also be applied directly on an existing Runner,
// such as interp.Params("-e")(runner).
// Note that options cannot be applied once Run or Reset have been called.
type RunnerOption func(*Runner) error

// TODO: enforce the rule above via didReset.

// Env sets the interpreter's environment. If nil, a copy of the current
// process's environment is used.
func Env(env expand.Environ) RunnerOption {
	return func(r *Runner) error {
		if env == nil {
			env = expand.ListEnviron(os.Environ()...)
		}
		r.Env = env
		return nil
	}
}

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

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cmd/
│   ├── gosh/
│   │   ├── main.go
│   │   └── main_test.go
│   └── shfmt/
│       ├── Dockerfile
│       ├── docker-entrypoint.sh
│       ├── main.go
│       ├── main_test.go
│       ├── shfmt.1.scd
│       └── testdata/
│           └── script/
│               ├── atomic.txtar
│               ├── basic.txtar
│               ├── diff.txtar
│               ├── editorconfig.txtar
│               ├── flags.txtar
│               ├── simplify.txtar
│               ├── tojson.txtar
│               └── walk.txtar
├── expand/
│   ├── arith.go
│   ├── braces.go
│   ├── braces_test.go
│   ├── doc.go
│   ├── environ.go
│   ├── environ_test.go
│   ├── expand.go
│   ├── expand_nonwindows.go
│   ├── expand_test.go
│   ├── expand_windows.go
│   ├── param.go
│   └── valuekind_string.go
├── fileutil/
│   ├── file.go
│   └── file_test.go
├── go.mod
├── go.sum
├── internal/
│   ├── pattern.go
│   └── testing.go
├── interp/
│   ├── api.go
│   ├── builtin.go
│   ├── example_test.go
│   ├── handler.go
│   ├── handler_test.go
│   ├── interp_test.go
│   ├── os_notunix.go
│   ├── os_unix.go
│   ├── runner.go
│   ├── test.go
│   ├── test_classic.go
│   ├── trace.go
│   ├── unexported_test.go
│   ├── unix_test.go
│   ├── vars.go
│   └── windows_test.go
├── moreinterp/
│   ├── coreutils/
│   │   ├── coreutils.go
│   │   ├── coreutils_test.go
│   │   └── error.go
│   ├── go.mod
│   └── go.sum
├── pattern/
│   ├── example_test.go
│   ├── pattern.go
│   └── pattern_test.go
├── shell/
│   ├── doc.go
│   ├── example_test.go
│   ├── expand.go
│   └── expand_test.go
└── syntax/
    ├── bench_test.go
    ├── braces.go
    ├── canonical.sh
    ├── doc.go
    ├── example_test.go
    ├── filetests_test.go
    ├── fuzz_test.go
    ├── lexer.go
    ├── nodes.go
    ├── parser.go
    ├── parser_arithm.go
    ├── parser_linux_test.go
    ├── parser_other_test.go
    ├── parser_test.go
    ├── printer.go
    ├── printer_test.go
    ├── quote.go
    ├── quote_test.go
    ├── simplify.go
    ├── simplify_test.go
    ├── testdata/
    │   └── fuzz/
    │       ├── FuzzParsePrint/
    │       │   ├── 293db3718a4ab7a5
    │       │   ├── 6d0dc226922dc40c
    │       │   └── cb6d714b0a2d2315
    │       └── FuzzQuote/
    │           ├── 23cf0175e40438e8033b11cdd1441a2d2893a99144c4ac0f2b5f4caa113c9edd
    │           ├── 25f36feab4af00bc4dfc3cf56da02b842b62ba8c5ac44862b5b3b776a0d519b4
    │           ├── 2788bd30d386289e06a1024a030ad5ab7f363c703bea8a5d035de174491029bf
    │           ├── 39d5fdf93d52b2cd50fb9582b27c82d159de0575623865538ced2a7780499fa6
    │           ├── 6fcce067200fb8ae6d4c2b1b7c1f55d3f7e4b38f4ee4f05e50e496a7c399f2d8
    │           ├── b26cd471412059c6ab6aa27b6153d42d2d00cbb00ad11d3cd88a192a7dfd2cdf
    │           ├── df6b5d69da50c7d58ca13f6dde15e2a7224a53ce7bd72a02d49893e580b6775b
    │           └── ea14da9b0299f4463c20659e2a51808fef8d5fb0de6324f0de64153511d4b1f8
    ├── token_string.go
    ├── tokens.go
    ├── typedjson/
    │   ├── json.go
    │   ├── json_test.go
    │   └── testdata/
    │       └── roundtrip/
    │           ├── file.json
    │           └── file.sh
    ├── walk.go
    └── walk_test.go
Download .txt
SYMBOL INDEX (1385 symbols across 68 files)

FILE: cmd/gosh/main.go
  function main (line 24) | func main() {
  function runAll (line 37) | func runAll() error {
  function run (line 60) | func run(r *interp.Runner, reader io.Reader, name string) error {
  function runPath (line 70) | func runPath(r *interp.Runner, path string) error {
  function runInteractive (line 79) | func runInteractive(r *interp.Runner, stdin io.Reader, stdout, stderr io...

FILE: cmd/gosh/main_test.go
  function TestInteractive (line 194) | func TestInteractive(t *testing.T) {
  function TestInteractiveExit (line 248) | func TestInteractiveExit(t *testing.T) {
  function readString (line 265) | func readString(r io.Reader, want string) error {

FILE: cmd/shfmt/main.go
  type boolStringValue (line 30) | type boolStringValue
    method Set (line 32) | func (b *boolStringValue) Set(val string) error {
    method String (line 36) | func (b *boolStringValue) String() string {
    method IsBoolFlag (line 39) | func (*boolStringValue) IsBoolFlag() bool { return true }
  function boolStringVar (line 41) | func boolStringVar(p *string, name string, value string, usage string) {
  function langVariantVar (line 46) | func langVariantVar(p *syntax.LangVariant, name string, value syntax.Lan...
  type multiFlag (line 51) | type multiFlag struct
  function flagVal (line 56) | func flagVal[T any](short, long string, val T, register func(*T, string,...
  function main (line 108) | func main() {
  function formatStdin (line 293) | func formatStdin(name string) error {
  function walkPath (line 327) | func walkPath(path string, entry fs.DirEntry) error {
  function propsOptions (line 367) | func propsOptions(lang syntax.LangVariant, props editorconfig.Section) (...
  function formatPath (line 398) | func formatPath(path string, checkShebang bool) error {
  function editorConfigLangs (line 452) | func editorConfigLangs(l syntax.LangVariant) []string {
  function formatBytes (line 469) | func formatBytes(src []byte, path string, fileLang syntax.LangVariant) e...
  constant terminalGreen (line 576) | terminalGreen = "\u001b[32m"
  constant terminalRed (line 577) | terminalRed   = "\u001b[31m"
  constant terminalCyan (line 578) | terminalCyan  = "\u001b[36m"
  constant terminalReset (line 579) | terminalReset = "\u001b[0m"
  constant terminalBold (line 580) | terminalBold  = "\u001b[1m"

FILE: cmd/shfmt/main_test.go
  function TestMain (line 14) | func TestMain(m *testing.M) {
  function TestScript (line 22) | func TestScript(t *testing.T) {

FILE: expand/arith.go
  function Arithm (line 17) | func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) {
  function oneIf (line 107) | func oneIf(b bool) int {
  function atoi (line 115) | func atoi(s string) int64 {
  method assgnArit (line 121) | func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) {
  function intPow (line 165) | func intPow(a, b int) int {
  function binArit (line 177) | func binArit(op syntax.BinAritOperator, x, y int) (int, error) {

FILE: expand/braces.go
  function Braces (line 18) | func Braces(word *syntax.Word) []*syntax.Word {
  function extraLeadingZeros (line 93) | func extraLeadingZeros(s string) int {

FILE: expand/braces_test.go
  function lit (line 13) | func lit(s string) *syntax.Lit                { return &syntax.Lit{Value...
  function word (line 14) | func word(ps ...syntax.WordPart) *syntax.Word { return &syntax.Word{Part...
  function litWord (line 15) | func litWord(s string) *syntax.Word           { return word(lit(s)) }
  function litWords (line 16) | func litWords(strs ...string) []*syntax.Word {
  function TestBraces (line 166) | func TestBraces(t *testing.T) {
  function wantBraceExpParts (line 188) | func wantBraceExpParts(t *testing.T, word *syntax.Word, want bool) {
  function printWords (line 203) | func printWords(words ...*syntax.Word) string {

FILE: expand/environ.go
  type Environ (line 15) | type Environ interface
  type WriteEnviron (line 42) | type WriteEnviron interface
  type ValueKind (line 62) | type ValueKind
  constant Unknown (line 66) | Unknown ValueKind = iota
  constant String (line 68) | String
  constant NameRef (line 70) | NameRef
  constant Indexed (line 72) | Indexed
  constant Associative (line 74) | Associative
  constant KeepValue (line 78) | KeepValue
  constant Unset (line 84) | Unset = Unknown
  type Variable (line 89) | type Variable struct
    method IsSet (line 108) | func (v Variable) IsSet() bool {
    method Declared (line 115) | func (v Variable) Declared() bool {
    method Flags (line 121) | func (v Variable) Flags() string {
    method String (line 142) | func (v Variable) String() string {
    method Resolve (line 163) | func (v Variable) Resolve(env Environ) (string, Variable) {
  constant maxNameRefDepth (line 159) | maxNameRefDepth = 100
  function FuncEnviron (line 180) | func FuncEnviron(fn func(string) string) Environ {
  type funcEnviron (line 184) | type funcEnviron
    method Get (line 186) | func (f funcEnviron) Get(name string) Variable {
    method Each (line 194) | func (f funcEnviron) Each(func(name string, vr Variable) bool) {}
  function ListEnviron (line 202) | func ListEnviron(pairs ...string) Environ {
  function listEnviron_ (line 208) | func listEnviron_(caseInsensitive bool, pairs ...string) Environ {
  type listEnviron (line 248) | type listEnviron struct
    method compare (line 253) | func (l listEnviron) compare(a, b string) int {
    method Get (line 263) | func (l listEnviron) Get(name string) Variable {
    method Each (line 285) | func (l listEnviron) Each(fn func(name string, vr Variable) bool) {

FILE: expand/environ_test.go
  function TestListEnviron (line 11) | func TestListEnviron(t *testing.T) {
  function TestGetWithSameSubPrefix (line 72) | func TestGetWithSameSubPrefix(t *testing.T) {

FILE: expand/expand.go
  type Config (line 30) | type Config struct
    method ifsRune (line 140) | func (cfg *Config) ifsRune(r rune) bool {
    method ifsJoin (line 149) | func (cfg *Config) ifsJoin(strs []string) string {
    method strBuilder (line 157) | func (cfg *Config) strBuilder() *strings.Builder {
    method envGet (line 163) | func (cfg *Config) envGet(name string) string {
    method envSet (line 167) | func (cfg *Config) envSet(name, value string) error {
    method fieldJoin (line 412) | func (cfg *Config) fieldJoin(parts []fieldPart) string {
    method escapedGlobField (line 426) | func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string...
    method wordField (line 520) | func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]...
    method cmdSubst (line 607) | func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) {
    method wordFields (line 620) | func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, e...
    method quotedElemFields (line 759) | func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string {
    method sliceElems (line 822) | func (cfg *Config) sliceElems(pe *syntax.ParamExp, elems []string, pos...
    method expandUser (line 857) | func (cfg *Config) expandUser(field string, moreFields bool) (prefix, ...
    method glob (line 939) | func (cfg *Config) glob(base, pat string) ([]string, error) {
    method globDir (line 1070) | func (cfg *Config) globDir(base, dir string, matcher func(string) bool...
  type UnexpectedCommandError (line 102) | type UnexpectedCommandError struct
    method Error (line 106) | func (u UnexpectedCommandError) Error() string {
  function prepareConfig (line 115) | func prepareConfig(cfg *Config) *Config {
  function Literal (line 181) | func Literal(cfg *Config, word *syntax.Word) (string, error) {
  function Document (line 199) | func Document(cfg *Config, word *syntax.Word) (string, error) {
  function Pattern (line 217) | func Pattern(cfg *Config, word *syntax.Word) (string, error) {
  function Format (line 247) | func Format(cfg *Config, format string, args []string) (string, int, err...
  function formatInto (line 259) | func formatInto(sb *strings.Builder, format string, args []string) (int,...
  function Fields (line 445) | func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
  function FieldsSeq (line 459) | func FieldsSeq(cfg *Config, words ...*syntax.Word) iter.Seq2[string, err...
  type fieldPart (line 506) | type fieldPart struct
  type quoteLevel (line 511) | type quoteLevel
  constant quoteNone (line 514) | quoteNone quoteLevel = iota
  constant quoteDouble (line 515) | quoteDouble
  constant quoteHeredoc (line 516) | quoteHeredoc
  constant quoteSingle (line 517) | quoteSingle
  function findAllIndex (line 905) | func findAllIndex(pat, name string, n int) [][]int {
  function pathJoin2 (line 921) | func pathJoin2(elem1, elem2 string) string {
  function pathSplit (line 934) | func pathSplit(path string) []string {
  function ReadFields (line 1109) | func ReadFields(cfg *Config, s string, n int, raw bool) []string {

FILE: expand/expand_nonwindows.go
  function isWindowsErrPathNotFound (line 8) | func isWindowsErrPathNotFound(error) bool { return false }

FILE: expand/expand_test.go
  function parseWord (line 16) | func parseWord(t *testing.T, src string) *syntax.Word {
  function TestConfigNils (line 26) | func TestConfigNils(t *testing.T) {
  function TestFieldsIdempotency (line 67) | func TestFieldsIdempotency(t *testing.T) {
  function Test_glob (line 95) | func Test_glob(t *testing.T) {
  type mockFileInfo (line 136) | type mockFileInfo struct
    method Name (line 144) | func (fi *mockFileInfo) Name() string      { return fi.name }
    method Type (line 145) | func (fi *mockFileInfo) Type() fs.FileMode { return fi.typ }

FILE: expand/expand_windows.go
  function isWindowsErrPathNotFound (line 12) | func isWindowsErrPathNotFound(err error) bool {

FILE: expand/param.go
  function nodeLit (line 20) | func nodeLit(node syntax.Node) string {
  type UnsetParameterError (line 29) | type UnsetParameterError struct
    method Error (line 34) | func (u UnsetParameterError) Error() string {
  function overridingUnset (line 38) | func overridingUnset(pe *syntax.ParamExp) bool {
  method paramExp (line 52) | func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) {
  function removePattern (line 346) | func removePattern(str, pat string, fromEnd, shortest bool) string {
  method varInd (line 375) | func (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, e...
  method namesByPrefix (line 421) | func (cfg *Config) namesByPrefix(prefix string) []string {

FILE: expand/valuekind_string.go
  function _ (line 7) | func _() {
  constant _ValueKind_name (line 19) | _ValueKind_name = "UnknownStringNameRefIndexedAssociativeKeepValue"
  method String (line 23) | func (i ValueKind) String() string {

FILE: fileutil/file.go
  function HasShebang (line 23) | func HasShebang(bs []byte) bool {
  function Shebang (line 32) | func Shebang(bs []byte) string {
  type ScriptConfidence (line 43) | type ScriptConfidence
  constant ConfNotScript (line 48) | ConfNotScript ScriptConfidence = iota
  constant ConfIfShebang (line 53) | ConfIfShebang
  constant ConfIsScript (line 57) | ConfIsScript
  function CouldBeScript (line 63) | func CouldBeScript(info fs.FileInfo) ScriptConfidence {
  function CouldBeScript2 (line 70) | func CouldBeScript2(entry fs.DirEntry) ScriptConfidence {

FILE: fileutil/file_test.go
  function TestShebang (line 11) | func TestShebang(t *testing.T) {

FILE: internal/pattern.go
  function ExtendedPatternMatcher (line 18) | func ExtendedPatternMatcher(pat string, mode pattern.Mode) (func(string)...
  function extNegatedMatcher (line 42) | func extNegatedMatcher(pat string, groups []pattern.NegExtGlobGroup) (fu...

FILE: internal/testing.go
  function TestMainSetup (line 16) | func TestMainSetup() {

FILE: interp/api.go
  type Runner (line 40) | type Runner struct
    method posixOptByName (line 562) | func (r *Runner) posixOptByName(name string) *bool {
    method posixOptByFlag (line 571) | func (r *Runner) posixOptByFlag(flag byte) *bool {
    method bashOptByName (line 580) | func (r *Runner) bashOptByName(name string) (status *bool, supported b...
    method Reset (line 761) | func (r *Runner) Reset() {
    method Run (line 911) | func (r *Runner) Run(ctx context.Context, node syntax.Node) error {
    method Exited (line 952) | func (r *Runner) Exited() bool {
    method Subshell (line 966) | func (r *Runner) Subshell() *Runner {
    method subshell (line 973) | func (r *Runner) subshell(background bool) *Runner {
  type exitStatus (line 178) | type exitStatus struct
    method clear (line 197) | func (e *exitStatus) clear() {
    method ok (line 205) | func (e *exitStatus) ok() bool { return e.code == 0 }
    method oneIf (line 210) | func (e *exitStatus) oneIf(b bool) {
    method fatal (line 216) | func (e *exitStatus) fatal(err error) {
    method fromHandlerError (line 228) | func (e *exitStatus) fromHandlerError(err error) {
  type bgProc (line 244) | type bgProc struct
  type alias (line 252) | type alias struct
  function New (line 263) | func New(opts ...RunnerOption) (*Runner, error) {
  type RunnerOption (line 301) | type RunnerOption
  function Env (line 307) | func Env(env expand.Environ) RunnerOption {
  function Dir (line 319) | func Dir(path string) RunnerOption {
  function Interactive (line 348) | func Interactive(enabled bool) RunnerOption {
  function Params (line 360) | func Params(args ...string) RunnerOption {
  function CallHandler (line 419) | func CallHandler(f CallHandlerFunc) RunnerOption {
  function ExecHandler (line 431) | func ExecHandler(f ExecHandlerFunc) RunnerOption {
  function ExecHandlers (line 452) | func ExecHandlers(middlewares ...func(next ExecHandlerFunc) ExecHandlerF...
  function OpenHandler (line 468) | func OpenHandler(f OpenHandlerFunc) RunnerOption {
  function ReadDirHandler (line 478) | func ReadDirHandler(f ReadDirHandlerFunc) RunnerOption {
  function ReadDirHandler2 (line 496) | func ReadDirHandler2(f ReadDirHandlerFunc2) RunnerOption {
  function StatHandler (line 504) | func StatHandler(f StatHandlerFunc) RunnerOption {
  function stdinFile (line 511) | func stdinFile(r io.Reader) (*os.File, error) {
  function StdIO (line 543) | func StdIO(in io.Reader, out, err io.Writer) RunnerOption {
  type runnerOpts (line 591) | type runnerOpts
  type posixOpt (line 593) | type posixOpt struct
  type bashOpt (line 598) | type bashOpt struct
  constant optAllExport (line 736) | optAllExport = iota
  constant optErrExit (line 737) | optErrExit
  constant optNoExec (line 738) | optNoExec
  constant optNoGlob (line 739) | optNoGlob
  constant optNoUnset (line 740) | optNoUnset
  constant optXTrace (line 741) | optXTrace
  constant optPipeFail (line 742) | optPipeFail
  constant optDotGlob (line 746) | optDotGlob
  constant optExpandAliases (line 747) | optExpandAliases
  constant optExtGlob (line 748) | optExtGlob
  constant optGlobStar (line 749) | optGlobStar
  constant optNoCaseGlob (line 750) | optNoCaseGlob
  constant optNullGlob (line 751) | optNullGlob
  type ExitStatus (line 875) | type ExitStatus
    method Error (line 877) | func (s ExitStatus) Error() string { return fmt.Sprintf("exit status %...
  function NewExitStatus (line 884) | func NewExitStatus(status uint8) error {
  function IsExitStatus (line 893) | func IsExitStatus(err error) (status uint8, ok bool) {

FILE: interp/builtin.go
  function IsBuiltin (line 39) | func IsBuiltin(name string) bool {
  function atoi (line 120) | func atoi(s string) int64 {
  type errBuiltinExitStatus (line 126) | type errBuiltinExitStatus
    method Error (line 128) | func (e errBuiltinExitStatus) Error() string {
  method Builtin (line 138) | func (hc HandlerContext) Builtin(ctx context.Context, args []string) err...
  method builtin (line 149) | func (r *Runner) builtin(ctx context.Context, pos syntax.Pos, name strin...
  function mapfileSplit (line 999) | func mapfileSplit(delim byte, dropDelim bool) bufio.SplitFunc {
  method printOptLine (line 1021) | func (r *Runner) printOptLine(name string, enabled, supported bool) {
  method readLine (line 1030) | func (r *Runner) readLine(ctx context.Context, raw bool) ([]byte, error) {
  method changeDir (line 1077) | func (r *Runner) changeDir(ctx context.Context, cmd, path string) uint8 {
  function absPath (line 1095) | func absPath(dir, path string) string {
  method absPath (line 1105) | func (r *Runner) absPath(path string) string {
  type flagParser (line 1115) | type flagParser struct
    method more (line 1120) | func (p *flagParser) more() bool {
    method flag (line 1144) | func (p *flagParser) flag() string {
    method value (line 1160) | func (p *flagParser) value() string {
    method args (line 1169) | func (p *flagParser) args() []string { return p.remaining }
  type getopts (line 1171) | type getopts struct
    method next (line 1176) | func (g *getopts) next(optstr string, args []string) (opt rune, optarg...
  method optStatusText (line 1214) | func (r *Runner) optStatusText(status bool) string {

FILE: interp/example_test.go
  function Example (line 18) | func Example() {
  function ExampleExecHandlers (line 40) | func ExampleExecHandlers() {
  type nopWriterCloser (line 75) | type nopWriterCloser struct
    method Write (line 79) | func (nopWriterCloser) Write([]byte) (int, error) { return 0, io.EOF }
    method Close (line 80) | func (nopWriterCloser) Close() error              { return nil }
  function ExampleOpenHandler (line 82) | func ExampleOpenHandler() {

FILE: interp/handler.go
  function HandlerCtx (line 26) | func HandlerCtx(ctx context.Context) HandlerContext {
  type handlerCtxKey (line 34) | type handlerCtxKey struct
  type handlerKind (line 36) | type handlerKind
  constant _ (line 39) | _                  handlerKind = iota
  constant handlerKindExec (line 40) | handlerKindExec
  constant handlerKindCall (line 41) | handlerKindCall
  constant handlerKindOpen (line 42) | handlerKindOpen
  constant handlerKindReadDir (line 43) | handlerKindReadDir
  type HandlerContext (line 48) | type HandlerContext struct
  type CallHandlerFunc (line 97) | type CallHandlerFunc
  type ExecHandlerFunc (line 113) | type ExecHandlerFunc
  function DefaultExecHandler (line 124) | func DefaultExecHandler(killTimeout time.Duration) ExecHandlerFunc {
  function checkStat (line 182) | func checkStat(dir, file string, checkExec bool) (string, error) {
  function winHasExt (line 200) | func winHasExt(file string) bool {
  function findExecutable (line 209) | func findExecutable(dir, file string, exts []string) (string, error) {
  function findFile (line 229) | func findFile(dir, file string, _ []string) (string, error) {
  function LookPath (line 234) | func LookPath(env expand.Environ, file string) (string, error) {
  function LookPathDir (line 243) | func LookPathDir(cwd string, env expand.Environ, file string) (string, e...
  function lookPathDir (line 250) | func lookPathDir(cwd string, env expand.Environ, file string, find findA...
  function scriptFromPathDir (line 285) | func scriptFromPathDir(cwd string, env expand.Environ, file string) (str...
  function pathExts (line 289) | func pathExts(env expand.Environ) []string {
  type OpenHandlerFunc (line 325) | type OpenHandlerFunc
  function DefaultOpenHandler (line 333) | func DefaultOpenHandler() OpenHandlerFunc {
  type ReadDirHandlerFunc (line 355) | type ReadDirHandlerFunc
  type ReadDirHandlerFunc2 (line 360) | type ReadDirHandlerFunc2
  function DefaultReadDirHandler (line 364) | func DefaultReadDirHandler() ReadDirHandlerFunc {
  function DefaultReadDirHandler2 (line 372) | func DefaultReadDirHandler2() ReadDirHandlerFunc2 {
  type StatHandlerFunc (line 380) | type StatHandlerFunc
  function DefaultStatHandler (line 384) | func DefaultStatHandler() StatHandlerFunc {

FILE: interp/handler_test.go
  function blocklistOneExec (line 27) | func blocklistOneExec(name string) func(interp.ExecHandlerFunc) interp.E...
  function blocklistAllExec (line 38) | func blocklistAllExec(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
  function blocklistNondevOpen (line 44) | func blocklistNondevOpen(ctx context.Context, path string, flags int, mo...
  function mockFileOpen (line 52) | func mockFileOpen(ctx context.Context, path string, flags int, mode os.F...
  function blocklistGlob (line 56) | func blocklistGlob(ctx context.Context, path string) ([]fs.FileInfo, err...
  function execPrint (line 60) | func execPrint(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
  function execExitStatus5 (line 68) | func execExitStatus5(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
  function execCustomError (line 74) | func execCustomError(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
  function execCustomExitStatus5 (line 80) | func execCustomExitStatus5(next interp.ExecHandlerFunc) interp.ExecHandl...
  function execDotRunnerBuiltin (line 86) | func execDotRunnerBuiltin(next interp.ExecHandlerFunc) interp.ExecHandle...
  function execPrintWouldExec (line 102) | func execPrintWouldExec(next interp.ExecHandlerFunc) interp.ExecHandlerF...
  function TestRunnerHandlers (line 418) | func TestRunnerHandlers(t *testing.T) {
  type readyBuffer (line 447) | type readyBuffer struct
    method Write (line 452) | func (b *readyBuffer) Write(p []byte) (n int, err error) {
  function TestKillTimeout (line 460) | func TestKillTimeout(t *testing.T) {
  function TestKillSignal (line 543) | func TestKillSignal(t *testing.T) {

FILE: interp/interp_test.go
  constant runnerRunTimeout (line 35) | runnerRunTimeout = 5 * time.Second
  function parse (line 46) | func parse(tb testing.TB, parser *syntax.Parser, src string) *syntax.File {
  function BenchmarkRun (line 57) | func BenchmarkRun(b *testing.B) {
  function TestMain (line 89) | func TestMain(m *testing.M) {
  function checkBash (line 179) | func checkBash() bool {
  type concBuffer (line 189) | type concBuffer struct
    method Write (line 194) | func (c *concBuffer) Write(p []byte) (int, error) {
    method WriteString (line 201) | func (c *concBuffer) WriteString(s string) (int, error) {
    method String (line 208) | func (c *concBuffer) String() string {
    method Reset (line 215) | func (c *concBuffer) Reset() {
  type runTest (line 221) | type runTest struct
  function init (line 4018) | func init() {
  function skipIfUnsupported (line 4036) | func skipIfUnsupported(tb testing.TB, src string) {
  function TestRunnerRun (line 4045) | func TestRunnerRun(t *testing.T) {
  function readLines (line 4094) | func readLines(hc interp.HandlerContext) ([][]byte, error) {
  function absPath (line 4106) | func absPath(dir, path string) string {
  function testExecHandler (line 4349) | func testExecHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
  function TestRunnerRunConfirm (line 4358) | func TestRunnerRunConfirm(t *testing.T) {
  function TestRunnerOpts (line 4407) | func TestRunnerOpts(t *testing.T) {
  function TestRunnerContext (line 4536) | func TestRunnerContext(t *testing.T) {
  function TestCancelBlockedStdinRead (line 4575) | func TestCancelBlockedStdinRead(t *testing.T) {
  function TestRunnerAltNodes (line 4619) | func TestRunnerAltNodes(t *testing.T) {
  function TestRunnerDir (line 4645) | func TestRunnerDir(t *testing.T) {
  function TestRunnerIncremental (line 4738) | func TestRunnerIncremental(t *testing.T) {
  function TestRunnerResetFields (line 4762) | func TestRunnerResetFields(t *testing.T) {
  function TestRunnerManyResets (line 4821) | func TestRunnerManyResets(t *testing.T) {
  function TestRunnerFilename (line 4829) | func TestRunnerFilename(t *testing.T) {
  function TestRunnerEnvNoModify (line 4846) | func TestRunnerEnvNoModify(t *testing.T) {
  function TestMalformedPathOnWindows (line 4870) | func TestMalformedPathOnWindows(t *testing.T) {
  function TestReadShouldNotPanicWithNilStdin (line 4901) | func TestReadShouldNotPanicWithNilStdin(t *testing.T) {
  function TestRunnerVars (line 4917) | func TestRunnerVars(t *testing.T) {
  function TestRunnerSubshell (line 4937) | func TestRunnerSubshell(t *testing.T) {
  function TestRunnerNonFileStdin (line 4978) | func TestRunnerNonFileStdin(t *testing.T) {

FILE: interp/os_notunix.go
  function mkfifo (line 15) | func mkfifo(path string, mode uint32) error {
  method access (line 22) | func (r *Runner) access(ctx context.Context, path string, mode uint32) e...
  method unTestOwnOrGrp (line 49) | func (r *Runner) unTestOwnOrGrp(ctx context.Context, op syntax.UnTestOpe...
  type waitStatus (line 54) | type waitStatus struct
    method Signaled (line 56) | func (waitStatus) Signaled() bool { return false }
    method Signal (line 57) | func (waitStatus) Signal() int    { return 0 }

FILE: interp/os_unix.go
  function mkfifo (line 18) | func mkfifo(path string, mode uint32) error {
  method access (line 24) | func (r *Runner) access(ctx context.Context, path string, mode uint32) e...
  method unTestOwnOrGrp (line 31) | func (r *Runner) unTestOwnOrGrp(ctx context.Context, op syntax.UnTestOpe...

FILE: interp/runner.go
  constant shellReplyPS3Var (line 34) | shellReplyPS3Var = "PS3"
  constant shellDefaultPS3 (line 36) | shellDefaultPS3 = "#? "
  constant shellReplyVar (line 39) | shellReplyVar = "REPLY"
  constant fifoNamePrefix (line 41) | fifoNamePrefix = "sh-interp-"
  method fillExpandConfig (line 44) | func (r *Runner) fillExpandConfig(ctx context.Context) {
  function catShortcutArg (line 159) | func catShortcutArg(stmt *syntax.Stmt) *syntax.Word {
  method updateExpandOpts (line 173) | func (r *Runner) updateExpandOpts() {
  method expandErr (line 189) | func (r *Runner) expandErr(err error) {
  method arithm (line 207) | func (r *Runner) arithm(expr syntax.ArithmExpr) int {
  method fields (line 213) | func (r *Runner) fields(words ...*syntax.Word) []string {
  method literal (line 219) | func (r *Runner) literal(word *syntax.Word) string {
  method document (line 225) | func (r *Runner) document(word *syntax.Word) string {
  method pattern (line 231) | func (r *Runner) pattern(word *syntax.Word) string {
  type expandEnv (line 238) | type expandEnv struct
    method Get (line 244) | func (e expandEnv) Get(name string) expand.Variable {
    method Set (line 248) | func (e expandEnv) Set(name string, vr expand.Variable) error {
    method Each (line 253) | func (e expandEnv) Each(fn func(name string, vr expand.Variable) bool) {
  method handlerCtx (line 259) | func (r *Runner) handlerCtx(ctx context.Context, kind handlerKind, pos s...
  method out (line 275) | func (r *Runner) out(s string) {
  method outf (line 279) | func (r *Runner) outf(format string, a ...any) {
  method errf (line 283) | func (r *Runner) errf(format string, a ...any) {
  method stop (line 287) | func (r *Runner) stop(ctx context.Context) bool {
  method stmt (line 302) | func (r *Runner) stmt(ctx context.Context, st *syntax.Stmt) {
  method stmtSync (line 329) | func (r *Runner) stmtSync(ctx context.Context, st *syntax.Stmt) {
  method cmd (line 367) | func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
  method trapCallback (line 812) | func (r *Runner) trapCallback(ctx context.Context, callback, name string) {
  method flattenAssigns (line 836) | func (r *Runner) flattenAssigns(args []*syntax.Assign) iter.Seq[*syntax....
  function match (line 867) | func match(pat, name string) bool {
  function elapsedString (line 873) | func elapsedString(d time.Duration, posix bool) string {
  method stmts (line 882) | func (r *Runner) stmts(ctx context.Context, stmts []*syntax.Stmt) {
  method hdocReader (line 888) | func (r *Runner) hdocReader(rd *syntax.Redirect) (*os.File, error) {
  method redir (line 939) | func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Clo...
  method loopStmtsBroken (line 1034) | func (r *Runner) loopStmtsBroken(ctx context.Context, stmts []*syntax.St...
  method call (line 1052) | func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) {
  method exec (line 1094) | func (r *Runner) exec(ctx context.Context, pos syntax.Pos, args []string) {
  method open (line 1098) | func (r *Runner) open(ctx context.Context, path string, flags int, mode ...
  method stat (line 1127) | func (r *Runner) stat(ctx context.Context, name string) (fs.FileInfo, er...
  method lstat (line 1132) | func (r *Runner) lstat(ctx context.Context, name string) (fs.FileInfo, e...

FILE: interp/test.go
  method bashTest (line 19) | func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, cla...
  method binTest (line 61) | func (r *Runner) binTest(ctx context.Context, op syntax.BinTestOperator,...
  method statMode (line 126) | func (r *Runner) statMode(ctx context.Context, name string, mode os.File...
  constant access_R_OK (line 133) | access_R_OK = 0x4
  constant access_W_OK (line 134) | access_W_OK = 0x2
  constant access_X_OK (line 135) | access_X_OK = 0x1
  method unTest (line 138) | func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x...

FILE: interp/test_classic.go
  constant illegalTok (line 12) | illegalTok = 0
  type testParser (line 14) | type testParser struct
    method errf (line 22) | func (p *testParser) errf(format string, a ...any) {
    method next (line 26) | func (p *testParser) next() {
    method followWord (line 36) | func (p *testParser) followWord(fval string) *syntax.Word {
    method classicTest (line 47) | func (p *testParser) classicTest(fval string, pastAndOr bool) syntax.T...
    method testExprBase (line 78) | func (p *testParser) testExprBase(fval string) syntax.TestExpr {
  function testUnaryOp (line 116) | func testUnaryOp(val string) syntax.UnTestOperator {
  function testBinaryOp (line 176) | func testBinaryOp(val string) syntax.BinTestOperator {

FILE: interp/trace.go
  type tracer (line 14) | type tracer struct
    method string (line 35) | func (t *tracer) string(s string) {
    method stringf (line 47) | func (t *tracer) stringf(f string, a ...any) {
    method expr (line 57) | func (t *tracer) expr(x syntax.Node) {
    method flush (line 72) | func (t *tracer) flush() {
    method newLineFlush (line 82) | func (t *tracer) newLineFlush() {
    method call (line 96) | func (t *tracer) call(cmd string, args ...string) {
  method tracer (line 21) | func (r *Runner) tracer() *tracer {

FILE: interp/unexported_test.go
  function TestElapsedString (line 11) | func TestElapsedString(t *testing.T) {

FILE: interp/unix_test.go
  function TestRunnerTerminalStdIO (line 21) | func TestRunnerTerminalStdIO(t *testing.T) {
  function TestRunnerTerminalExec (line 84) | func TestRunnerTerminalExec(t *testing.T) {
  function shortPathName (line 153) | func shortPathName(path string) (string, error) {

FILE: interp/vars.go
  function newOverlayEnviron (line 22) | func newOverlayEnviron(parent expand.Environ, background bool) *overlayE...
  type overlayEnviron (line 37) | type overlayEnviron struct
    method normalize (line 63) | func (o *overlayEnviron) normalize(name string) string {
    method Get (line 70) | func (o *overlayEnviron) Get(name string) expand.Variable {
    method Set (line 81) | func (o *overlayEnviron) Set(name string, vr expand.Variable) error {
    method Each (line 118) | func (o *overlayEnviron) Each(f func(name string, vr expand.Variable) ...
  type namedVariable (line 53) | type namedVariable struct
  function execEnv (line 129) | func execEnv(env expand.Environ) []string {
  method lookupVar (line 151) | func (r *Runner) lookupVar(name string) expand.Variable {
  method envGet (line 210) | func (r *Runner) envGet(name string) string {
  method delVar (line 214) | func (r *Runner) delVar(name string) {
  method setVarString (line 222) | func (r *Runner) setVarString(name, value string) {
  method setVar (line 226) | func (r *Runner) setVar(name string, vr expand.Variable) {
  method setVarWithIndex (line 237) | func (r *Runner) setVarWithIndex(prev expand.Variable, name string, inde...
  method setFunc (line 302) | func (r *Runner) setFunc(name string, body *syntax.Stmt) {
  function stringIndex (line 309) | func stringIndex(index syntax.ArithmExpr) bool {
  method assignVal (line 323) | func (r *Runner) assignVal(prev expand.Variable, as *syntax.Assign, valT...

FILE: interp/windows_test.go
  function shortPathName (line 14) | func shortPathName(path string) (string, error) {

FILE: moreinterp/coreutils/coreutils.go
  function ExecHandler (line 61) | func ExecHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {

FILE: moreinterp/coreutils/coreutils_test.go
  function TestExecHandler (line 17) | func TestExecHandler(t *testing.T) {

FILE: moreinterp/coreutils/error.go
  type Error (line 6) | type Error struct
    method Error (line 15) | func (err *Error) Error() string {
    method Unwrap (line 19) | func (err *Error) Unwrap() error {

FILE: pattern/example_test.go
  function ExampleRegexp (line 13) | func ExampleRegexp() {
  function ExampleQuoteMeta (line 33) | func ExampleQuoteMeta() {

FILE: pattern/pattern.go
  type Mode (line 21) | type Mode
  type SyntaxError (line 23) | type SyntaxError struct
    method Error (line 28) | func (e SyntaxError) Error() string { return e.msg }
    method Unwrap (line 30) | func (e SyntaxError) Unwrap() error { return e.err }
  type NegExtGlobGroup (line 34) | type NegExtGlobGroup struct
  type NegExtGlobError (line 42) | type NegExtGlobError struct
    method Error (line 46) | func (e *NegExtGlobError) Error() string {
  constant Shortest (line 55) | Shortest          Mode = 1 << iota
  constant Filenames (line 56) | Filenames
  constant EntireString (line 57) | EntireString
  constant NoGlobCase (line 58) | NoGlobCase
  constant NoGlobStar (line 59) | NoGlobStar
  constant GlobLeadingDot (line 60) | GlobLeadingDot
  constant ExtendedOperators (line 61) | ExtendedOperators
  function Regexp (line 73) | func Regexp(pat string, mode Mode) (string, error) {
  type stringLexer (line 132) | type stringLexer struct
    method next (line 137) | func (sl *stringLexer) next() byte {
    method last (line 146) | func (sl *stringLexer) last() byte {
    method peekNext (line 153) | func (sl *stringLexer) peekNext() byte {
    method peekRest (line 160) | func (sl *stringLexer) peekRest() string {
  function regexpNext (line 164) | func regexpNext(sb *strings.Builder, sl *stringLexer, mode Mode) error {
  function charClass (line 328) | func charClass(s string) (string, error) {
  function HasMeta (line 361) | func HasMeta(pat string, mode Mode) bool {
  function QuoteMeta (line 379) | func QuoteMeta(pat string, mode Mode) string {

FILE: pattern/pattern_test.go
  function TestRegexp (line 166) | func TestRegexp(t *testing.T) {
  function TestMeta (line 209) | func TestMeta(t *testing.T) {

FILE: shell/example_test.go
  function ExampleExpand (line 12) | func ExampleExpand() {
  function ExampleFields (line 34) | func ExampleFields() {

FILE: shell/expand.go
  function Expand (line 26) | func Expand(s string, env func(string) string) (string, error) {
  function Fields (line 52) | func Fields(s string, env func(string) string) ([]string, error) {

FILE: shell/expand_test.go
  function strEnviron (line 15) | func strEnviron(pairs ...string) func(string) string {
  function TestExpand (line 43) | func TestExpand(t *testing.T) {
  function TestUnexpectedCmdSubst (line 59) | func TestUnexpectedCmdSubst(t *testing.T) {
  function TestFields (line 110) | func TestFields(t *testing.T) {

FILE: syntax/bench_test.go
  function BenchmarkParse (line 12) | func BenchmarkParse(b *testing.B) {
  function BenchmarkPrint (line 39) | func BenchmarkPrint(b *testing.B) {

FILE: syntax/braces.go
  function SplitBraces (line 29) | func SplitBraces(word *Word) bool {

FILE: syntax/example_test.go
  function Example (line 14) | func Example() {
  function ExampleWord (line 28) | func ExampleWord() {
  function ExampleCommand (line 57) | func ExampleCommand() {
  function ExampleNewParser_options (line 77) | func ExampleNewParser_options() {
  function ExampleQuote (line 102) | func ExampleQuote() {
  function ExampleWalk (line 137) | func ExampleWalk() {
  function ExampleDebugPrint (line 154) | func ExampleDebugPrint() {

FILE: syntax/filetests_test.go
  function lit (line 12) | func lit(s string) *Lit { return &Lit{Value: s} }
  function lits (line 13) | func lits(strs ...string) []*Lit {
  function word (line 20) | func word(ps ...WordPart) *Word { return &Word{Parts: ps} }
  function litWord (line 21) | func litWord(s string) *Word    { return word(lit(s)) }
  function litWords (line 22) | func litWords(strs ...string) []*Word {
  function litAssigns (line 30) | func litAssigns(pairs ...string) []*Assign {
  function call (line 45) | func call(words ...*Word) *CallExpr    { return &CallExpr{Args: words} }
  function litCall (line 46) | func litCall(strs ...string) *CallExpr { return call(litWords(strs...).....
  function stmt (line 48) | func stmt(cmd Command) *Stmt { return &Stmt{Cmd: cmd} }
  function stmts (line 49) | func stmts(cmds ...Command) []*Stmt {
  function litStmt (line 57) | func litStmt(strs ...string) *Stmt { return stmt(litCall(strs...)) }
  function litStmts (line 58) | func litStmts(strs ...string) []*Stmt {
  function sglQuoted (line 66) | func sglQuoted(s string) *SglQuoted        { return &SglQuoted{Value: s} }
  function sglDQuoted (line 67) | func sglDQuoted(s string) *SglQuoted       { return &SglQuoted{Dollar: t...
  function dblQuoted (line 68) | func dblQuoted(ps ...WordPart) *DblQuoted  { return &DblQuoted{Parts: ps} }
  function dblDQuoted (line 69) | func dblDQuoted(ps ...WordPart) *DblQuoted { return &DblQuoted{Dollar: t...
  function block (line 70) | func block(sts ...*Stmt) *Block            { return &Block{Stmts: sts} }
  function subshell (line 71) | func subshell(sts ...*Stmt) *Subshell      { return &Subshell{Stmts: sts} }
  function arithmExp (line 72) | func arithmExp(e ArithmExpr) *ArithmExp    { return &ArithmExp{X: e} }
  function arithmExpBr (line 73) | func arithmExpBr(e ArithmExpr) *ArithmExp  { return &ArithmExp{Bracket: ...
  function arithmCmd (line 74) | func arithmCmd(e ArithmExpr) *ArithmCmd    { return &ArithmCmd{X: e} }
  function parenArit (line 75) | func parenArit(e ArithmExpr) *ParenArithm  { return &ParenArithm{X: e} }
  function parenTest (line 76) | func parenTest(e TestExpr) *ParenTest      { return &ParenTest{X: e} }
  function cmdSubst (line 78) | func cmdSubst(sts ...*Stmt) *CmdSubst { return &CmdSubst{Stmts: sts} }
  function litParamExp (line 79) | func litParamExp(s string) *ParamExp {
  function letClause (line 83) | func letClause(exps ...ArithmExpr) *LetClause {
  function arrValues (line 87) | func arrValues(words ...*Word) *ArrayExpr {
  function fullProg (line 95) | func fullProg(v any) *File {
  type fileTestCase (line 124) | type fileTestCase struct
    method setForLangs (line 141) | func (c *fileTestCase) setForLangs(val any, langSets ...LangVariant) {
  function flipConfirm2 (line 137) | func flipConfirm2(langSet LangVariant) func(*fileTestCase) {
  function fileTest (line 158) | func fileTest(in []string, opts ...func(*fileTestCase)) fileTestCase {
  function langSkip (line 166) | func langSkip(langSets ...LangVariant) func(*fileTestCase) {
  function langFile (line 170) | func langFile(wantNode any, langSets ...LangVariant) func(*fileTestCase) {
  function langErr2 (line 176) | func langErr2(wantErr string, langSets ...LangVariant) func(*fileTestCas...
  type sanityChecker (line 5255) | type sanityChecker struct
    method checkPos (line 5261) | func (c sanityChecker) checkPos(node Node, pos Pos, strs ...string) {
    method visit (line 5299) | func (c sanityChecker) visit(node Node) bool {

FILE: syntax/fuzz_test.go
  function FuzzQuote (line 15) | func FuzzQuote(f *testing.F) {
  function FuzzParsePrint (line 79) | func FuzzParsePrint(f *testing.F) {

FILE: syntax/lexer.go
  function asciiLetter (line 12) | func asciiLetter[T rune | byte](r T) bool {
  function asciiDigit (line 16) | func asciiDigit[T rune | byte](r T) bool {
  function regOps (line 21) | func regOps(r rune) bool {
  function paramOps (line 30) | func paramOps(r rune) bool {
  function arithmOps (line 40) | func arithmOps(r rune) bool {
  function bquoteEscaped (line 49) | func bquoteEscaped(b byte) bool {
  constant escNewl (line 57) | escNewl rune = utf8.RuneSelf + 1
  method rune (line 59) | func (p *Parser) rune() rune {
  method fill (line 148) | func (p *Parser) fill() (n int) {
  method nextKeepSpaces (line 186) | func (p *Parser) nextKeepSpaces() {
  method next (line 234) | func (p *Parser) next() {
  method extendedGlob (line 404) | func (p *Parser) extendedGlob() bool {
  method peek (line 429) | func (p *Parser) peek() byte {
  method peekTwo (line 439) | func (p *Parser) peekTwo() (byte, byte) {
  method regToken (line 454) | func (p *Parser) regToken(r rune) token {
  method dqToken (line 669) | func (p *Parser) dqToken(r rune) token {
  method paramToken (line 701) | func (p *Parser) paramToken(r rune) token {
  method arithmToken (line 795) | func (p *Parser) arithmToken(r rune) token {
  method newLit (line 953) | func (p *Parser) newLit(r rune) {
  method endLit (line 968) | func (p *Parser) endLit() (s string) {
  method isLitRedir (line 978) | func (p *Parser) isLitRedir() bool {
  function positionalRuneParam (line 986) | func positionalRuneParam[T rune | byte](r T) bool {
  function singleRuneParam (line 994) | func singleRuneParam[T rune | byte](r T) bool {
  function paramNameRune (line 1002) | func paramNameRune[T rune | byte](r T) bool {
  method advanceLitOther (line 1006) | func (p *Parser) advanceLitOther(r rune) {
  method zshNumRange (line 1048) | func (p *Parser) zshNumRange() bool {
  method advanceLitNone (line 1069) | func (p *Parser) advanceLitNone(r rune) {
  method advanceLitDquote (line 1124) | func (p *Parser) advanceLitDquote(r rune) {
  method advanceLitHdoc (line 1141) | func (p *Parser) advanceLitHdoc(r rune) {
  method quotedHdocWord (line 1209) | func (p *Parser) quotedHdocWord() *Word {
  method advanceLitRe (line 1256) | func (p *Parser) advanceLitRe(r rune) {
  function testUnaryOp (line 1286) | func testUnaryOp(val string) UnTestOperator {
  function testBinaryOp (line 1343) | func testBinaryOp(val string) BinTestOperator {

FILE: syntax/nodes.go
  type Node (line 13) | type Node interface
  type File (line 24) | type File struct
    method Pos (line 31) | func (f *File) Pos() Pos { return stmtsPos(f.Stmts, f.Last) }
    method End (line 32) | func (f *File) End() Pos { return stmtsEnd(f.Stmts, f.Last) }
  function stmtsPos (line 34) | func stmtsPos(stmts []*Stmt, last []Comment) Pos {
  function stmtsEnd (line 51) | func stmtsEnd(stmts []*Stmt, last []Comment) Pos {
  type Pos (line 69) | type Pos struct
    method Offset (line 122) | func (p Pos) Offset() uint {
    method Line (line 134) | func (p Pos) Line() uint { return uint(p.lineCol >> colBitSize) }
    method Col (line 141) | func (p Pos) Col() uint { return uint(p.lineCol & colBitMask) }
    method String (line 143) | func (p Pos) String() string {
    method IsValid (line 165) | func (p Pos) IsValid() bool {
    method IsRecovered (line 173) | func (p Pos) IsRecovered() bool { return p == recoveredPos }
    method After (line 178) | func (p Pos) After(p2 Pos) bool {
  constant offsetRecovered (line 78) | offsetRecovered = math.MaxUint32 - 10
  constant offsetMax (line 79) | offsetMax       = math.MaxUint32 - 11
  constant lineBitSize (line 84) | lineBitSize = 18
  constant lineMax (line 85) | lineMax     = (1 << lineBitSize) - 1
  constant colBitSize (line 87) | colBitSize = 32 - lineBitSize
  constant colMax (line 88) | colMax     = (1 << colBitSize) - 1
  constant colBitMask (line 89) | colBitMask = colMax
  function NewPos (line 101) | func NewPos(offset, line, column uint) Pos {
  function posAddCol (line 185) | func posAddCol(p Pos, n int) Pos {
  function posMax (line 195) | func posMax(p1, p2 Pos) Pos {
  type Comment (line 203) | type Comment struct
    method Pos (line 208) | func (c *Comment) Pos() Pos { return c.Hash }
    method End (line 209) | func (c *Comment) End() Pos { return posAddCol(c.Hash, 1+len(c.Text)) }
  type Stmt (line 214) | type Stmt struct
    method Pos (line 227) | func (s *Stmt) Pos() Pos { return s.Position }
    method End (line 228) | func (s *Stmt) End() Pos {
  type Command (line 255) | type Command interface
  type Assign (line 288) | type Assign struct
    method Pos (line 297) | func (a *Assign) Pos() Pos {
    method End (line 304) | func (a *Assign) End() Pos {
  type Redirect (line 321) | type Redirect struct
    method Pos (line 329) | func (r *Redirect) Pos() Pos {
    method End (line 336) | func (r *Redirect) End() Pos {
  type CallExpr (line 348) | type CallExpr struct
    method commandNode (line 260) | func (*CallExpr) commandNode()     {}
    method Pos (line 353) | func (c *CallExpr) Pos() Pos {
    method End (line 360) | func (c *CallExpr) End() Pos {
  type Subshell (line 369) | type Subshell struct
    method commandNode (line 266) | func (*Subshell) commandNode()     {}
    method Pos (line 376) | func (s *Subshell) Pos() Pos { return s.Lparen }
    method End (line 377) | func (s *Subshell) End() Pos { return posAddCol(s.Rparen, 1) }
  type Block (line 381) | type Block struct
    method commandNode (line 265) | func (*Block) commandNode()        {}
    method Pos (line 388) | func (b *Block) Pos() Pos { return b.Lbrace }
    method End (line 389) | func (b *Block) End() Pos { return posAddCol(b.Rbrace, 1) }
  type IfClause (line 392) | type IfClause struct
    method commandNode (line 261) | func (*IfClause) commandNode()     {}
    method Pos (line 407) | func (c *IfClause) Pos() Pos { return c.Position }
    method End (line 408) | func (c *IfClause) End() Pos { return posAddCol(c.FiPos, 2) }
  type WhileClause (line 411) | type WhileClause struct
    method commandNode (line 262) | func (*WhileClause) commandNode()  {}
    method Pos (line 421) | func (w *WhileClause) Pos() Pos { return w.WhilePos }
    method End (line 422) | func (w *WhileClause) End() Pos { return posAddCol(w.DonePos, 4) }
  type ForClause (line 426) | type ForClause struct
    method commandNode (line 263) | func (*ForClause) commandNode()    {}
    method Pos (line 436) | func (f *ForClause) Pos() Pos { return f.ForPos }
    method End (line 437) | func (f *ForClause) End() Pos { return posAddCol(f.DonePos, 4) }
  type Loop (line 440) | type Loop interface
  type WordIter (line 451) | type WordIter struct
    method loopNode (line 445) | func (*WordIter) loopNode()   {}
    method Pos (line 457) | func (w *WordIter) Pos() Pos { return w.Name.Pos() }
    method End (line 458) | func (w *WordIter) End() Pos {
  type CStyleLoop (line 469) | type CStyleLoop struct
    method loopNode (line 446) | func (*CStyleLoop) loopNode() {}
    method Pos (line 475) | func (c *CStyleLoop) Pos() Pos { return c.Lparen }
    method End (line 476) | func (c *CStyleLoop) End() Pos { return posAddCol(c.Rparen, 2) }
  type BinaryCmd (line 479) | type BinaryCmd struct
    method commandNode (line 267) | func (*BinaryCmd) commandNode()    {}
    method Pos (line 485) | func (b *BinaryCmd) Pos() Pos { return b.X.Pos() }
    method End (line 486) | func (b *BinaryCmd) End() Pos { return b.Y.End() }
  type FuncDecl (line 489) | type FuncDecl struct
    method commandNode (line 268) | func (*FuncDecl) commandNode()     {}
    method Pos (line 503) | func (f *FuncDecl) Pos() Pos { return f.Position }
    method End (line 504) | func (f *FuncDecl) End() Pos { return f.Body.End() }
  type Word (line 509) | type Word struct
    method Pos (line 513) | func (w *Word) Pos() Pos { return w.Parts[0].Pos() }
    method End (line 514) | func (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() }
    method Lit (line 522) | func (w *Word) Lit() string {
    method arithmExprNode (line 743) | func (*Word) arithmExprNode()         {}
    method testExprNode (line 870) | func (*Word) testExprNode()       {}
  type WordPart (line 542) | type WordPart interface
  type Lit (line 562) | type Lit struct
    method wordPartNode (line 547) | func (*Lit) wordPartNode()       {}
    method Pos (line 567) | func (l *Lit) Pos() Pos { return l.ValuePos }
    method End (line 568) | func (l *Lit) End() Pos { return l.ValueEnd }
  type SglQuoted (line 571) | type SglQuoted struct
    method wordPartNode (line 548) | func (*SglQuoted) wordPartNode() {}
    method Pos (line 577) | func (q *SglQuoted) Pos() Pos { return q.Left }
    method End (line 578) | func (q *SglQuoted) End() Pos { return posAddCol(q.Right, 1) }
  type DblQuoted (line 581) | type DblQuoted struct
    method wordPartNode (line 549) | func (*DblQuoted) wordPartNode() {}
    method Pos (line 587) | func (q *DblQuoted) Pos() Pos { return q.Left }
    method End (line 588) | func (q *DblQuoted) End() Pos { return posAddCol(q.Right, 1) }
  type CmdSubst (line 591) | type CmdSubst struct
    method wordPartNode (line 551) | func (*CmdSubst) wordPartNode()  {}
    method Pos (line 602) | func (c *CmdSubst) Pos() Pos { return c.Left }
    method End (line 603) | func (c *CmdSubst) End() Pos { return posAddCol(c.Right, 1) }
  type ParamExp (line 606) | type ParamExp struct
    method wordPartNode (line 550) | func (*ParamExp) wordPartNode()  {}
    method simple (line 649) | func (p *ParamExp) simple() bool {
    method Pos (line 657) | func (p *ParamExp) Pos() Pos {
    method End (line 663) | func (p *ParamExp) End() Pos {
    method nakedIndex (line 674) | func (p *ParamExp) nakedIndex() bool {
  type Slice (line 684) | type Slice struct
  type Replace (line 689) | type Replace struct
  type Expansion (line 696) | type Expansion struct
  type ArithmExp (line 702) | type ArithmExp struct
    method wordPartNode (line 552) | func (*ArithmExp) wordPartNode() {}
    method Pos (line 710) | func (a *ArithmExp) Pos() Pos { return a.Left }
    method End (line 711) | func (a *ArithmExp) End() Pos {
  type ArithmCmd (line 721) | type ArithmCmd struct
    method commandNode (line 269) | func (*ArithmCmd) commandNode()    {}
    method Pos (line 728) | func (a *ArithmCmd) Pos() Pos { return a.Left }
    method End (line 729) | func (a *ArithmCmd) End() Pos { return posAddCol(a.Right, 2) }
  type ArithmExpr (line 734) | type ArithmExpr interface
  type BinaryArithm (line 753) | type BinaryArithm struct
    method arithmExprNode (line 739) | func (*BinaryArithm) arithmExprNode() {}
    method Pos (line 759) | func (b *BinaryArithm) Pos() Pos { return b.X.Pos() }
    method End (line 760) | func (b *BinaryArithm) End() Pos { return b.Y.End() }
  type UnaryArithm (line 767) | type UnaryArithm struct
    method arithmExprNode (line 740) | func (*UnaryArithm) arithmExprNode()  {}
    method Pos (line 774) | func (u *UnaryArithm) Pos() Pos {
    method End (line 781) | func (u *UnaryArithm) End() Pos {
  type ParenArithm (line 789) | type ParenArithm struct
    method arithmExprNode (line 741) | func (*ParenArithm) arithmExprNode()  {}
    method Pos (line 795) | func (p *ParenArithm) Pos() Pos { return p.Lparen }
    method End (line 796) | func (p *ParenArithm) End() Pos { return posAddCol(p.Rparen, 1) }
  type FlagsArithm (line 802) | type FlagsArithm struct
    method arithmExprNode (line 742) | func (*FlagsArithm) arithmExprNode()  {}
    method Pos (line 807) | func (z *FlagsArithm) Pos() Pos { return posAddCol(z.Flags.Pos(), -1) }
    method End (line 808) | func (z *FlagsArithm) End() Pos {
  type CaseClause (line 816) | type CaseClause struct
    method commandNode (line 264) | func (*CaseClause) commandNode()   {}
    method Pos (line 825) | func (c *CaseClause) Pos() Pos { return c.Case }
    method End (line 826) | func (c *CaseClause) End() Pos { return posAddCol(c.Esac, 4) }
  type CaseItem (line 829) | type CaseItem struct
    method Pos (line 839) | func (c *CaseItem) Pos() Pos { return c.Patterns[0].Pos() }
    method End (line 840) | func (c *CaseItem) End() Pos {
  type TestClause (line 850) | type TestClause struct
    method commandNode (line 270) | func (*TestClause) commandNode()   {}
    method Pos (line 856) | func (t *TestClause) Pos() Pos { return t.Left }
    method End (line 857) | func (t *TestClause) End() Pos { return posAddCol(t.Right, 2) }
  type TestExpr (line 862) | type TestExpr interface
  type BinaryTest (line 873) | type BinaryTest struct
    method testExprNode (line 867) | func (*BinaryTest) testExprNode() {}
    method Pos (line 879) | func (b *BinaryTest) Pos() Pos { return b.X.Pos() }
    method End (line 880) | func (b *BinaryTest) End() Pos { return b.Y.End() }
  type UnaryTest (line 884) | type UnaryTest struct
    method testExprNode (line 868) | func (*UnaryTest) testExprNode()  {}
    method Pos (line 890) | func (u *UnaryTest) Pos() Pos { return u.OpPos }
    method End (line 891) | func (u *UnaryTest) End() Pos { return u.X.End() }
  type ParenTest (line 894) | type ParenTest struct
    method testExprNode (line 869) | func (*ParenTest) testExprNode()  {}
    method Pos (line 900) | func (p *ParenTest) Pos() Pos { return p.Lparen }
    method End (line 901) | func (p *ParenTest) End() Pos { return posAddCol(p.Rparen, 1) }
  type DeclClause (line 909) | type DeclClause struct
    method commandNode (line 271) | func (*DeclClause) commandNode()   {}
    method Pos (line 916) | func (d *DeclClause) Pos() Pos { return d.Variant.Pos() }
    method End (line 917) | func (d *DeclClause) End() Pos {
  type ArrayExpr (line 927) | type ArrayExpr struct
    method Pos (line 934) | func (a *ArrayExpr) Pos() Pos { return a.Lparen }
    method End (line 935) | func (a *ArrayExpr) End() Pos { return posAddCol(a.Rparen, 1) }
  type ArrayElem (line 942) | type ArrayElem struct
    method Pos (line 948) | func (a *ArrayElem) Pos() Pos {
    method End (line 955) | func (a *ArrayElem) End() Pos {
  type ExtGlob (line 971) | type ExtGlob struct
    method wordPartNode (line 554) | func (*ExtGlob) wordPartNode()   {}
    method Pos (line 977) | func (e *ExtGlob) Pos() Pos { return e.OpPos }
    method End (line 978) | func (e *ExtGlob) End() Pos { return posAddCol(e.Pattern.End(), 1) }
  type ProcSubst (line 983) | type ProcSubst struct
    method wordPartNode (line 553) | func (*ProcSubst) wordPartNode() {}
    method Pos (line 991) | func (s *ProcSubst) Pos() Pos { return s.OpPos }
    method End (line 992) | func (s *ProcSubst) End() Pos { return posAddCol(s.Rparen, 1) }
  type TimeClause (line 998) | type TimeClause struct
    method commandNode (line 273) | func (*TimeClause) commandNode()   {}
    method Pos (line 1004) | func (c *TimeClause) Pos() Pos { return c.Time }
    method End (line 1005) | func (c *TimeClause) End() Pos {
  type CoprocClause (line 1015) | type CoprocClause struct
    method commandNode (line 274) | func (*CoprocClause) commandNode() {}
    method Pos (line 1021) | func (c *CoprocClause) Pos() Pos { return c.Coproc }
    method End (line 1022) | func (c *CoprocClause) End() Pos { return c.Stmt.End() }
  type LetClause (line 1027) | type LetClause struct
    method commandNode (line 272) | func (*LetClause) commandNode()    {}
    method Pos (line 1032) | func (l *LetClause) Pos() Pos { return l.Let }
    method End (line 1033) | func (l *LetClause) End() Pos { return l.Exprs[len(l.Exprs)-1].End() }
  type BraceExp (line 1038) | type BraceExp struct
    method wordPartNode (line 555) | func (*BraceExp) wordPartNode()  {}
    method Pos (line 1043) | func (b *BraceExp) Pos() Pos {
    method End (line 1047) | func (b *BraceExp) End() Pos {
  type TestDecl (line 1052) | type TestDecl struct
    method commandNode (line 275) | func (*TestDecl) commandNode()     {}
    method Pos (line 1058) | func (f *TestDecl) Pos() Pos { return f.Position }
    method End (line 1059) | func (f *TestDecl) End() Pos { return f.Body.End() }
  function wordLastEnd (line 1061) | func wordLastEnd(ws []*Word) Pos {

FILE: syntax/parser.go
  type ParserOption (line 19) | type ParserOption
  function KeepComments (line 23) | func KeepComments(enabled bool) ParserOption {
  type LangVariant (line 31) | type LangVariant
    method String (line 121) | func (l LangVariant) String() string {
    method Set (line 139) | func (l *LangVariant) Set(s string) error {
    method in (line 159) | func (l LangVariant) in(l2 LangVariant) bool {
    method count (line 163) | func (l LangVariant) count() int {
    method index (line 167) | func (l LangVariant) index() int {
    method bits (line 171) | func (l LangVariant) bits() iter.Seq[LangVariant] {
  constant LangBash (line 44) | LangBash LangVariant = 1 << iota
  constant LangPOSIX (line 50) | LangPOSIX
  constant LangMirBSDKorn (line 60) | LangMirBSDKorn
  constant LangBats (line 67) | LangBats
  constant LangZsh (line 77) | LangZsh
  constant LangAuto (line 84) | LangAuto
  constant langBashLegacy (line 88) | langBashLegacy LangVariant = 0
  constant langResolvedVariants (line 92) | langResolvedVariants = LangBash | LangPOSIX | LangMirBSDKorn | LangBats ...
  constant langResolvedVariantsCount (line 97) | langResolvedVariantsCount = 5
  constant langBashLike (line 100) | langBashLike = LangBash | LangBats
  function Variant (line 108) | func Variant(l LangVariant) ParserOption {
  function StopAt (line 197) | func StopAt(word string) ParserOption {
  function RecoverErrors (line 223) | func RecoverErrors(maximum int) ParserOption {
  function NewParser (line 228) | func NewParser(options ...ParserOption) *Parser {
  type wrappedReader (line 295) | type wrappedReader struct
    method Read (line 304) | func (w *wrappedReader) Read(p []byte) (n int, err error) {
  type Parser (line 480) | type Parser struct
    method Parse (line 244) | func (p *Parser) Parse(r io.Reader, name string) (*File, error) {
    method Stmts (line 262) | func (p *Parser) Stmts(r io.Reader, fn func(*Stmt) bool) error {
    method StmtsSeq (line 275) | func (p *Parser) StmtsSeq(r io.Reader) iter.Seq2[*Stmt, error] {
    method Interactive (line 328) | func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error {
    method InteractiveSeq (line 365) | func (p *Parser) InteractiveSeq(r io.Reader) iter.Seq2[[]*Stmt, error] {
    method Words (line 398) | func (p *Parser) Words(r io.Reader, fn func(*Word) bool) error {
    method WordsSeq (line 419) | func (p *Parser) WordsSeq(r io.Reader) iter.Seq2[*Word, error] {
    method Document (line 452) | func (p *Parser) Document(r io.Reader) (*Word, error) {
    method Arithmetic (line 467) | func (p *Parser) Arithmetic(r io.Reader) (ArithmExpr, error) {
    method Incomplete (line 553) | func (p *Parser) Incomplete() bool {
    method reset (line 561) | func (p *Parser) reset() {
    method nextPos (line 583) | func (p *Parser) nextPos() Pos {
    method lit (line 597) | func (p *Parser) lit(pos Pos, val string) *Lit {
    method wordAnyNumber (line 614) | func (p *Parser) wordAnyNumber() *Word {
    method wordOne (line 625) | func (p *Parser) wordOne(part WordPart) *Word {
    method call (line 637) | func (p *Parser) call(w *Word) *CallExpr {
    method preNested (line 692) | func (p *Parser) preNested(quote quoteState) (s saveState) {
    method postNested (line 698) | func (p *Parser) postNested(s saveState) {
    method unquotedWordBytes (line 702) | func (p *Parser) unquotedWordBytes(w *Word) ([]byte, bool) {
    method unquotedWordPart (line 711) | func (p *Parser) unquotedWordPart(buf []byte, wp WordPart, quotes bool...
    method doHeredocs (line 736) | func (p *Parser) doHeredocs() {
    method got (line 772) | func (p *Parser) got(tok token) bool {
    method gotRsrv (line 780) | func (p *Parser) gotRsrv(val string) (Pos, bool) {
    method recoverError (line 789) | func (p *Parser) recoverError() bool {
    method followErr (line 813) | func (p *Parser) followErr(pos Pos, left, right any) {
    method followErrExp (line 817) | func (p *Parser) followErrExp(pos Pos, left any) {
    method follow (line 821) | func (p *Parser) follow(lpos Pos, left string, tok token) {
    method followRsrv (line 827) | func (p *Parser) followRsrv(lpos Pos, left, val string) Pos {
    method followStmts (line 838) | func (p *Parser) followStmts(left string, lpos Pos, stops ...string) (...
    method followWordTok (line 867) | func (p *Parser) followWordTok(tok token, pos Pos) *Word {
    method stmtEnd (line 878) | func (p *Parser) stmtEnd(n Node, start, end string) Pos {
    method quoteErr (line 889) | func (p *Parser) quoteErr(lpos Pos, quote token) {
    method matchingErr (line 893) | func (p *Parser) matchingErr(lpos Pos, left, right token) {
    method matched (line 897) | func (p *Parser) matched(lpos Pos, left, right token) Pos {
    method errPass (line 908) | func (p *Parser) errPass(err error) {
    method posErr (line 1021) | func (p *Parser) posErr(pos Pos, format string, args ...any) {
    method curErr (line 1035) | func (p *Parser) curErr(format string, args ...any) {
    method checkLang (line 1039) | func (p *Parser) checkLang(pos Pos, langSet LangVariant, format string...
    method stmts (line 1057) | func (p *Parser) stmts(yield func(*Stmt, error) bool, stops ...string) {
    method stmtList (line 1106) | func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) {
    method invalidStmtStart (line 1140) | func (p *Parser) invalidStmtStart() {
    method getWord (line 1151) | func (p *Parser) getWord() *Word {
    method getLit (line 1158) | func (p *Parser) getLit() *Lit {
    method wordParts (line 1167) | func (p *Parser) wordParts(wps []WordPart) []WordPart {
    method ensureNoNested (line 1189) | func (p *Parser) ensureNoNested(pos Pos) {
    method wordPart (line 1195) | func (p *Parser) wordPart() WordPart {
    method cmdSubst (line 1392) | func (p *Parser) cmdSubst() *CmdSubst {
    method dblQuoted (line 1402) | func (p *Parser) dblQuoted() *DblQuoted {
    method paramExp (line 1430) | func (p *Parser) paramExp() *ParamExp {
    method nestedParameterStart (line 1618) | func (p *Parser) nestedParameterStart(pe *ParamExp) (left token, quote...
    method paramExpParameter (line 1650) | func (p *Parser) paramExpParameter(pe *ParamExp) *ParamExp {
    method paramExpExp (line 1725) | func (p *Parser) paramExpExp() *Expansion {
    method eitherIndex (line 1750) | func (p *Parser) eitherIndex() ArithmExpr {
    method zshSubFlags (line 1764) | func (p *Parser) zshSubFlags() *FlagsArithm {
    method stopToken (line 1790) | func (p *Parser) stopToken() bool {
    method backquoteEnd (line 1801) | func (p *Parser) backquoteEnd() bool {
    method hasValidIdent (line 1833) | func (p *Parser) hasValidIdent() bool {
    method getAssign (line 1850) | func (p *Parser) getAssign(needEqual bool) *Assign {
    method peekRedir (line 1970) | func (p *Parser) peekRedir() bool {
    method doRedirect (line 1980) | func (p *Parser) doRedirect(s *Stmt) {
    method getStmt (line 2031) | func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt {
    method gotStmtPipe (line 2102) | func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt {
    method subshell (line 2288) | func (p *Parser) subshell(s *Stmt) {
    method arithmExpCmd (line 2298) | func (p *Parser) arithmExpCmd(s *Stmt) {
    method block (line 2311) | func (p *Parser) block(s *Stmt) {
    method ifClause (line 2325) | func (p *Parser) ifClause(s *Stmt) {
    method whileClause (line 2361) | func (p *Parser) whileClause(s *Stmt, until bool) {
    method forClause (line 2377) | func (p *Parser) forClause(s *Stmt) {
    method loop (line 2399) | func (p *Parser) loop(fpos Pos) Loop {
    method wordIter (line 2423) | func (p *Parser) wordIter(ftok string, fpos Pos) *WordIter {
    method selectClause (line 2451) | func (p *Parser) selectClause(s *Stmt) {
    method caseClause (line 2461) | func (p *Parser) caseClause(s *Stmt) {
    method caseItems (line 2484) | func (p *Parser) caseItems(stop string) (items []*CaseItem) {
    method testClause (line 2546) | func (p *Parser) testClause(s *Stmt) {
    method testExprBinary (line 2561) | func (p *Parser) testExprBinary(pastAndOr bool) TestExpr {
    method testExprUnary (line 2621) | func (p *Parser) testExprUnary() TestExpr {
    method declClause (line 2675) | func (p *Parser) declClause(s *Stmt) {
    method timeClause (line 2715) | func (p *Parser) timeClause(s *Stmt) {
    method coprocClause (line 2725) | func (p *Parser) coprocClause(s *Stmt) {
    method letClause (line 2754) | func (p *Parser) letClause(s *Stmt) {
    method bashFuncDecl (line 2772) | func (p *Parser) bashFuncDecl(s *Stmt) {
    method testDecl (line 2800) | func (p *Parser) testDecl(s *Stmt) {
    method callExpr (line 2812) | func (p *Parser) callExpr(s *Stmt, w *Word, assign bool) {
    method funcDecl (line 2893) | func (p *Parser) funcDecl(s *Stmt, pos Pos, long, withParens bool, nam...
  constant bufSize (line 559) | bufSize = 1 << 10
  type wordAlloc (line 609) | type wordAlloc struct
  type quoteState (line 648) | type quoteState
  constant noState (line 652) | noState quoteState = 1 << iota
  constant runeByRune (line 656) | runeByRune
  constant unquotedWordCont (line 660) | unquotedWordCont
  constant subCmd (line 662) | subCmd
  constant subCmdBckquo (line 663) | subCmdBckquo
  constant dblQuotes (line 664) | dblQuotes
  constant hdocWord (line 665) | hdocWord
  constant hdocBody (line 666) | hdocBody
  constant hdocBodyTabs (line 667) | hdocBodyTabs
  constant arithmExpr (line 668) | arithmExpr
  constant arithmExprLet (line 669) | arithmExprLet
  constant arithmExprCmd (line 670) | arithmExprCmd
  constant testExpr (line 671) | testExpr
  constant testExprRegexp (line 672) | testExprRegexp
  constant switchCase (line 673) | switchCase
  constant paramExpArithm (line 674) | paramExpArithm
  constant paramExpRepl (line 675) | paramExpRepl
  constant paramExpExp (line 676) | paramExpExp
  constant arrayElems (line 677) | arrayElems
  constant allKeepSpaces (line 679) | allKeepSpaces = runeByRune | paramExpRepl | dblQuotes | hdocBody |
  constant allRegTokens (line 681) | allRegTokens = noState | unquotedWordCont | subCmd | subCmdBckquo | hdoc...
  constant allArithmExpr (line 683) | allArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd | paramExpArithm
  constant allParamExp (line 684) | allParamExp   = paramExpArithm | paramExpRepl | paramExpExp
  type saveState (line 687) | type saveState struct
  type noQuote (line 797) | type noQuote
    method Format (line 799) | func (s noQuote) Format(f fmt.State, verb rune) {
  method Format (line 803) | func (t token) Format(f fmt.State, verb rune) {
  function IsIncomplete (line 921) | func IsIncomplete(err error) bool {
  function IsKeyword (line 931) | func IsKeyword(word string) bool {
  type ParseError (line 964) | type ParseError struct
    method Error (line 972) | func (e ParseError) Error() string {
  type LangError (line 982) | type LangError struct
    method Error (line 996) | func (e LangError) Error() string {
  function ValidName (line 1806) | func ValidName(val string) bool {
  function numberLiteral (line 1821) | func numberLiteral[T string | []byte](val T) bool {
  function isBashCompoundCommand (line 2700) | func isBashCompoundCommand(tok token, val string) bool {

FILE: syntax/parser_arithm.go
  method arithmExpr (line 5) | func (p *Parser) arithmExpr(compact bool) ArithmExpr {
  method arithmExprComma (line 11) | func (p *Parser) arithmExprComma(compact bool) ArithmExpr {
  method arithmExprAssign (line 15) | func (p *Parser) arithmExprAssign(compact bool) ArithmExpr {
  method arithmExprTernary (line 46) | func (p *Parser) arithmExprTernary(compact bool) ArithmExpr {
  method arithmExprLor (line 86) | func (p *Parser) arithmExprLor(compact bool) ArithmExpr {
  method arithmExprLand (line 90) | func (p *Parser) arithmExprLand(compact bool) ArithmExpr {
  method arithmExprBor (line 94) | func (p *Parser) arithmExprBor(compact bool) ArithmExpr {
  method arithmExprBxor (line 98) | func (p *Parser) arithmExprBxor(compact bool) ArithmExpr {
  method arithmExprBand (line 102) | func (p *Parser) arithmExprBand(compact bool) ArithmExpr {
  method arithmExprEquality (line 106) | func (p *Parser) arithmExprEquality(compact bool) ArithmExpr {
  method arithmExprComparison (line 110) | func (p *Parser) arithmExprComparison(compact bool) ArithmExpr {
  method arithmExprShift (line 114) | func (p *Parser) arithmExprShift(compact bool) ArithmExpr {
  method arithmExprAddition (line 118) | func (p *Parser) arithmExprAddition(compact bool) ArithmExpr {
  method arithmExprMultiplication (line 122) | func (p *Parser) arithmExprMultiplication(compact bool) ArithmExpr {
  method arithmExprPower (line 126) | func (p *Parser) arithmExprPower(compact bool) ArithmExpr {
  method arithmExprUnary (line 152) | func (p *Parser) arithmExprUnary(compact bool) ArithmExpr {
  method arithmExprValue (line 169) | func (p *Parser) arithmExprValue(compact bool) ArithmExpr {
  method nextArith (line 246) | func (p *Parser) nextArith(compact bool) bool {
  method nextArithOp (line 257) | func (p *Parser) nextArithOp(compact bool) {
  method arithmExprBinary (line 266) | func (p *Parser) arithmExprBinary(compact bool, nextOp func(bool) Arithm...
  function isArithName (line 301) | func isArithName(left ArithmExpr) bool {
  method followArithm (line 316) | func (p *Parser) followArithm(ftok token, fpos Pos) ArithmExpr {
  method peekArithmEnd (line 324) | func (p *Parser) peekArithmEnd() bool {
  method arithmMatchingErr (line 328) | func (p *Parser) arithmMatchingErr(pos Pos, left, right token) {
  method matchedArithm (line 348) | func (p *Parser) matchedArithm(lpos Pos, left, right token) {
  method arithmEnd (line 354) | func (p *Parser) arithmEnd(ltok token, lpos Pos, old saveState) Pos {

FILE: syntax/parser_linux_test.go
  function killCommandOnTestExit (line 11) | func killCommandOnTestExit(cmd *exec.Cmd) {

FILE: syntax/parser_other_test.go
  function killCommandOnTestExit (line 10) | func killCommandOnTestExit(cmd *exec.Cmd) {

FILE: syntax/parser_test.go
  function TestParseFiles (line 26) | func TestParseFiles(t *testing.T) {
  function TestParseErr (line 61) | func TestParseErr(t *testing.T) {
  function TestParseConfirm (line 88) | func TestParseConfirm(t *testing.T) {
  function TestParseBashKeepComments (line 133) | func TestParseBashKeepComments(t *testing.T) {
  function TestParsePosOverflow (line 147) | func TestParsePosOverflow(t *testing.T) {
  function TestMain (line 209) | func TestMain(m *testing.M) {
  type externalShell (line 240) | type externalShell struct
  function skipExternal (line 249) | func skipExternal(tb testing.TB, message string) {
  function cmdContains (line 283) | func cmdContains(substr, cmd string, args ...string) bool {
  function confirmParse (line 294) | func confirmParse(in, cmd string, wantErr bool) func(*testing.T) {
  function singleParse (line 364) | func singleParse(p *Parser, in string, want *File) func(t *testing.T) {
  type errorCase (line 377) | type errorCase struct
  function errCase (line 386) | func errCase(in string, opts ...func(*errorCase)) errorCase {
  function langErr (line 394) | func langErr(want string, langSets ...LangVariant) func(*errorCase) {
  function flipConfirm (line 414) | func flipConfirm(langSet LangVariant) func(*errorCase) {
  function init (line 424) | func init() {
  function TestInputName (line 2075) | func TestInputName(t *testing.T) {
  type badReader (line 2093) | type badReader struct
    method Read (line 2095) | func (b badReader) Read(p []byte) (int, error) { return 0, errBadReader }
  function TestReadErr (line 2097) | func TestReadErr(t *testing.T) {
  type strictStringReader (line 2110) | type strictStringReader struct
    method Read (line 2119) | func (r *strictStringReader) Read(p []byte) (int, error) {
  function newStrictReader (line 2115) | func newStrictReader(s string) *strictStringReader {
  function TestParseStmtsSeq (line 2130) | func TestParseStmtsSeq(t *testing.T) {
  function TestParseStmtsSeqStopEarly (line 2157) | func TestParseStmtsSeqStopEarly(t *testing.T) {
  function TestParseStmtsSeqError (line 2186) | func TestParseStmtsSeqError(t *testing.T) {
  function TestParseWords (line 2214) | func TestParseWords(t *testing.T) {
  function TestParseWordsStopEarly (line 2242) | func TestParseWordsStopEarly(t *testing.T) {
  function TestParseWordsError (line 2259) | func TestParseWordsError(t *testing.T) {
  function TestParseDocument (line 2303) | func TestParseDocument(t *testing.T) {
  function TestParseDocumentError (line 2320) | func TestParseDocumentError(t *testing.T) {
  function TestParseArithmetic (line 2397) | func TestParseArithmetic(t *testing.T) {
  function TestParseArithmeticError (line 2413) | func TestParseArithmeticError(t *testing.T) {
  function TestParseStopAt (line 2468) | func TestParseStopAt(t *testing.T) {
  function TestValidName (line 2477) | func TestValidName(t *testing.T) {
  function TestIsIncomplete (line 2502) | func TestIsIncomplete(t *testing.T) {
  function TestPosEdgeCases (line 2560) | func TestPosEdgeCases(t *testing.T) {
  function TestParseRecoverErrors (line 2582) | func TestParseRecoverErrors(t *testing.T) {
  function countRecoveredPositions (line 2724) | func countRecoveredPositions(x reflect.Value) int {

FILE: syntax/printer.go
  type PrinterOption (line 21) | type PrinterOption
  function Indent (line 25) | func Indent(spaces uint) PrinterOption {
  function BinaryNextLine (line 32) | func BinaryNextLine(enabled bool) PrinterOption {
  function SwitchCaseIndent (line 38) | func SwitchCaseIndent(enabled bool) PrinterOption {
  function SpaceRedirects (line 47) | func SpaceRedirects(enabled bool) PrinterOption {
  function KeepPadding (line 63) | func KeepPadding(enabled bool) PrinterOption {
  function Minify (line 83) | func Minify(enabled bool) PrinterOption {
  function SingleLine (line 93) | func SingleLine(enabled bool) PrinterOption {
  function FunctionNextLine (line 98) | func FunctionNextLine(enabled bool) PrinterOption {
  function NewPrinter (line 103) | func NewPrinter(opts ...PrinterOption) *Printer {
  type bufWriter (line 177) | type bufWriter interface
  type colCounter (line 185) | type colCounter struct
    method addByte (line 191) | func (c *colCounter) addByte(b byte) {
    method WriteByte (line 203) | func (c *colCounter) WriteByte(b byte) error {
    method WriteString (line 208) | func (c *colCounter) WriteString(s string) (int, error) {
    method Reset (line 215) | func (c *colCounter) Reset(w io.Writer) {
  type Printer (line 223) | type Printer struct
    method Print (line 120) | func (p *Printer) Print(w io.Writer, node Node) error {
    method reset (line 272) | func (p *Printer) reset() {
    method spaces (line 287) | func (p *Printer) spaces(n uint) {
    method space (line 293) | func (p *Printer) space() {
    method spacePad (line 298) | func (p *Printer) spacePad(pos Pos) {
    method wantsNewline (line 316) | func (p *Printer) wantsNewline(pos Pos, escapingNewline bool) bool {
    method bslashNewl (line 335) | func (p *Printer) bslashNewl() {
    method spacedString (line 344) | func (p *Printer) spacedString(s string, pos Pos) {
    method spacedToken (line 350) | func (p *Printer) spacedToken(s string, pos Pos) {
    method semiOrNewl (line 361) | func (p *Printer) semiOrNewl(s string, pos Pos) {
    method writeLit (line 378) | func (p *Printer) writeLit(s string) {
    method incLevel (line 389) | func (p *Printer) incLevel() {
    method decLevel (line 401) | func (p *Printer) decLevel() {
    method indent (line 408) | func (p *Printer) indent() {
    method newline (line 429) | func (p *Printer) newline(pos Pos) {
    method advanceLine (line 438) | func (p *Printer) advanceLine(line uint) {
    method flushHeredocs (line 444) | func (p *Printer) flushHeredocs() {
    method newlines (line 515) | func (p *Printer) newlines(pos Pos) {
    method rightParen (line 537) | func (p *Printer) rightParen(pos Pos) {
    method semiRsrv (line 545) | func (p *Printer) semiRsrv(s string, pos Pos) {
    method flushComments (line 560) | func (p *Printer) flushComments() {
    method comments (line 599) | func (p *Printer) comments(comments ...Comment) {
    method wordParts (line 613) | func (p *Printer) wordParts(wps []WordPart, quoted bool) {
    method wordPart (line 642) | func (p *Printer) wordPart(wp, next WordPart) {
    method dblQuoted (line 699) | func (p *Printer) dblQuoted(dq *DblQuoted) {
    method wroteIndex (line 715) | func (p *Printer) wroteIndex(index ArithmExpr) bool {
    method paramExp (line 727) | func (p *Printer) paramExp(pe *ParamExp) {
    method cmdSubst (line 801) | func (p *Printer) cmdSubst(cs *CmdSubst) {
    method loop (line 832) | func (p *Printer) loop(loop Loop) {
    method arithmExpr (line 854) | func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus ...
    method arithmExprRecurse (line 861) | func (p *Printer) arithmExprRecurse(expr ArithmExpr, compact, spacePlu...
    method testExpr (line 907) | func (p *Printer) testExpr(expr TestExpr) {
    method testExprSameLine (line 918) | func (p *Printer) testExprSameLine(expr TestExpr) {
    method word (line 951) | func (p *Printer) word(w *Word) {
    method unquotedWord (line 956) | func (p *Printer) unquotedWord(w *Word) {
    method wordJoin (line 977) | func (p *Printer) wordJoin(ws []*Word) {
    method casePatternJoin (line 995) | func (p *Printer) casePatternJoin(pats []*Word) {
    method elemJoin (line 1021) | func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) {
    method stmt (line 1054) | func (p *Printer) stmt(s *Stmt) {
    method printRedirsUntil (line 1107) | func (p *Printer) printRedirsUntil(redirs []*Redirect, startRedirs int...
    method command (line 1130) | func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedir...
    method ifClause (line 1372) | func (p *Printer) ifClause(ic *IfClause, elif bool) {
    method stmtList (line 1410) | func (p *Printer) stmtList(stmts []*Stmt, last []Comment) {
    method nestedStmts (line 1453) | func (p *Printer) nestedStmts(stmts []*Stmt, last []Comment, closing P...
    method assigns (line 1479) | func (p *Printer) assigns(assigns []*Assign) {
  type wantSpaceState (line 1514) | type wantSpaceState
  constant spaceNotRequired (line 1517) | spaceNotRequired wantSpaceState = iota
  constant spaceRequired (line 1518) | spaceRequired
  constant spaceWritten (line 1519) | spaceWritten
  type extraIndenter (line 1525) | type extraIndenter struct
    method WriteByte (line 1534) | func (e *extraIndenter) WriteByte(b byte) error {
    method WriteString (line 1574) | func (e *extraIndenter) WriteString(s string) (int, error) {
  function startsWithLparen (line 1581) | func startsWithLparen(node Node) bool {

FILE: syntax/printer_test.go
  function TestPrintFiles (line 15) | func TestPrintFiles(t *testing.T) {
  function strPrint (line 41) | func strPrint(p *Printer, node Node) (string, error) {
  type printCase (line 47) | type printCase struct
  function samePrint (line 51) | func samePrint(s string) printCase { return printCase{in: s, want: s} }
  function TestPrintTable (line 666) | func TestPrintTable(t *testing.T) {
  function parsePath (line 683) | func parsePath(tb testing.TB, path string) *File {
  constant canonicalPath (line 696) | canonicalPath = "canonical.sh"
  function TestPrintMultiline (line 698) | func TestPrintMultiline(t *testing.T) {
  function TestPrintSpaces (line 720) | func TestPrintSpaces(t *testing.T) {
  type badWriter (line 764) | type badWriter struct
    method Write (line 766) | func (b badWriter) Write(p []byte) (int, error) { return 0, errBadWrit...
  function TestWriteErr (line 768) | func TestWriteErr(t *testing.T) {
  function TestPrintBinaryNextLine (line 789) | func TestPrintBinaryNextLine(t *testing.T) {
  function TestPrintSwitchCaseIndent (line 859) | func TestPrintSwitchCaseIndent(t *testing.T) {
  function TestPrintFunctionNextLine (line 881) | func TestPrintFunctionNextLine(t *testing.T) {
  function TestPrintSpaceRedirects (line 918) | func TestPrintSpaceRedirects(t *testing.T) {
  function TestPrintKeepPadding (line 940) | func TestPrintKeepPadding(t *testing.T) {
  function TestPrintKeepPaddingSpaces (line 983) | func TestPrintKeepPaddingSpaces(t *testing.T) {
  function TestPrintMinify (line 1001) | func TestPrintMinify(t *testing.T) {
  function TestPrintSingleLine (line 1104) | func TestPrintSingleLine(t *testing.T) {
  function TestPrintOptionsNotBroken (line 1160) | func TestPrintOptionsNotBroken(t *testing.T) {
  function printTest (line 1232) | func printTest(t *testing.T, parser *Parser, printer *Printer, in, want ...
  function TestPrintNodeTypes (line 1266) | func TestPrintNodeTypes(t *testing.T) {
  function TestPrintManyStmts (line 1347) | func TestPrintManyStmts(t *testing.T) {
  function TestKeepPaddingRepeated (line 1381) | func TestKeepPaddingRepeated(t *testing.T) {

FILE: syntax/quote.go
  type QuoteError (line 13) | type QuoteError struct
    method Error (line 18) | func (e QuoteError) Error() string {
  constant quoteErrNull (line 23) | quoteErrNull  = "shell strings cannot contain null bytes"
  constant quoteErrPOSIX (line 24) | quoteErrPOSIX = "POSIX shell lacks escape sequences"
  constant quoteErrRange (line 25) | quoteErrRange = "rune out of range"
  constant quoteErrMksh (line 26) | quoteErrMksh  = "mksh cannot escape codepoints above 16 bits"
  function Quote (line 48) | func Quote(s string, lang LangVariant) (string, error) {
  function isHex (line 181) | func isHex(r rune) bool {

FILE: syntax/quote_test.go
  function TestQuote (line 12) | func TestQuote(t *testing.T) {

FILE: syntax/simplify.go
  function Simplify (line 19) | func Simplify(n Node) bool {
  type simplifier (line 25) | type simplifier struct
    method visit (line 29) | func (s *simplifier) visit(node Node) bool {
    method simplifyWord (line 91) | func (s *simplifier) simplifyWord(wps []WordPart) []WordPart {
    method removeParensArithm (line 138) | func (s *simplifier) removeParensArithm(x ArithmExpr) ArithmExpr {
    method inlineSimpleParams (line 149) | func (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr {
    method inlineSubshell (line 171) | func (s *simplifier) inlineSubshell(stmts []*Stmt) []*Stmt {
    method unquoteParams (line 188) | func (s *simplifier) unquoteParams(x TestExpr) TestExpr {
    method removeParensTest (line 205) | func (s *simplifier) removeParensTest(x TestExpr) TestExpr {
    method removeNegateTest (line 216) | func (s *simplifier) removeNegateTest(x TestExpr) TestExpr {

FILE: syntax/simplify_test.go
  type simplifyTest (line 12) | type simplifyTest struct
  function noSimple (line 16) | func noSimple(in string) simplifyTest {
  function TestSimplify (line 72) | func TestSimplify(t *testing.T) {

FILE: syntax/token_string.go
  function _ (line 7) | func _() {
  constant _token_name (line 157) | _token_name = "illegalTokEOFNewlLitLitWordLitRedirrealTokenBoundary'\"`&...
  method String (line 161) | func (i token) String() string {

FILE: syntax/tokens.go
  type token (line 8) | type token
    method isLit (line 174) | func (t token) isLit() bool {
  constant illegalTok (line 12) | illegalTok token = iota
  constant _EOF (line 14) | _EOF
  constant _Newl (line 15) | _Newl
  constant _Lit (line 16) | _Lit
  constant _LitWord (line 17) | _LitWord
  constant _LitRedir (line 18) | _LitRedir
  constant _realTokenBoundary (line 21) | _realTokenBoundary
  constant sglQuote (line 23) | sglQuote
  constant dblQuote (line 24) | dblQuote
  constant bckQuote (line 25) | bckQuote
  constant and (line 27) | and
  constant andAnd (line 28) | andAnd
  constant orOr (line 29) | orOr
  constant or (line 30) | or
  constant orAnd (line 31) | orAnd
  constant andPipe (line 32) | andPipe
  constant andBang (line 33) | andBang
  constant dollar (line 35) | dollar
  constant dollSglQuote (line 36) | dollSglQuote
  constant dollDblQuote (line 37) | dollDblQuote
  constant dollBrace (line 38) | dollBrace
  constant dollBrack (line 39) | dollBrack
  constant dollParen (line 40) | dollParen
  constant dollDblParen (line 41) | dollDblParen
  constant leftBrace (line 42) | leftBrace
  constant leftBrack (line 43) | leftBrack
  constant dblLeftBrack (line 44) | dblLeftBrack
  constant leftParen (line 45) | leftParen
  constant dblLeftParen (line 46) | dblLeftParen
  constant rightBrace (line 48) | rightBrace
  constant rightBrack (line 49) | rightBrack
  constant dblRightBrack (line 50) | dblRightBrack
  constant rightParen (line 51) | rightParen
  constant dblRightParen (line 52) | dblRightParen
  constant semicolon (line 53) | semicolon
  constant dblSemicolon (line 55) | dblSemicolon
  constant semiAnd (line 56) | semiAnd
  constant dblSemiAnd (line 57) | dblSemiAnd
  constant semiOr (line 58) | semiOr
  constant exclMark (line 60) | exclMark
  constant tilde (line 61) | tilde
  constant addAdd (line 62) | addAdd
  constant subSub (line 63) | subSub
  constant star (line 64) | star
  constant power (line 65) | power
  constant equal (line 66) | equal
  constant nequal (line 67) | nequal
  constant lequal (line 68) | lequal
  constant gequal (line 69) | gequal
  constant addAssgn (line 71) | addAssgn
  constant subAssgn (line 72) | subAssgn
  constant mulAssgn (line 73) | mulAssgn
  constant quoAssgn (line 74) | quoAssgn
  constant remAssgn (line 75) | remAssgn
  constant andAssgn (line 76) | andAssgn
  constant orAssgn (line 77) | orAssgn
  constant xorAssgn (line 78) | xorAssgn
  constant shlAssgn (line 79) | shlAssgn
  constant shrAssgn (line 80) | shrAssgn
  constant andBoolAssgn (line 81) | andBoolAssgn
  constant orBoolAssgn (line 82) | orBoolAssgn
  constant xorBoolAssgn (line 83) | xorBoolAssgn
  constant powAssgn (line 84) | powAssgn
  constant rdrOut (line 86) | rdrOut
  constant appOut (line 87) | appOut
  constant rdrIn (line 88) | rdrIn
  constant rdrInOut (line 89) | rdrInOut
  constant dplIn (line 90) | dplIn
  constant dplOut (line 91) | dplOut
  constant rdrClob (line 92) | rdrClob
  constant appClob (line 93) | appClob
  constant hdoc (line 94) | hdoc
  constant dashHdoc (line 95) | dashHdoc
  constant wordHdoc (line 96) | wordHdoc
  constant rdrAll (line 97) | rdrAll
  constant rdrAllClob (line 98) | rdrAllClob
  constant appAll (line 99) | appAll
  constant appAllClob (line 100) | appAllClob
  constant cmdIn (line 102) | cmdIn
  constant assgnParen (line 103) | assgnParen
  constant cmdOut (line 104) | cmdOut
  constant plus (line 106) | plus
  constant colPlus (line 107) | colPlus
  constant minus (line 108) | minus
  constant colMinus (line 109) | colMinus
  constant quest (line 110) | quest
  constant colQuest (line 111) | colQuest
  constant assgn (line 112) | assgn
  constant colAssgn (line 113) | colAssgn
  constant perc (line 114) | perc
  constant dblPerc (line 115) | dblPerc
  constant hash (line 116) | hash
  constant dblHash (line 117) | dblHash
  constant colHash (line 118) | colHash
  constant colPipe (line 119) | colPipe
  constant colStar (line 120) | colStar
  constant caret (line 121) | caret
  constant dblCaret (line 122) | dblCaret
  constant comma (line 123) | comma
  constant dblComma (line 124) | dblComma
  constant at (line 125) | at
  constant slash (line 126) | slash
  constant dblSlash (line 127) | dblSlash
  constant period (line 128) | period
  constant colon (line 129) | colon
  constant tsExists (line 131) | tsExists
  constant tsRegFile (line 132) | tsRegFile
  constant tsDirect (line 133) | tsDirect
  constant tsCharSp (line 134) | tsCharSp
  constant tsBlckSp (line 135) | tsBlckSp
  constant tsNmPipe (line 136) | tsNmPipe
  constant tsSocket (line 137) | tsSocket
  constant tsSmbLink (line 138) | tsSmbLink
  constant tsSticky (line 139) | tsSticky
  constant tsGIDSet (line 140) | tsGIDSet
  constant tsUIDSet (line 141) | tsUIDSet
  constant tsGrpOwn (line 142) | tsGrpOwn
  constant tsUsrOwn (line 143) | tsUsrOwn
  constant tsModif (line 144) | tsModif
  constant tsRead (line 145) | tsRead
  constant tsWrite (line 146) | tsWrite
  constant tsExec (line 147) | tsExec
  constant tsNoEmpty (line 148) | tsNoEmpty
  constant tsFdTerm (line 149) | tsFdTerm
  constant tsEmpStr (line 150) | tsEmpStr
  constant tsNempStr (line 151) | tsNempStr
  constant tsOptSet (line 152) | tsOptSet
  constant tsVarSet (line 153) | tsVarSet
  constant tsRefVar (line 154) | tsRefVar
  constant tsReMatch (line 156) | tsReMatch
  constant tsNewer (line 157) | tsNewer
  constant tsOlder (line 158) | tsOlder
  constant tsDevIno (line 159) | tsDevIno
  constant tsEql (line 160) | tsEql
  constant tsNeq (line 161) | tsNeq
  constant tsLeq (line 162) | tsLeq
  constant tsGeq (line 163) | tsGeq
  constant tsLss (line 164) | tsLss
  constant tsGtr (line 165) | tsGtr
  constant globQuest (line 167) | globQuest
  constant globStar (line 168) | globStar
  constant globPlus (line 169) | globPlus
  constant globAt (line 170) | globAt
  constant globExcl (line 171) | globExcl
  type RedirOperator (line 178) | type RedirOperator
    method String (line 382) | func (o RedirOperator) String() string    { return token(o).String() }
  constant RdrOut (line 181) | RdrOut     = RedirOperator(rdrOut) + iota
  constant AppOut (line 182) | AppOut
  constant RdrIn (line 183) | RdrIn
  constant RdrInOut (line 184) | RdrInOut
  constant DplIn (line 185) | DplIn
  constant DplOut (line 186) | DplOut
  constant RdrClob (line 187) | RdrClob
  constant AppClob (line 188) | AppClob
  constant Hdoc (line 189) | Hdoc
  constant DashHdoc (line 190) | DashHdoc
  constant WordHdoc (line 191) | WordHdoc
  constant RdrAll (line 192) | RdrAll
  constant RdrAllClob (line 193) | RdrAllClob
  constant AppAll (line 194) | AppAll
  constant AppAllClob (line 195) | AppAllClob
  constant ClbOut (line 200) | ClbOut = RdrClob
  type ProcOperator (line 203) | type ProcOperator
    method String (line 383) | func (o ProcOperator) String() string     { return token(o).String() }
  constant CmdIn (line 206) | CmdIn     = ProcOperator(cmdIn) + iota
  constant CmdInTemp (line 207) | CmdInTemp
  constant CmdOut (line 208) | CmdOut
  type GlobOperator (line 211) | type GlobOperator
    method String (line 384) | func (o GlobOperator) String() string     { return token(o).String() }
  constant GlobZeroOrOne (line 214) | GlobZeroOrOne  = GlobOperator(globQuest) + iota
  constant GlobZeroOrMore (line 215) | GlobZeroOrMore
  constant GlobOneOrMore (line 216) | GlobOneOrMore
  constant GlobOne (line 217) | GlobOne
  constant GlobExcept (line 218) | GlobExcept
  type BinCmdOperator (line 221) | type BinCmdOperator
    method String (line 385) | func (o BinCmdOperator) String() string   { return token(o).String() }
  constant AndStmt (line 224) | AndStmt = BinCmdOperator(andAnd) + iota
  constant OrStmt (line 225) | OrStmt
  constant Pipe (line 226) | Pipe
  constant PipeAll (line 227) | PipeAll
  type CaseOperator (line 230) | type CaseOperator
    method String (line 386) | func (o CaseOperator) String() string     { return token(o).String() }
  constant Break (line 233) | Break       = CaseOperator(dblSemicolon) + iota
  constant Fallthrough (line 234) | Fallthrough
  constant Resume (line 235) | Resume
  constant ResumeKorn (line 236) | ResumeKorn
  type ParNamesOperator (line 239) | type ParNamesOperator
    method String (line 387) | func (o ParNamesOperator) String() string { return token(o).String() }
  constant NamesPrefix (line 242) | NamesPrefix      = ParNamesOperator(star)
  constant NamesPrefixWords (line 243) | NamesPrefixWords = ParNamesOperator(at)
  type ParExpOperator (line 246) | type ParExpOperator
    method String (line 388) | func (o ParExpOperator) String() string   { return token(o).String() }
  constant AlternateUnset (line 249) | AlternateUnset       = ParExpOperator(plus) + iota
  constant AlternateUnsetOrNull (line 250) | AlternateUnsetOrNull
  constant DefaultUnset (line 251) | DefaultUnset
  constant DefaultUnsetOrNull (line 252) | DefaultUnsetOrNull
  constant ErrorUnset (line 253) | ErrorUnset
  constant ErrorUnsetOrNull (line 254) | ErrorUnsetOrNull
  constant AssignUnset (line 255) | AssignUnset
  constant AssignUnsetOrNull (line 256) | AssignUnsetOrNull
  constant RemSmallSuffix (line 257) | RemSmallSuffix
  constant RemLargeSuffix (line 258) | RemLargeSuffix
  constant RemSmallPrefix (line 259) | RemSmallPrefix
  constant RemLargePrefix (line 260) | RemLargePrefix
  constant MatchEmpty (line 261) | MatchEmpty
  constant ArrayExclude (line 262) | ArrayExclude
  constant ArrayIntersect (line 263) | ArrayIntersect
  constant UpperFirst (line 264) | UpperFirst
  constant UpperAll (line 265) | UpperAll
  constant LowerFirst (line 266) | LowerFirst
  constant LowerAll (line 267) | LowerAll
  constant OtherParamOps (line 268) | OtherParamOps
  type UnAritOperator (line 271) | type UnAritOperator
    method String (line 389) | func (o UnAritOperator) String() string   { return token(o).String() }
  constant Not (line 274) | Not         = UnAritOperator(exclMark) + iota
  constant BitNegation (line 275) | BitNegation
  constant Inc (line 276) | Inc
  constant Dec (line 277) | Dec
  constant Plus (line 278) | Plus        = UnAritOperator(plus)
  constant Minus (line 279) | Minus       = UnAritOperator(minus)
  type BinAritOperator (line 282) | type BinAritOperator
    method String (line 390) | func (o BinAritOperator) String() string  { return token(o).String() }
  constant Add (line 285) | Add = BinAritOperator(plus)
  constant Sub (line 286) | Sub = BinAritOperator(minus)
  constant Mul (line 287) | Mul = BinAritOperator(star)
  constant Quo (line 288) | Quo = BinAritOperator(slash)
  constant Rem (line 289) | Rem = BinAritOperator(perc)
  constant Pow (line 290) | Pow = BinAritOperator(power)
  constant Eql (line 291) | Eql = BinAritOperator(equal)
  constant Gtr (line 292) | Gtr = BinAritOperator(rdrOut)
  constant Lss (line 293) | Lss = BinAritOperator(rdrIn)
  constant Neq (line 294) | Neq = BinAritOperator(nequal)
  constant Leq (line 295) | Leq = BinAritOperator(lequal)
  constant Geq (line 296) | Geq = BinAritOperator(gequal)
  constant And (line 297) | And = BinAritOperator(and)
  constant Or (line 298) | Or  = BinAritOperator(or)
  constant Xor (line 299) | Xor = BinAritOperator(caret)
  constant Shr (line 300) | Shr = BinAritOperator(appOut)
  constant Shl (line 301) | Shl = BinAritOperator(hdoc)
  constant AndArit (line 305) | AndArit   = BinAritOperator(andAnd)
  constant OrArit (line 306) | OrArit    = BinAritOperator(orOr)
  constant XorBool (line 307) | XorBool   = BinAritOperator(dblCaret)
  constant Comma (line 308) | Comma     = BinAritOperator(comma)
  constant TernQuest (line 309) | TernQuest = BinAritOperator(quest)
  constant TernColon (line 310) | TernColon = BinAritOperator(colon)
  constant Assgn (line 312) | Assgn        = BinAritOperator(assgn)
  constant AddAssgn (line 313) | AddAssgn     = BinAritOperator(addAssgn)
  constant SubAssgn (line 314) | SubAssgn     = BinAritOperator(subAssgn)
  constant MulAssgn (line 315) | MulAssgn     = BinAritOperator(mulAssgn)
  constant QuoAssgn (line 316) | QuoAssgn     = BinAritOperator(quoAssgn)
  constant RemAssgn (line 317) | RemAssgn     = BinAritOperator(remAssgn)
  constant AndAssgn (line 318) | AndAssgn     = BinAritOperator(andAssgn)
  constant OrAssgn (line 319) | OrAssgn      = BinAritOperator(orAssgn)
  constant XorAssgn (line 320) | XorAssgn     = BinAritOperator(xorAssgn)
  constant ShlAssgn (line 321) | ShlAssgn     = BinAritOperator(shlAssgn)
  constant ShrAssgn (line 322) | ShrAssgn     = BinAritOperator(shrAssgn)
  constant AndBoolAssgn (line 323) | AndBoolAssgn = BinAritOperator(andBoolAssgn)
  constant OrBoolAssgn (line 324) | OrBoolAssgn  = BinAritOperator(orBoolAssgn)
  constant XorBoolAssgn (line 325) | XorBoolAssgn = BinAritOperator(xorBoolAssgn)
  constant PowAssgn (line 326) | PowAssgn     = BinAritOperator(powAssgn)
  type UnTestOperator (line 329) | type UnTestOperator
    method String (line 391) | func (o UnTestOperator) String() string   { return token(o).String() }
  constant TsExists (line 332) | TsExists  = UnTestOperator(tsExists) + iota
  constant TsRegFile (line 333) | TsRegFile
  constant TsDirect (line 334) | TsDirect
  constant TsCharSp (line 335) | TsCharSp
  constant TsBlckSp (line 336) | TsBlckSp
  constant TsNmPipe (line 337) | TsNmPipe
  constant TsSocket (line 338) | TsSocket
  constant TsSmbLink (line 339) | TsSmbLink
  constant TsSticky (line 340) | TsSticky
  constant TsGIDSet (line 341) | TsGIDSet
  constant TsUIDSet (line 342) | TsUIDSet
  constant TsGrpOwn (line 343) | TsGrpOwn
  constant TsUsrOwn (line 344) | TsUsrOwn
  constant TsModif (line 345) | TsModif
  constant TsRead (line 346) | TsRead
  constant TsWrite (line 347) | TsWrite
  constant TsExec (line 348) | TsExec
  constant TsNoEmpty (line 349) | TsNoEmpty
  constant TsFdTerm (line 350) | TsFdTerm
  constant TsEmpStr (line 351) | TsEmpStr
  constant TsNempStr (line 352) | TsNempStr
  constant TsOptSet (line 353) | TsOptSet
  constant TsVarSet (line 354) | TsVarSet
  constant TsRefVar (line 355) | TsRefVar
  constant TsNot (line 356) | TsNot     = UnTestOperator(exclMark)
  constant TsParen (line 357) | TsParen   = UnTestOperator(leftParen)
  type BinTestOperator (line 360) | type BinTestOperator
    method String (line 392) | func (o BinTestOperator) String() string  { return token(o).String() }
  constant TsReMatch (line 363) | TsReMatch    = BinTestOperator(tsReMatch) + iota
  constant TsNewer (line 364) | TsNewer
  constant TsOlder (line 365) | TsOlder
  constant TsDevIno (line 366) | TsDevIno
  constant TsEql (line 367) | TsEql
  constant TsNeq (line 368) | TsNeq
  constant TsLeq (line 369) | TsLeq
  constant TsGeq (line 370) | TsGeq
  constant TsLss (line 371) | TsLss
  constant TsGtr (line 372) | TsGtr
  constant AndTest (line 373) | AndTest      = BinTestOperator(andAnd)
  constant OrTest (line 374) | OrTest       = BinTestOperator(orOr)
  constant TsMatchShort (line 375) | TsMatchShort = BinTestOperator(assgn)
  constant TsMatch (line 376) | TsMatch      = BinTestOperator(equal)
  constant TsNoMatch (line 377) | TsNoMatch    = BinTestOperator(nequal)
  constant TsBefore (line 378) | TsBefore     = BinTestOperator(rdrIn)
  constant TsAfter (line 379) | TsAfter      = BinTestOperator(rdrOut)

FILE: syntax/typedjson/json.go
  function Encode (line 29) | func Encode(w io.Writer, node syntax.Node) error {
  type EncodeOptions (line 34) | type EncodeOptions struct
    method Encode (line 42) | func (opts EncodeOptions) Encode(w io.Writer, node syntax.Node) error {
  function encodeValue (line 56) | func encodeValue(val reflect.Value) (reflect.Value, string) {
  type exportedPos (line 171) | type exportedPos struct
  function encodePos (line 175) | func encodePos(encPtr reflect.Value, val syntax.Pos) {
  function decodePos (line 189) | func decodePos(val reflect.Value, enc map[string]any) {
  function Decode (line 197) | func Decode(r io.Reader) (syntax.Node, error) {
  type DecodeOptions (line 202) | type DecodeOptions struct
    method Decode (line 208) | func (opts DecodeOptions) Decode(r io.Reader) (syntax.Node, error) {
  function decodeValue (line 263) | func decodeValue(val reflect.Value, enc any) error {

FILE: syntax/typedjson/json_test.go
  function TestRoundtrip (line 22) | func TestRoundtrip(t *testing.T) {

FILE: syntax/walk.go
  function Walk (line 16) | func Walk(node Node, f func(Node) bool) {
  type nilableNode (line 187) | type nilableNode interface
  function walkNilable (line 192) | func walkNilable[N nilableNode](node N, f func(Node) bool) {
  function walkList (line 199) | func walkList[N Node](list []N, f func(Node) bool) {
  function walkComments (line 205) | func walkComments(list []Comment, f func(Node) bool) {
  function DebugPrint (line 214) | func DebugPrint(w io.Writer, node Node) error {
  type debugPrinter (line 221) | type debugPrinter struct
    method printf (line 227) | func (p *debugPrinter) printf(format string, args ...any) {
    method newline (line 234) | func (p *debugPrinter) newline() {
    method print (line 241) | func (p *debugPrinter) print(x reflect.Value) {

FILE: syntax/walk_test.go
  function TestWalk (line 12) | func TestWalk(t *testing.T) {
  type newNode (line 114) | type newNode struct
    method Pos (line 116) | func (newNode) Pos() Pos { return Pos{} }
    method End (line 117) | func (newNode) End() Pos { return Pos{} }
  function TestWalkUnexpectedType (line 119) | func TestWalkUnexpectedType(t *testing.T) {
Condensed preview — 107 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,187K chars).
[
  {
    "path": ".gitattributes",
    "chars": 81,
    "preview": "# To prevent CRLF breakages on Windows for fragile files, like testdata.\n* -text\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 14,
    "preview": "github: mvdan\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 8731,
    "preview": "on: [push, pull_request]\nname: Test\njobs:\n  test:\n    strategy:\n      matrix:\n        go-version: [1.25.x, 1.26.x]\n     "
  },
  {
    "path": ".gitignore",
    "chars": 99,
    "preview": "*.a\n*.zip\n\n# Don't store any of this in the master branch.\nsuppressions/\ncrashers/\ncorpus/\nvendor/\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 36164,
    "preview": "# 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- **c"
  },
  {
    "path": "LICENSE",
    "chars": 1487,
    "preview": "Copyright (c) 2016, Daniel Martí. All rights reserved.\n\nRedistribution and use in source and binary forms, with or witho"
  },
  {
    "path": "README.md",
    "chars": 7894,
    "preview": "# 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,"
  },
  {
    "path": "cmd/gosh/main.go",
    "chars": 1951,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// gosh is a proof of con"
  },
  {
    "path": "cmd/gosh/main_test.go",
    "chars": 5066,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage main\n\nimport (\n\t\""
  },
  {
    "path": "cmd/shfmt/Dockerfile",
    "chars": 393,
    "preview": "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"
  },
  {
    "path": "cmd/shfmt/docker-entrypoint.sh",
    "chars": 556,
    "preview": "#!/bin/sh\n# SPDX-License-Identifier: BSD-3-Clause\n#\n# Copyright (C) 2019 Olliver Schinagl <oliver@schinagl.nl>\n#\n# A beg"
  },
  {
    "path": "cmd/shfmt/main.go",
    "chars": 17474,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// shfmt formats shell pr"
  },
  {
    "path": "cmd/shfmt/main_test.go",
    "chars": 578,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage main\n\nimport (\n\t\""
  },
  {
    "path": "cmd/shfmt/shfmt.1.scd",
    "chars": 5304,
    "preview": "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"
  },
  {
    "path": "cmd/shfmt/testdata/script/atomic.txtar",
    "chars": 537,
    "preview": "[windows] skip 'atomic writes aren''t supported on Windows'\n[!exec:sh] skip 'sh is required to run this test'\n\n# If we d"
  },
  {
    "path": "cmd/shfmt/testdata/script/basic.txtar",
    "chars": 655,
    "preview": "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 -\n"
  },
  {
    "path": "cmd/shfmt/testdata/script/diff.txtar",
    "chars": 775,
    "preview": "stdin input.sh\n! exec shfmt -d\ncmp stdout input.sh.stdindiff\n! stderr .\n\nstdin input.sh\n! exec shfmt --diff\ncmp stdout i"
  },
  {
    "path": "cmd/shfmt/testdata/script/editorconfig.txtar",
    "chars": 5840,
    "preview": "cp input.sh input.sh.orig\n\n# Using stdin should use EditorConfig.\nstdin input.sh\nexec shfmt\ncmp stdout input.sh.golden\n!"
  },
  {
    "path": "cmd/shfmt/testdata/script/flags.txtar",
    "chars": 4311,
    "preview": "exec shfmt -h\n! stderr 'flag provided but not defined'\nstderr 'usage: shfmt'\nstderr 'Utilities' # definitely includes ou"
  },
  {
    "path": "cmd/shfmt/testdata/script/simplify.txtar",
    "chars": 384,
    "preview": "exec shfmt -s input.sh\ncmp stdout input.sh.simplify-golden\n\nexec shfmt --simplify input.sh\ncmp stdout input.sh.simplify-"
  },
  {
    "path": "cmd/shfmt/testdata/script/tojson.txtar",
    "chars": 3245,
    "preview": "stdin empty.sh\nexec shfmt -tojson # old flag name\ncmp stdout empty.sh.json\n! stderr .\n\nstdin simple.sh\nexec shfmt --to-j"
  },
  {
    "path": "cmd/shfmt/testdata/script/walk.txtar",
    "chars": 5049,
    "preview": "mkdir symlink/subdir\n# Remember that the symlink target is relative to the symlink directory.\n[symlink] symlink symlink/"
  },
  {
    "path": "expand/arith.go",
    "chars": 4837,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/braces.go",
    "chars": 2250,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/braces_test.go",
    "chars": 3725,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/doc.go",
    "chars": 179,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package expand contain"
  },
  {
    "path": "expand/environ.go",
    "chars": 9068,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/environ_test.go",
    "chars": 1869,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/expand.go",
    "chars": 31923,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/expand_nonwindows.go",
    "chars": 191,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n//go:build !windows\n\npack"
  },
  {
    "path": "expand/expand_test.go",
    "chars": 2890,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/expand_windows.go",
    "chars": 307,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/param.go",
    "chars": 9502,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage expand\n\nimport (\n"
  },
  {
    "path": "expand/valuekind_string.go",
    "chars": 768,
    "preview": "// Code generated by \"stringer -type=ValueKind\"; DO NOT EDIT.\n\npackage expand\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"inva"
  },
  {
    "path": "fileutil/file.go",
    "chars": 2767,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package fileutil allow"
  },
  {
    "path": "fileutil/file_test.go",
    "chars": 991,
    "preview": "// Copyright (c) 2025, Ville Skyttä <ville.skytta@iki.fi>\n// See LICENSE for licensing information\n\npackage fileutil\n\nim"
  },
  {
    "path": "go.mod",
    "chars": 557,
    "preview": "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."
  },
  {
    "path": "go.sum",
    "chars": 2401,
    "preview": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.24 h1:bJr"
  },
  {
    "path": "internal/pattern.go",
    "chars": 2438,
    "preview": "// Copyright (c) 2026, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage internal\n\nimport "
  },
  {
    "path": "internal/testing.go",
    "chars": 1521,
    "preview": "// Copyright (c) 2026, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage internal\n\nimport "
  },
  {
    "path": "interp/api.go",
    "chars": 28502,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package interp impleme"
  },
  {
    "path": "interp/builtin.go",
    "chars": 28093,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n"
  },
  {
    "path": "interp/example_test.go",
    "chars": 2694,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp_test\n\nimpo"
  },
  {
    "path": "interp/handler.go",
    "chars": 12808,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n"
  },
  {
    "path": "interp/handler_test.go",
    "chars": 15348,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp_test\n\nimpo"
  },
  {
    "path": "interp/interp_test.go",
    "chars": 114308,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp_test\n\nimpo"
  },
  {
    "path": "interp/os_notunix.go",
    "chars": 1517,
    "preview": "// Copyright (c) 2017, Andrey Nering <andrey.nering@gmail.com>\n// See LICENSE for licensing information\n\n//go:build !uni"
  },
  {
    "path": "interp/os_unix.go",
    "chars": 1270,
    "preview": "// Copyright (c) 2017, Andrey Nering <andrey.nering@gmail.com>\n// See LICENSE for licensing information\n\n//go:build unix"
  },
  {
    "path": "interp/runner.go",
    "chars": 27347,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n"
  },
  {
    "path": "interp/test.go",
    "chars": 5467,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n"
  },
  {
    "path": "interp/test_classic.go",
    "chars": 3960,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n"
  },
  {
    "path": "interp/trace.go",
    "chars": 2350,
    "preview": "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"
  },
  {
    "path": "interp/unexported_test.go",
    "chars": 906,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n"
  },
  {
    "path": "interp/unix_test.go",
    "chars": 3623,
    "preview": "// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n//go:build unix\n\npackage "
  },
  {
    "path": "interp/vars.go",
    "chars": 10870,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage interp\n\nimport (\n"
  },
  {
    "path": "interp/windows_test.go",
    "chars": 650,
    "preview": "// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n//go:build windows\n\npacka"
  },
  {
    "path": "moreinterp/coreutils/coreutils.go",
    "chars": 3088,
    "preview": "// Copyright (c) 2025, Andrey Nering <andrey@nering.com.br>\n// See LICENSE for licensing information\n\n// Package coreuti"
  },
  {
    "path": "moreinterp/coreutils/coreutils_test.go",
    "chars": 1083,
    "preview": "// Copyright (c) 2025, Andrey Nering <andrey@nering.com.br>\n// See LICENSE for licensing information\n\npackage coreutils\n"
  },
  {
    "path": "moreinterp/coreutils/error.go",
    "chars": 356,
    "preview": "package coreutils\n\nimport \"fmt\"\n\n// Error wraps any error returned from the core utilities.\ntype Error struct {\n\terr err"
  },
  {
    "path": "moreinterp/go.mod",
    "chars": 497,
    "preview": "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\tmvd"
  },
  {
    "path": "moreinterp/go.sum",
    "chars": 2603,
    "preview": "github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08"
  },
  {
    "path": "pattern/example_test.go",
    "chars": 896,
    "preview": "// Copyright (c) 2019, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage pattern_test\n\nimp"
  },
  {
    "path": "pattern/pattern.go",
    "chars": 11251,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package pattern allows"
  },
  {
    "path": "pattern/pattern_test.go",
    "chars": 7749,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage pattern\n\nimport ("
  },
  {
    "path": "shell/doc.go",
    "chars": 600,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package shell contains"
  },
  {
    "path": "shell/example_test.go",
    "chars": 1142,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage shell_test\n\nimpor"
  },
  {
    "path": "shell/expand.go",
    "chars": 2131,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage shell\n\nimport (\n\t"
  },
  {
    "path": "shell/expand_test.go",
    "chars": 2748,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage shell\n\nimport (\n\t"
  },
  {
    "path": "syntax/bench_test.go",
    "chars": 1193,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/braces.go",
    "chars": 4031,
    "preview": "// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/canonical.sh",
    "chars": 317,
    "preview": "#!/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   "
  },
  {
    "path": "syntax/doc.go",
    "chars": 225,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package syntax impleme"
  },
  {
    "path": "syntax/example_test.go",
    "chars": 5015,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax_test\n\nimpo"
  },
  {
    "path": "syntax/filetests_test.go",
    "chars": 122607,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/fuzz_test.go",
    "chars": 4251,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/lexer.go",
    "chars": 26314,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/nodes.go",
    "chars": 29105,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/parser.go",
    "chars": 74236,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/parser_arithm.go",
    "chars": 8707,
    "preview": "package syntax\n\n// compact specifies whether we allow spaces between expressions.\n// This is true for let\nfunc (p *Parse"
  },
  {
    "path": "syntax/parser_linux_test.go",
    "chars": 659,
    "preview": "// Copyright (c) 2025, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/parser_other_test.go",
    "chars": 247,
    "preview": "// Copyright (c) 2025, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n//go:build !linux\n\npackag"
  },
  {
    "path": "syntax/parser_test.go",
    "chars": 70225,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/printer.go",
    "chars": 38726,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/printer_test.go",
    "chars": 32879,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/quote.go",
    "chars": 5321,
    "preview": "// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/quote_test.go",
    "chars": 1346,
    "preview": "// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/simplify.go",
    "chars": 5600,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport \"s"
  },
  {
    "path": "syntax/simplify_test.go",
    "chars": 2402,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzParsePrint/293db3718a4ab7a5",
    "chars": 164,
    "preview": "go test fuzz v1\nstring(\"A=0(\")\nbyte('\\x00')\nbool(true)\nbool(false)\nbyte('\\x00')\nbool(false)\nbool(false)\nbool(false)\nbool"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzParsePrint/6d0dc226922dc40c",
    "chars": 171,
    "preview": "go test fuzz v1\nstring(\"`\\\\$\\\\\\\\000\")\nbyte('\\x02')\nbool(true)\nbool(false)\nbyte('\\x00')\nbool(false)\nbool(false)\nbool(fals"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzParsePrint/cb6d714b0a2d2315",
    "chars": 168,
    "preview": "go test fuzz v1\nstring(\"ec!!ho ${(\")\nbyte('\\x01')\nbool(true)\nbool(false)\nbyte('\\x00')\nbool(false)\nbool(false)\nbool(true)"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/23cf0175e40438e8033b11cdd1441a2d2893a99144c4ac0f2b5f4caa113c9edd",
    "chars": 46,
    "preview": "go test fuzz v1\nstring(\"\\uffff\")\nbyte('\\x02')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/25f36feab4af00bc4dfc3cf56da02b842b62ba8c5ac44862b5b3b776a0d519b4",
    "chars": 45,
    "preview": "go test fuzz v1\nstring(\"\\xb3c\")\nbyte('\\x02')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/2788bd30d386289e06a1024a030ad5ab7f363c703bea8a5d035de174491029bf",
    "chars": 45,
    "preview": "go test fuzz v1\nstring(\"\\x0fC\")\nbyte('\\x00')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/39d5fdf93d52b2cd50fb9582b27c82d159de0575623865538ced2a7780499fa6",
    "chars": 47,
    "preview": "go test fuzz v1\nstring(\"\\u05f5A\")\nbyte('\\x00')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/6fcce067200fb8ae6d4c2b1b7c1f55d3f7e4b38f4ee4f05e50e496a7c399f2d8",
    "chars": 50,
    "preview": "go test fuzz v1\nstring(\"\\U00086199\")\nbyte('\\x02')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/b26cd471412059c6ab6aa27b6153d42d2d00cbb00ad11d3cd88a192a7dfd2cdf",
    "chars": 44,
    "preview": "go test fuzz v1\nstring(\"\\xb6\")\nbyte('\\x01')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/df6b5d69da50c7d58ca13f6dde15e2a7224a53ce7bd72a02d49893e580b6775b",
    "chars": 45,
    "preview": "go test fuzz v1\nstring(\"\\x050\")\nbyte('\\x02')\n"
  },
  {
    "path": "syntax/testdata/fuzz/FuzzQuote/ea14da9b0299f4463c20659e2a51808fef8d5fb0de6324f0de64153511d4b1f8",
    "chars": 51,
    "preview": "go test fuzz v1\nstring(\"\\U000600a04\")\nbyte('\\x00')\n"
  },
  {
    "path": "syntax/token_string.go",
    "chars": 4418,
    "preview": "// Code generated by \"stringer -type token -linecomment -trimprefix _\"; DO NOT EDIT.\n\npackage syntax\n\nimport \"strconv\"\n\n"
  },
  {
    "path": "syntax/tokens.go",
    "chars": 11436,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\n//go:gene"
  },
  {
    "path": "syntax/typedjson/json.go",
    "chars": 9150,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\n// Package typedjson allo"
  },
  {
    "path": "syntax/typedjson/json_test.go",
    "chars": 2333,
    "preview": "// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage typedjson_test\n\ni"
  },
  {
    "path": "syntax/typedjson/testdata/roundtrip/file.json",
    "chars": 65091,
    "preview": "{\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\""
  },
  {
    "path": "syntax/typedjson/testdata/roundtrip/file.sh",
    "chars": 369,
    "preview": "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 ba"
  },
  {
    "path": "syntax/walk.go",
    "chars": 6274,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  },
  {
    "path": "syntax/walk_test.go",
    "chars": 3146,
    "preview": "// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>\n// See LICENSE for licensing information\n\npackage syntax\n\nimport (\n"
  }
]

About this extraction

This page contains the full source code of the mvdan/sh GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 107 files (991.3 KB), approximately 345.3k tokens, and a symbol index with 1385 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!