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
[](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
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
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[](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.