pax_global_header00006660000000000000000000000064142042511170014507gustar00rootroot0000000000000052 comment=c85a7f318ddda57126cbfb9f12ff50e28eeebfae sh-3.4.3/000077500000000000000000000000001420425111700121305ustar00rootroot00000000000000sh-3.4.3/.gitattributes000066400000000000000000000001211420425111700150150ustar00rootroot00000000000000# To prevent CRLF breakages on Windows for fragile files, like testdata. * -text sh-3.4.3/.github/000077500000000000000000000000001420425111700134705ustar00rootroot00000000000000sh-3.4.3/.github/FUNDING.yml000066400000000000000000000000161420425111700153020ustar00rootroot00000000000000github: mvdan sh-3.4.3/.github/workflows/000077500000000000000000000000001420425111700155255ustar00rootroot00000000000000sh-3.4.3/.github/workflows/test.yml000066400000000000000000000175551420425111700172440ustar00rootroot00000000000000on: [push, pull_request] name: Go jobs: test: strategy: matrix: go-version: [1.16.x, 1.17.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Test run: go test -count=1 ./... - name: Test with -short -race run: go test -short -race -count=1 ./... - name: Test with GOARCH=386 run: GOARCH=386 go test -count=1 ./... if: matrix.os == 'ubuntu-latest' - name: gofmt check run: diff <(echo -n) <(gofmt -d .) if: matrix.os == 'ubuntu-latest' test-linux-alpine: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Test as root, without cgo, and with busybox run: docker run -v="$PWD:/pwd" -w=/pwd -e=CGO_ENABLED=0 golang:1.17.1-alpine go test ./... docker: name: Build and test Docker images # Only deploy if previous stages pass. needs: [test, test-linux-alpine] runs-on: ubuntu-latest services: registry: image: registry:2 ports: - 5000:5000 # this is needed because we restart the docker daemon for experimental # support options: "--restart always" env: # Export environment variables for all stages. DOCKER_USER: ${{ secrets.DOCKER_USER }} DOCKER_DEPLOY_IMAGES: false # Pushing READMEs to Dockerhub currently only works with username/password # and not with personal access tokens (Step: Update DockerHub description) DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 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: - name: Checkout code uses: actions/checkout@v2 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 - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx (local builds) uses: docker/setup-buildx-action@v1 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@v2 with: 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@v2 with: 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_PASSWORD }}" ]] || missing+=(DOCKER_PASSWORD) 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@v1 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - 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@v2 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} repository: ${{ secrets.DOCKER_USER }}/${{ env.DOCKER_REPO }} readme-filepath: README.md sh-3.4.3/.gitignore000066400000000000000000000002131420425111700141140ustar00rootroot00000000000000*.a *.zip # Don't store any of this in the master branch. suppressions/ crashers/ corpus/ vendor/ /_js/index.js* /_js/_js.js* /_js/*.log sh-3.4.3/CHANGELOG.md000066400000000000000000000604241420425111700137470ustar00rootroot00000000000000# Changelog ## [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 `$( 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 `, 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.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 sh-3.4.3/LICENSE000066400000000000000000000027201420425111700131360ustar00rootroot00000000000000Copyright (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. sh-3.4.3/README.md000066400000000000000000000135711420425111700134160ustar00rootroot00000000000000# sh [![Go Reference](https://pkg.go.dev/badge/mvdan.cc/sh/v3.svg)](https://pkg.go.dev/mvdan.cc/sh/v3) A shell parser, formatter, and interpreter. Supports [POSIX Shell], [Bash], and [mksh]. Requires Go 1.16 or later. ### Quick start To parse shell scripts, inspect them, and print them out, see the [syntax examples](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#pkg-examples). For high-level operations like performing shell expansions on strings, see the [shell examples](https://pkg.go.dev/mvdan.cc/sh/v3/shell#pkg-examples). ### shfmt go install mvdan.cc/sh/v3/cmd/shfmt@latest `shfmt` formats shell programs. See [canonical.sh](syntax/canonical.sh) for a quick look at its default style. For example: shfmt -l -w script.sh For more information, see [its manpage](cmd/shfmt/shfmt.1.scd), which can be viewed directly as Markdown or rendered with [scdoc]. Packages are available on [Alpine], [Arch], [Docker], [FreeBSD], [Homebrew], [MacPorts], [NixOS], [Scoop], [Snapcraft], [Void] and [webi]. ### gosh go install mvdan.cc/sh/v3/cmd/gosh@latest Proof of concept shell that uses `interp`. Note that it's not meant to replace a POSIX shell at the moment, and its options are intentionally minimalistic. ### Fuzzing We use Go's native fuzzing support, which requires Go 1.18 or later. 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 1:16: not a valid arithmetic operator: string $ 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 )) ``` * Some builtins like `export` and `let` are parsed as keywords. This allows statically building their syntax tree, as opposed to keeping the arguments as a slice of words. It is also required to support `declare foo=(bar)`. Note that this means expansions like `declare {a,b}=c` are not supported. ### JavaScript A subset of the Go packages are available as an npm package called [mvdan-sh]. See the [_js](_js) directory for more information. ### Docker To build a Docker image, checkout a specific version of the repository and run: docker build -t my:tag -f cmd/shfmt/Dockerfile . This creates an image that only includes shfmt. Alternatively, if you want an image that includes alpine, add `--target alpine`. To use the Docker image, run: docker run --rm -v $PWD:/mnt -w /mnt my:tag ### pre-commit It is possible to use shfmt with [pre-commit][pre-commit] and a `local` repo configuration like: ```yaml - repo: local hooks: - id: shfmt name: shfmt minimum_pre_commit_version: 2.4.0 language: golang additional_dependencies: [mvdan.cc/sh/v3/cmd/shfmt@v3.2.2] entry: shfmt args: [-w] types: [shell] ``` ### Related projects The following editor integrations wrap `shfmt`: - [format-shell] - Atom plugin - [intellij-shellcript] - Intellij Jetbrains `shell script` plugin - [micro] - Editor with a built-in plugin - [shell-format] - VS Code plugin - [shfmt.el] - Emacs package - [Sublime-Pretty-Shell] - Sublime Text 3 plugin - [vim-shfmt] - Vim plugin Other noteworthy integrations include: - Alternative docker image by [PeterDaveHello][dockerized-peterdavehello] - [modd] - A developer tool that responds to filesystem changes - [prettier-plugin-sh] - [Prettier] plugin using [mvdan-sh] - [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 [alpine]: https://pkgs.alpinelinux.org/packages?name=shfmt [arch]: https://www.archlinux.org/packages/community/x86_64/shfmt/ [bash]: https://www.gnu.org/software/bash/ [docker]: https://hub.docker.com/r/mvdan/shfmt/ [dockerized-peterdavehello]: https://github.com/PeterDaveHello/dockerized-shfmt/ [editorconfig]: https://editorconfig.org/ [examples]: https://pkg.go.dev/mvdan.cc/sh/v3/syntax#pkg-examples [format-shell]: https://atom.io/packages/format-shell [freebsd]: https://www.freshports.org/devel/shfmt [homebrew]: https://formulae.brew.sh/formula/shfmt [intellij-shellcript]: https://www.jetbrains.com/help/idea/shell-scripts.html [macports]: https://ports.macports.org/port/shfmt/summary [mdformat-shfmt]: https://github.com/hukkinj1/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 [nixos]: https://github.com/NixOS/nixpkgs/blob/HEAD/pkgs/tools/text/shfmt/default.nix [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 [prettier-plugin-sh]: https://github.com/rx-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 [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 [vim-shfmt]: https://github.com/z0mbix/vim-shfmt [void]: https://github.com/void-linux/void-packages/blob/HEAD/srcpkgs/shfmt/template [webi]: https://webinstall.dev/shfmt/ sh-3.4.3/_js/000077500000000000000000000000001420425111700127035ustar00rootroot00000000000000sh-3.4.3/_js/LICENSE000066400000000000000000000027201420425111700137110ustar00rootroot00000000000000Copyright (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. sh-3.4.3/_js/README.md000066400000000000000000000056731420425111700141750ustar00rootroot00000000000000## mvdan-sh This package is a JavaScript version of a shell package written in Go, available at https://github.com/mvdan/sh. It is transpiled from Go to JS using a GopherJS fork, available at https://github.com/myitcv/gopherjs. ### Sample usage ``` const sh = require('mvdan-sh') const syntax = sh.syntax var parser = syntax.NewParser() var printer = syntax.NewPrinter() var src = "echo 'foo'" var f = parser.Parse(src, "src.sh") // print out the syntax tree syntax.DebugPrint(f) console.log() // replace all single quoted string values syntax.Walk(f, function(node) { if (syntax.NodeType(node) == "SglQuoted") { node.Value = "bar" } return true }) // print the code back out console.log(printer.Print(f)) // echo 'bar' ``` You can find more samples in [testmain.js](https://github.com/mvdan/sh/blob/master/_js/testmain.js). ### Available APIs The APIs listed below are wrapped to be usable in JavaScript. Follow the links to read their documentation. * [syntax.NewParser](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#NewParser) - [Parser.Parse](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#Parser.Parse) - [Parser.Interactive](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#Parser.Interactive) - [Parser.Incomplete](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#Parser.Incomplete) * [syntax.DebugPrint](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#DebugPrint) * [syntax.Walk](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#Walk) * [syntax.NewPrinter](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#NewPrinter) - [Printer.Print](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#Printer.Print) Constructor options like [syntax.KeepComments](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#KeepComments) are also available. The original `io.Reader` parameters can take a string or a [stream.Readable](https://nodejs.org/api/stream.html#stream_class_stream_readable) object. `io.Writer` parameters are replaced by string returns. The nodes you will find in the syntax tree are all equivalent to the nodes you will see on the Go API. To get the type of a node, use `syntax.NodeType` as the example above shows. Some of the most common node types include: * [syntax.File](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#File) * [syntax.Stmt](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#Stmt) * [syntax.CallExpr](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#CallExpr) * [syntax.Word](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#Word) * [syntax.Lit](https://pkg.go.dev/mvdan.cc/sh/v3/syntax#Lit) The five above will show up in your syntax tree if you parse a `echo foo` command, which you can see if you use `syntax.DebugPrint` to inspect the syntax tree. ### Building You will need: * Latest Go 1.11.x * Latest `gopherjs` from @myitcv's fork: https://github.com/myitcv/gopherjs * NodeJS, to run the `testmain.js` test suite Then, simply run `./build`. The result will be `index.js`, which isn't minified. At the time of writing, `index.js` weighs 1.7MiB in plaintext, and 220KiB when minified and gzipped. sh-3.4.3/_js/api_dump.go000066400000000000000000000135141420425111700150340ustar00rootroot00000000000000// Copyright (c) 2019, Daniel Martí // See LICENSE for licensing information //go:build ignore // +build ignore // api_dump is a simple program to create a description of what the syntax // package's API is. The main purpose is so that the JS package users can // inspect the API programmatically, to generate documentation, code, etc. // // To run with Go 1.11 or later: GO111MODULE=on go run api_dump.go package main import ( "encoding/json" "fmt" "go/ast" "go/token" "go/types" "os" "reflect" "golang.org/x/tools/go/packages" ) type Package struct { Types map[string]NamedType `json:"types"` Funcs map[string]DocType `json:"funcs"` } type NamedType struct { Doc string `json:"doc"` Type interface{} `json:"type"` EnumValues []string `json:"enumvalues,omitempty"` Implementers []string `json:"implementers,omitempty"` Methods map[string]DocType `json:"methods"` } type DocType struct { Doc string `json:"doc"` Type interface{} `json:"type"` } func main() { cfg := &packages.Config{Mode: packages.LoadSyntax} pkgs, err := packages.Load(cfg, "mvdan.cc/sh/v3/syntax") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if packages.PrintErrors(pkgs) > 0 { os.Exit(1) } if len(pkgs) != 1 { panic("expected exactly one package") } dump := &Package{ Types: map[string]NamedType{}, Funcs: map[string]DocType{}, } // from identifier position to its doc docs := make(map[token.Pos]*ast.CommentGroup) pkg := pkgs[0] for _, file := range pkg.Syntax { ast.Inspect(file, func(node ast.Node) bool { switch node := node.(type) { case *ast.FuncDecl: docs[node.Name.Pos()] = node.Doc case *ast.GenDecl: if len(node.Specs) != 1 { break } // TypeSpec.Doc is sometimes not passed on switch spec := node.Specs[0].(type) { case *ast.TypeSpec: spec.Doc = node.Doc } case *ast.TypeSpec: docs[node.Name.Pos()] = node.Doc case *ast.Field: if len(node.Names) != 1 { break } docs[node.Names[0].Pos()] = node.Doc case *ast.ValueSpec: if len(node.Names) != 1 { break } docs[node.Names[0].Pos()] = node.Doc } return true }) } scope := pkg.Types.Scope() var allImpls []*types.Pointer var allConsts []*types.Const for _, name := range scope.Names() { obj := scope.Lookup(name) if !obj.Exported() { continue } switch obj := obj.(type) { case *types.TypeName: // not interfaces if _, ok := obj.Type().(*types.Interface); !ok { // include pointer receivers too allImpls = append(allImpls, types.NewPointer(obj.Type())) } case *types.Const: allConsts = append(allConsts, obj) } } for _, name := range scope.Names() { obj := scope.Lookup(name) if !obj.Exported() { continue } if fn, ok := obj.(*types.Func); ok { dump.Funcs[fn.Name()] = DocType{ Doc: docs[fn.Pos()].Text(), Type: dumpType(docs, fn.Type()), } continue } tname, ok := obj.(*types.TypeName) if !ok { continue } name := tname.Name() named, ok := obj.Type().(*types.Named) if !ok { continue } under := named.Underlying() dumpNamed := NamedType{ Doc: docs[tname.Pos()].Text(), Type: dumpType(docs, under), Methods: map[string]DocType{}, } switch under := under.(type) { case *types.Basic: if under.Info()&types.IsInteger == 0 { break } for _, cnst := range allConsts { if cnst.Type() == named { dumpNamed.EnumValues = append(dumpNamed.EnumValues, cnst.Name()) } } case *types.Interface: for _, typ := range allImpls { if types.Implements(typ, under) { dumpNamed.Implementers = append(dumpNamed.Implementers, typ.Elem().String()) } } } for i := 0; i < named.NumMethods(); i++ { fn := named.Method(i) if !fn.Exported() { continue } dumpNamed.Methods[fn.Name()] = DocType{ Doc: docs[fn.Pos()].Text(), Type: dumpType(docs, fn.Type()), } } dump.Types[name] = dumpNamed } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", "\t") if err := enc.Encode(dump); err != nil { fmt.Fprintln(os.Stderr, err) } } func dumpType(docs map[token.Pos]*ast.CommentGroup, typ types.Type) interface{} { dump := map[string]interface{}{} switch typ := typ.(type) { case *types.Interface: dump["kind"] = "interface" methods := map[string]DocType{} for i := 0; i < typ.NumMethods(); i++ { fn := typ.Method(i) if !fn.Exported() { continue } methods[fn.Name()] = DocType{ Doc: docs[fn.Pos()].Text(), Type: dumpType(docs, fn.Type()), } } dump["methods"] = methods return dump case *types.Struct: dump["kind"] = "struct" type Field struct { Doc string `json:"doc"` Type interface{} `json:"type"` Embedded bool `json:"embedded"` } fields := map[string]Field{} for i := 0; i < typ.NumFields(); i++ { fd := typ.Field(i) if !fd.Exported() { continue } fields[fd.Name()] = Field{ Doc: docs[fd.Pos()].Text(), Type: dumpType(docs, fd.Type()), Embedded: fd.Embedded(), } } dump["fields"] = fields return dump case *types.Slice: dump["kind"] = "list" dump["elem"] = dumpType(docs, typ.Elem()) return dump case *types.Pointer: dump["kind"] = "pointer" dump["elem"] = dumpType(docs, typ.Elem()) return dump case *types.Signature: dump["kind"] = "function" dump["params"] = dumpTuple(docs, typ.Params()) dump["results"] = dumpTuple(docs, typ.Results()) return dump case *types.Basic: return typ.String() case *types.Named: return typ.String() } panic("TODO: " + reflect.TypeOf(typ).String()) } func dumpTuple(docs map[token.Pos]*ast.CommentGroup, tuple *types.Tuple) []interface{} { typs := make([]interface{}, 0) for i := 0; i < tuple.Len(); i++ { vr := tuple.At(i) typs = append(typs, map[string]interface{}{ "name": vr.Name(), "type": dumpType(docs, vr.Type()), }) } return typs } sh-3.4.3/_js/build000077500000000000000000000003631420425111700137320ustar00rootroot00000000000000#!/bin/sh if ! go version | grep -q go1.12; then echo "Go 1.12.x required to build." exit 1 fi export GO111MODULE=on # build in module-aware mode go run github.com/gopherjs/gopherjs build -o index.js || exit 1 node testmain.js || exit 1 sh-3.4.3/_js/go.mod000066400000000000000000000004021420425111700140050ustar00rootroot00000000000000module shjs go 1.11 replace github.com/gopherjs/gopherjs => github.com/myitcv/gopherjs v1.12.80 replace mvdan.cc/sh/v3 => ../ require ( github.com/gopherjs/gopherjs v0.0.0-00010101000000-000000000000 mvdan.cc/sh/v3 v3.0.0-00010101000000-000000000000 ) sh-3.4.3/_js/go.sum000066400000000000000000000070111420425111700140350ustar00rootroot00000000000000github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/myitcv/gopherjs v1.12.0 h1:4QfVBUZ+1vDcNsx/jLHwJPAfkSG3NqOG2OpOxQIYoSI= github.com/myitcv/gopherjs v1.12.0/go.mod h1:d55Q4EjGQHeJVms+9LGtXul6ykz5Xzx1E1gaXQXdimY= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/rogpeppe/go-internal v1.0.1-alpha.1 h1:NGPDe06RaK5iQFZkqFMNb986UARfoHc9FLyWI7b6+aQ= github.com/rogpeppe/go-internal v1.0.1-alpha.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/httpfs v0.0.0-20181222201310-74dc9339e414 h1:IYVb70m/qpJGjyZV2S4qbdSDnsMl+w9nsQ2iQedf1HI= github.com/shurcooL/httpfs v0.0.0-20181222201310-74dc9339e414/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/vfsgen v0.0.0-20180915214035-33ae1944be3f/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= golang.org/x/crypto v0.0.0-20180807104621-f027049dab0a h1:PulT0Y50PcfTWomfsD39bSQyVrjjWdIuJKfyR4nOCJw= golang.org/x/crypto v0.0.0-20180807104621-f027049dab0a/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w= golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180807162357-acbc56fc7007 h1:UnHxDq9ldm4vol94wlSWDF3SU4IyC8IWVWtg266CzoY= golang.org/x/sys v0.0.0-20180807162357-acbc56fc7007/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/tools v0.0.0-20190308142131-b40df0fb21c3 h1:GPRyd2anWNtiuo1j4VS6+tIW4Qu5bCWKy4sjzUf/3oI= golang.org/x/tools v0.0.0-20190308142131-b40df0fb21c3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= sh-3.4.3/_js/main.go000066400000000000000000000142121420425111700141560ustar00rootroot00000000000000package main import ( "bytes" "fmt" "io" "math" "os" "strings" "github.com/gopherjs/gopherjs/js" "mvdan.cc/sh/v3/syntax" ) func main() { var exps *js.Object if js.Module != js.Undefined { // node exps = js.Module.Get("exports") } else { // browser, `module.exports` in unavailable exps = js.MakeWrapper(map[string]interface{}{}) originalGlobalSh := js.Global.Get("sh") exps.Set("noConflict", func() *js.Object { js.Global.Set("sh", originalGlobalSh) return exps }) // make it accessible on `window.sh` js.Global.Set("sh", exps) } exps.Set("syntax", map[string]interface{}{}) stx := exps.Get("syntax") // Type helpers just for JS stx.Set("NodeType", func(v interface{}) string { if v == nil { return "nil" } node, ok := v.(syntax.Node) if !ok { panic("NodeType requires a Node argument") } typ := fmt.Sprintf("%T", node) if i := strings.LastIndexAny(typ, "*.]"); i >= 0 { typ = typ[i+1:] } return typ }) // Parser stx.Set("NewParser", func(options ...func(interface{})) *js.Object { p := syntax.NewParser() jp := js.MakeFullWrapper(&jsParser{Parser: *p}) // Apply the options after we've wrapped the parser, as // otherwise we cannot internalise the value. for _, opt := range options { opt(jp) } return jp }) stx.Set("IsIncomplete", syntax.IsIncomplete) stx.Set("KeepComments", func(enabled bool) func(interface{}) { return func(v interface{}) { syntax.KeepComments(enabled)(&v.(*jsParser).Parser) } }) stx.Set("Variant", func(l syntax.LangVariant) func(interface{}) { if math.IsNaN(float64(l)) { panic("Variant requires a LangVariant argument") } return func(v interface{}) { syntax.Variant(l)(&v.(*jsParser).Parser) } }) stx.Set("LangBash", syntax.LangBash) stx.Set("LangPOSIX", syntax.LangPOSIX) stx.Set("LangMirBSDKorn", syntax.LangMirBSDKorn) stx.Set("StopAt", func(word string) func(interface{}) { return func(v interface{}) { syntax.StopAt(word)(&v.(*jsParser).Parser) } }) // Printer stx.Set("NewPrinter", func(options ...func(interface{})) *js.Object { p := syntax.NewPrinter() jp := js.MakeFullWrapper(&jsPrinter{Printer: *p}) // Apply the options after we've wrapped the printer, as // otherwise we cannot internalise the value. for _, opt := range options { opt(jp) } return jp }) stx.Set("Indent", func(spaces uint) func(interface{}) { return func(v interface{}) { syntax.Indent(spaces)(&v.(*jsPrinter).Printer) } }) stx.Set("BinaryNextLine", func(enabled bool) func(interface{}) { return func(v interface{}) { syntax.BinaryNextLine(enabled)(&v.(*jsPrinter).Printer) } }) stx.Set("SwitchCaseIndent", func(enabled bool) func(interface{}) { return func(v interface{}) { syntax.SwitchCaseIndent(enabled)(&v.(*jsPrinter).Printer) } }) stx.Set("SpaceRedirects", func(enabled bool) func(interface{}) { return func(v interface{}) { syntax.SpaceRedirects(enabled)(&v.(*jsPrinter).Printer) } }) stx.Set("KeepPadding", func(enabled bool) func(interface{}) { return func(v interface{}) { syntax.KeepPadding(enabled)(&v.(*jsPrinter).Printer) } }) stx.Set("Minify", func(enabled bool) func(interface{}) { return func(v interface{}) { syntax.Minify(enabled)(&v.(*jsPrinter).Printer) } }) stx.Set("FunctionNextLine", func(enabled bool) func(interface{}) { return func(v interface{}) { syntax.FunctionNextLine(enabled)(&v.(*jsPrinter).Printer) } }) // Syntax utilities stx.Set("Walk", func(node syntax.Node, jsFn func(*js.Object) bool) { fn := func(node syntax.Node) bool { if node == nil { return jsFn(nil) } return jsFn(js.MakeFullWrapper(node)) } syntax.Walk(node, fn) }) stx.Set("DebugPrint", func(node syntax.Node) { syntax.DebugPrint(os.Stdout, node) }) stx.Set("SplitBraces", func(w *syntax.Word) *js.Object { syntax.SplitBraces(w) return js.MakeFullWrapper(w) }) } func throw(err error) { js.Global.Call("$throw", js.MakeFullWrapper(err)) } // streamReader is an io.Reader wrapper for Node's stream.Readable. See // https://nodejs.org/api/stream.html#stream_class_stream_readable // TODO: support https://streams.spec.whatwg.org/#rs-class too? type streamReader struct { stream *js.Object } func (r streamReader) Read(p []byte) (n int, err error) { obj := r.stream.Call("read", len(p)) if obj == nil { return 0, io.EOF } bs := []byte(obj.String()) return copy(p, bs), nil } type jsParser struct { syntax.Parser accumulated []*syntax.Stmt incomplete bytes.Buffer } func adaptReader(src *js.Object) io.Reader { if src.Get("read") != js.Undefined { return streamReader{stream: src} } return strings.NewReader(src.String()) } func (p *jsParser) Parse(src *js.Object, name string) *js.Object { f, err := p.Parser.Parse(adaptReader(src), name) if err != nil { throw(err) } return js.MakeFullWrapper(f) } func (p *jsParser) Incomplete() bool { return p.Parser.Incomplete() } func (p *jsParser) Interactive(src *js.Object, jsFn func([]*js.Object) bool) { fn := func(stmts []*syntax.Stmt) bool { objs := make([]*js.Object, len(stmts)) for i, stmt := range stmts { objs[i] = js.MakeFullWrapper(stmt) } return jsFn(objs) } err := p.Parser.Interactive(adaptReader(src), fn) if err != nil { throw(err) } } func (p *jsParser) InteractiveStep(line string) []*syntax.Stmt { // pick up previous chunks of the incomplete statement r := strings.NewReader(p.incomplete.String() + line) lastEnd := uint(0) err := p.Parser.Interactive(r, func(stmts []*syntax.Stmt) bool { if len(stmts) > 0 { // don't re-parse finished statements lastEnd = stmts[len(stmts)-1].End().Offset() } p.accumulated = append(p.accumulated, stmts...) return false }) if syntax.IsIncomplete(err) { // starting or continuing an incomplete statement p.incomplete.WriteString(line[lastEnd:]) return p.accumulated } // complete; empty both fields and return p.incomplete.Reset() if err != nil { throw(err) } acc := p.accumulated p.accumulated = p.accumulated[:0] return acc } type jsPrinter struct { syntax.Printer } func (p jsPrinter) Print(file *syntax.File) string { var buf bytes.Buffer if err := p.Printer.Print(&buf, file); err != nil { throw(err) } return buf.String() } sh-3.4.3/_js/package.json000066400000000000000000000006171420425111700151750ustar00rootroot00000000000000{ "name": "mvdan-sh", "version": "0.5.0", "description": "A shell parser and formatter (POSIX/Bash/mksh)", "main": "index.js", "repository": "https://github.com/mvdan/sh", "author": "Daniel Martí", "license": "BSD-3-Clause", "files": [ "README.md", "LICENSE", "index.js" ], "keywords": [ "shell", "bash", "sh", "parser", "ast", "syntax", "posix", "bash-parser" ] } sh-3.4.3/_js/testbrowser.html000066400000000000000000000015601420425111700161560ustar00rootroot00000000000000 mvdan-sh sh-3.4.3/_js/testmain.js000066400000000000000000000127601420425111700150730ustar00rootroot00000000000000const assert = require('assert').strict const stream = require('stream') const sh = require('./index') const syntax = sh.syntax const parser = syntax.NewParser() const printer = syntax.NewPrinter() { // parsing a simple program const src = "echo 'foo'" var f = parser.Parse(src, "src") var stmts = f.Stmts assert.equal(f.Stmts.length, 1) var args = f.Stmts[0].Cmd.Args assert.equal(args.length, 2) assert.equal(args[0].Parts.length, 1) assert.equal(args[0].Parts[0].Value, "echo") } { // accessing fields or methods creates separate objects const src = "echo 'foo'" var f = parser.Parse(src, "src") assert.equal(f.Stmts == f.Stmts, false) assert.equal(f.Stmts === f.Stmts, false) var stmtsObj = f.Stmts assert.equal(stmtsObj == stmtsObj, true) assert.equal(stmtsObj === stmtsObj, true) } { // fatal parse error const src = "echo )" try { parser.Parse(src, "src") assert.fail("did not error") } catch (err) { assert.equal(err.Filename, "src") assert.equal(err.Pos.Line(), 1) assert.equal(syntax.IsIncomplete(err), false) } } { // incomplete parse error const src = "echo ${" try { parser.Parse(src, "src") assert.fail("did not error") } catch (err) { assert.equal(syntax.IsIncomplete(err), true) } } { // js error from the wrapper layer var foo = {} try { syntax.NodeType(foo) assert.fail("did not error") } catch (err) { assert.equal(err.message.includes("requires a Node argument"), true) } } { // node types, operators, and positions const src = "foo || bar" var f = parser.Parse(src, "src") var cmd = f.Stmts[0].Cmd assert.equal(syntax.NodeType(cmd), "BinaryCmd") // TODO: see https://github.com/myitcv/gopherjs/issues/26 // assert.equal(syntax.String(cmd.Op), "||") assert.equal(cmd.Pos().String(), "1:1") assert.equal(cmd.OpPos.String(), "1:5") assert.equal(cmd.OpPos.Line(), 1) assert.equal(cmd.OpPos.Col(), 5) assert.equal(cmd.OpPos.Offset(), 4) } { // running Walk const src = "foo bar" var f = parser.Parse(src, "src") var nilCount = 0 var nonNilCount = 0 var seenBar = false var seenCall = false syntax.Walk(f, function(node) { var typ = syntax.NodeType(node) if (node == null) { nilCount++ assert.equal(typ, "nil") } else { nonNilCount++ if (node.Value == "bar") { seenBar = true } assert.notEqual(typ, "nil") } if (typ == "CallExpr") { seenCall = true } return true }) assert.equal(nonNilCount, 7) assert.equal(nilCount, 7) assert.equal(seenBar, true) assert.equal(seenCall, true) } { // printing const src = "echo 'foo'" var f = parser.Parse(src, "src") var out = printer.Print(f) assert.equal(out, "echo 'foo'\n") } { // parser options const parser = syntax.NewParser( syntax.KeepComments(true), syntax.Variant(syntax.LangMirBSDKorn), syntax.StopAt("$$") ) const src = "echo ${|stmts;} # bar\n$$" var f = parser.Parse(src, "src") var out = printer.Print(f) assert.equal(out, "echo ${|stmts;} # bar\n") } { // parsing a readable stream const src = new stream.Readable src.push("echo foo") src.push(null) var f = parser.Parse(src, "src") var cmd = f.Stmts[0].Cmd assert.equal(cmd.Args.length, 2) } { // using the parser interactively const lines = [ "foo\n", "bar; baz\n", "\n", "foo; 'incom\n", " \n", "plete'\n", ] const wantCallbacks = [ {"count": 1, "incomplete": false}, {"count": 2, "incomplete": false}, {"count": 0, "incomplete": false}, {"count": 1, "incomplete": true}, {"count": 1, "incomplete": true}, {"count": 2, "incomplete": false}, ] var gotCallbacks = [] const src = {"read": function(size) { if (lines.length == 0) { if (gotCallbacks.length == 0) { throw "did not see any callbacks before EOF" } return null // EOF } s = lines[0] lines.shift() return s }} parser.Interactive(src, function(stmts) { for (var i in stmts) { var stmt = stmts[i] assert.equal(syntax.NodeType(stmt), "Stmt") } gotCallbacks.push({ "count": stmts.length, "incomplete": parser.Incomplete(), }) return true }) assert.deepEqual(gotCallbacks, wantCallbacks) } { // using the parser interactively with steps const lines = [ "foo\n", "bar; baz\n", "\n", "foo; 'incom\n", " \n", "plete'\n", ] const wantResults = [ {"count": 1, "incomplete": false}, {"count": 2, "incomplete": false}, {"count": 0, "incomplete": false}, {"count": 1, "incomplete": true}, {"count": 1, "incomplete": true}, {"count": 2, "incomplete": false}, ] var gotResults = [] for (var i = 0; i < lines.length; i++) { var line = lines[i] var want = wantResults[i] var stmts = parser.InteractiveStep(line) gotResults.push({ "count": stmts.length, "incomplete": parser.Incomplete(), }) } assert.deepEqual(gotResults, wantResults) } { // splitting brace expressions const parser = syntax.NewParser() const src = "{foo,bar}" var f = parser.Parse(src, "src") var word = f.Stmts[0].Cmd.Args[0] assert.equal(word.Parts.length, 1) assert.equal(syntax.NodeType(word.Parts[0]), "Lit") word = syntax.SplitBraces(word) // TODO: get rid of the empty lit assert.equal(word.Parts.length, 2) assert.equal(syntax.NodeType(word.Parts[0]), "BraceExp") assert.equal(word.Parts[0].Elems.length, 2) assert.equal(syntax.NodeType(word.Parts[1]), "Lit") assert.equal(word.Parts[1].Value, "") } { const printer = syntax.NewPrinter( syntax.Indent(2), syntax.BinaryNextLine(true) ) const f = parser.Parse("RUN yarn install && \\\n yarn build") const out = printer.Print(f) assert.equal(out, "RUN yarn install \\\n && yarn build\n") } sh-3.4.3/cmd/000077500000000000000000000000001420425111700126735ustar00rootroot00000000000000sh-3.4.3/cmd/gosh/000077500000000000000000000000001420425111700136335ustar00rootroot00000000000000sh-3.4.3/cmd/gosh/main.go000066400000000000000000000035301420425111700151070ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package main import ( "context" "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() if e, ok := interp.IsExitStatus(err); ok { os.Exit(int(e)) } if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func runAll() error { r, err := interp.New(interp.StdIO(os.Stdin, os.Stdout, os.Stderr)) if err != nil { return err } if *command != "" { return run(r, strings.NewReader(*command), "") } if flag.NArg() == 0 { if term.IsTerminal(int(os.Stdin.Fd())) { return runInteractive(r, os.Stdin, os.Stdout, os.Stderr) } return run(r, os.Stdin, "") } for _, path := range flag.Args() { if err := runPath(r, path); err != nil { return err } } return nil } func run(r *interp.Runner, reader io.Reader, name string) error { prog, err := syntax.NewParser().Parse(reader, name) if err != nil { return err } r.Reset() ctx := context.Background() return r.Run(ctx, prog) } func runPath(r *interp.Runner, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() return run(r, f, path) } func runInteractive(r *interp.Runner, stdin io.Reader, stdout, stderr io.Writer) error { parser := syntax.NewParser() fmt.Fprintf(stdout, "$ ") var runErr error fn := func(stmts []*syntax.Stmt) bool { if parser.Incomplete() { fmt.Fprintf(stdout, "> ") return true } ctx := context.Background() for _, stmt := range stmts { runErr = r.Run(ctx, stmt) if r.Exited() { return false } } fmt.Fprintf(stdout, "$ ") return true } if err := parser.Interactive(stdin, fn); err != nil { return err } return runErr } sh-3.4.3/cmd/gosh/main_test.go000066400000000000000000000101511420425111700161430ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package main import ( "fmt" "io" "testing" "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 foo; exit 0; echo bar\n", "foo\n", "echo baz\n", "", }, }, { pairs: []string{ "echo foo; exit 1; echo bar\n", "foo\n", "echo baz\n", "", }, wantErr: "exit status 1", }, { pairs: []string{ "(\n", "> ", }, wantErr: "1:1: reached EOF without matching ( with )", }, } func TestInteractive(t *testing.T) { t.Parallel() for _, tc := range interactiveTests { t.Run("", func(t *testing.T) { inReader, inWriter := io.Pipe() outReader, outWriter := io.Pipe() runner, _ := interp.New(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) }() if err := readString(outReader, "$ "); err != nil { t.Fatal(err) } line := 1 for len(tc.pairs) > 0 { if _, err := io.WriteString(inWriter, tc.pairs[0]); err != nil { t.Fatal(err) } 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 := io.Pipe() defer inReader.Close() go io.WriteString(inWriter, "exit\n") w := io.Discard runner, _ := interp.New(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 } sh-3.4.3/cmd/shfmt/000077500000000000000000000000001420425111700140145ustar00rootroot00000000000000sh-3.4.3/cmd/shfmt/Dockerfile000066400000000000000000000007011420425111700160040ustar00rootroot00000000000000FROM golang:1.17.1-alpine AS build WORKDIR /src RUN apk add --no-cache git COPY . . RUN CGO_ENABLED=0 go build -ldflags "-w -s -extldflags '-static' -X main.version=$(git describe --always --dirty --tags)" ./cmd/shfmt FROM alpine:3.14.2 AS alpine COPY --from=build /src/shfmt /bin/shfmt COPY "./cmd/shfmt/docker-entrypoint.sh" "/init" ENTRYPOINT ["/init"] FROM scratch COPY --from=build /src/shfmt /bin/shfmt ENTRYPOINT ["/bin/shfmt"] CMD ["-h"] sh-3.4.3/cmd/shfmt/docker-entrypoint.sh000077500000000000000000000010541420425111700200330ustar00rootroot00000000000000#!/bin/sh # SPDX-License-Identifier: BSD-3-Clause # # Copyright (C) 2019 Olliver Schinagl # # 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 sh-3.4.3/cmd/shfmt/json.go000066400000000000000000000034311420425111700153150ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package main import ( "encoding/json" "go/ast" "io" "reflect" "mvdan.cc/sh/v3/syntax" ) func writeJSON(w io.Writer, node syntax.Node, pretty bool) error { val := reflect.ValueOf(node) v, _ := encode(val) enc := json.NewEncoder(w) if pretty { enc.SetIndent("", "\t") } return enc.Encode(v) } func encode(val reflect.Value) (interface{}, string) { switch val.Kind() { case reflect.Ptr: elem := val.Elem() if !elem.IsValid() { return nil, "" } return encode(elem) case reflect.Interface: if val.IsNil() { return nil, "" } v, tname := encode(val.Elem()) m := v.(map[string]interface{}) m["Type"] = tname return m, "" case reflect.Struct: m := make(map[string]interface{}, val.NumField()+1) typ := val.Type() for i := 0; i < val.NumField(); i++ { ftyp := typ.Field(i) if ftyp.Type.Name() == "Pos" { continue } if !ast.IsExported(ftyp.Name) { continue } fval := val.Field(i) v, _ := encode(fval) m[ftyp.Name] = v } // Pos methods are defined on struct pointer receivers. for _, name := range [...]string{"Pos", "End"} { if fn := val.Addr().MethodByName(name); fn.IsValid() { m[name] = translatePos(fn.Call(nil)[0]) } } return m, typ.Name() case reflect.Slice: l := make([]interface{}, val.Len()) for i := 0; i < val.Len(); i++ { elem := val.Index(i) l[i], _ = encode(elem) } return l, "" default: return val.Interface(), "" } } func translatePos(val reflect.Value) map[string]interface{} { return map[string]interface{}{ "Offset": val.MethodByName("Offset").Call(nil)[0].Uint(), "Line": val.MethodByName("Line").Call(nil)[0].Uint(), "Col": val.MethodByName("Col").Call(nil)[0].Uint(), } } sh-3.4.3/cmd/shfmt/main.go000066400000000000000000000225431420425111700152750ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package main import ( "bytes" "flag" "fmt" "io" "io/fs" "os" "path/filepath" "regexp" "runtime/debug" maybeio "github.com/google/renameio/maybe" "github.com/pkg/diff" diffwrite "github.com/pkg/diff/write" "golang.org/x/term" "mvdan.cc/editorconfig" "mvdan.cc/sh/v3/fileutil" "mvdan.cc/sh/v3/syntax" ) const unsetLang = syntax.LangVariant(-1) var ( showVersion = flag.Bool("version", false, "") list = flag.Bool("l", false, "") write = flag.Bool("w", false, "") simple = flag.Bool("s", false, "") minify = flag.Bool("mn", false, "") find = flag.Bool("f", false, "") diffOut = flag.Bool("d", false, "") // useEditorConfig will be false if any parser or printer flags were used. useEditorConfig = true lang = unsetLang posix = flag.Bool("p", false, "") filename = flag.String("filename", "", "") indent = flag.Uint("i", 0, "") binNext = flag.Bool("bn", false, "") caseIndent = flag.Bool("ci", false, "") spaceRedirs = flag.Bool("sr", false, "") keepPadding = flag.Bool("kp", false, "") funcNext = flag.Bool("fn", false, "") toJSON = flag.Bool("tojson", false, "") parser *syntax.Parser printer *syntax.Printer readBuf, writeBuf bytes.Buffer copyBuf = make([]byte, 32*1024) in io.Reader = os.Stdin out io.Writer = os.Stdout color bool version = "(devel)" // to match the default from runtime/debug ) func init() { flag.Var(&lang, "ln", "") } func main() { os.Exit(main1()) } func main1() int { 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 list files whose formatting differs from shfmt's -w write result to file instead of stdout -d error with a diff when the formatting differs -s simplify the code -mn minify the code to reduce its size (implies -s) Parser options: -ln str language variant to parse (bash/posix/mksh/bats, default "bash") -p shorthand for -ln=posix -filename str provide a name for the standard input file Printer options: -i uint indent: 0 for tabs (default), >0 for number of spaces -bn binary ops like && and | may start a line -ci switch cases will be indented -sr redirect operators will be followed by a space -kp keep column alignment paddings -fn function opening braces are placed on a separate line Utilities: -f recursively find all shell files and print the paths -tojson print syntax tree to stdout as a typed JSON For more information, see 'man shfmt' and https://github.com/mvdan/sh. `) } flag.Parse() if *showVersion { // don't overwrite the version if it was set by -ldflags=-X if info, ok := debug.ReadBuildInfo(); ok && version == "(devel)" { mod := &info.Main if mod.Replace != nil { mod = mod.Replace } version = mod.Version } fmt.Println(version) return 0 } if *posix && lang != unsetLang { fmt.Fprintf(os.Stderr, "-p and -ln=lang cannot coexist\n") return 1 } if *minify { *simple = true } if os.Getenv("SHFMT_NO_EDITORCONFIG") == "true" { useEditorConfig = false } flag.Visit(func(f *flag.Flag) { switch f.Name { case "ln", "p", "i", "bn", "ci", "sr", "kp", "fn": useEditorConfig = false } }) parser = syntax.NewParser(syntax.KeepComments(true)) printer = syntax.NewPrinter(syntax.Minify(*minify)) if !useEditorConfig { if *posix { // -p equals -ln=posix lang = syntax.LangPOSIX } else if lang == unsetLang { // if unset, default to -ln=bash lang = syntax.LangBash } syntax.Variant(lang)(parser) syntax.Indent(*indent)(printer) syntax.BinaryNextLine(*binNext)(printer) syntax.SwitchCaseIndent(*caseIndent)(printer) syntax.SpaceRedirects(*spaceRedirs)(printer) syntax.KeepPadding(*keepPadding)(printer) syntax.FunctionNextLine(*funcNext)(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 f, ok := out.(*os.File); ok && term.IsTerminal(int(f.Fd())) { color = true } if flag.NArg() == 0 || (flag.NArg() == 1 && flag.Arg(0) == "-") { name := "" if *filename != "" { name = *filename } if err := formatStdin(name); err != nil { if err != errChangedWithDiff { fmt.Fprintln(os.Stderr, err) } return 1 } return 0 } if *filename != "" { fmt.Fprintln(os.Stderr, "-filename can only be used with stdin") return 1 } if *toJSON { fmt.Fprintln(os.Stderr, "-tojson can only be used with stdin") return 1 } status := 0 for _, path := range flag.Args() { if info, err := os.Stat(path); err == nil && !info.IsDir() && !*find { // When given paths to files directly, always format // them, no matter their extension or shebang. // // The only exception is the -f flag; in that case, we // do want to report whether the file is a shell script. if err := formatPath(path, false); err != nil { if err != errChangedWithDiff { fmt.Fprintln(os.Stderr, err) } status = 1 } continue } if err := filepath.WalkDir(path, func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } switch err := walkPath(path, entry); err { case nil: case filepath.SkipDir: return err case errChangedWithDiff: status = 1 default: fmt.Fprintln(os.Stderr, err) status = 1 } return nil }); err != nil { fmt.Fprintln(os.Stderr, err) status = 1 } } return status } var errChangedWithDiff = fmt.Errorf("") func formatStdin(name string) error { if *write { return fmt.Errorf("-w cannot be used on standard input") } src, err := io.ReadAll(in) if err != nil { return err } return formatBytes(src, name) } 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 } if useEditorConfig { props, err := ecQuery.Find(path) 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(props editorconfig.Section) { lang := syntax.LangBash 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) 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) syntax.FunctionNextLine(props.Get("function_next_line") == "true")(printer) } func formatPath(path string, checkShebang bool) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() readBuf.Reset() if checkShebang { n, err := io.ReadAtLeast(f, copyBuf[:32], len("#/bin/sh\n")) if err == io.EOF || err == io.ErrUnexpectedEOF { return nil // too short to have a shebang } if err != nil { return err // some other read error } if !fileutil.HasShebang(copyBuf[:n]) { return nil } readBuf.Write(copyBuf[:n]) } if *find { fmt.Fprintln(out, path) return nil } if _, err := io.CopyBuffer(&readBuf, f, copyBuf); err != nil { return err } f.Close() return formatBytes(readBuf.Bytes(), path) } func formatBytes(src []byte, path string) error { if useEditorConfig { props, err := ecQuery.Find(path) if err != nil { return err } propsOptions(props) } prog, err := parser.Parse(bytes.NewReader(src), path) if err != nil { return err } if *simple { syntax.Simplify(prog) } if *toJSON { // must be standard input; fine to return return writeJSON(out, prog, true) } writeBuf.Reset() printer.Print(&writeBuf, prog) res := writeBuf.Bytes() if !bytes.Equal(src, res) { if *list { if _, err := fmt.Fprintln(out, path); err != nil { return err } } if *write { info, err := os.Lstat(path) if err != nil { return err } perm := info.Mode().Perm() // TODO: support atomic writes on Windows? if err := maybeio.WriteFile(path, res, perm); err != nil { return err } } if *diffOut { opts := []diffwrite.Option{} if color { opts = append(opts, diffwrite.TerminalColor()) } if err := diff.Text(path+".orig", path, src, res, out, opts...); err != nil { return fmt.Errorf("computing diff: %s", err) } return errChangedWithDiff } } if !*list && !*write && !*diffOut { if _, err := out.Write(res); err != nil { return err } } return nil } sh-3.4.3/cmd/shfmt/main_test.go000066400000000000000000000010631420425111700163260ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package main import ( "flag" "os" "path/filepath" "testing" "github.com/rogpeppe/go-internal/testscript" ) func TestMain(m *testing.M) { os.Exit(testscript.RunMain(m, map[string]func() int{ "shfmt": main1, })) } var update = flag.Bool("u", false, "update testscript output files") func TestScripts(t *testing.T) { t.Parallel() testscript.Run(t, testscript.Params{ Dir: filepath.Join("testdata", "scripts"), UpdateScripts: *update, }) } sh-3.4.3/cmd/shfmt/shfmt.1.scd000066400000000000000000000063031420425111700157710ustar00rootroot00000000000000shfmt(1) # 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 files 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* List files whose formatting differs from shfmt's. *-w* Write result to file instead of stdout. *-d* 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*. *-s* Simplify the code. *-mn* Minify the code to reduce its size (implies *-s*). ## Parser flags *-ln* Language variant to parse (*bash*/*posix*/*mksh*/*bats*, default *bash*). *-p* Shorthand for *-ln=posix*. *-filename* str Provide a name for the standard input file. ## Printer flags *-i* Indent: *0* for tabs (default), *>0* for number of spaces. *-bn* Binary ops like *&&* and *|* may start a line. *-ci* Switch cases will be indented. *-sr* Redirect operators will be followed by a space. *-kp* Keep column alignment paddings. *-fn* Function opening braces are placed on a separate line. ## Utility flags *-f* Recursively find all shell files and print the paths. *-tojson* Print syntax tree to stdout 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 : shfmt -i 2 -ci -bn Below is a sample EditorConfig file as defined by , showing how to set any option: ``` [*.sh] # like -i=4 indent_style = space indent_size = 4 shell_variant = posix # like -ln=posix binary_next_line = true # like -bn switch_case_indent = true # like -ci space_redirects = true # like -sr keep_padding = true # like -kp function_next_line = true # like -fn # Ignore the entire "third_party" directory. [third_party/**] ignore = true ``` 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í , who is assisted by other open source contributors. For more information and development, see . sh-3.4.3/cmd/shfmt/testdata/000077500000000000000000000000001420425111700156255ustar00rootroot00000000000000sh-3.4.3/cmd/shfmt/testdata/scripts/000077500000000000000000000000001420425111700173145ustar00rootroot00000000000000sh-3.4.3/cmd/shfmt/testdata/scripts/atomic.txt000066400000000000000000000010311420425111700213240ustar00rootroot00000000000000[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 sh-3.4.3/cmd/shfmt/testdata/scripts/basic.txt000066400000000000000000000007471420425111700211460ustar00rootroot00000000000000cp input.sh input.sh.orig stdin input.sh shfmt cmp stdout input.sh.golden ! stderr . stdin input.sh shfmt - cmp stdout input.sh.golden ! stderr . shfmt input.sh cmp stdout input.sh.golden ! stderr . shfmt -l input.sh stdout 'input\.sh' ! stdout foo ! stderr . cmp input.sh input.sh.orig shfmt -l input.sh input.sh stdout -count=2 'input.sh' shfmt -l -w input.sh stdout 'input\.sh' ! stdout foo ! stderr . cmp input.sh input.sh.golden -- input.sh -- foo -- input.sh.golden -- foo sh-3.4.3/cmd/shfmt/testdata/scripts/diff.txt000066400000000000000000000011311420425111700207610ustar00rootroot00000000000000stdin input.sh ! shfmt -d cmp stdout input.sh.stdindiff ! stderr . ! shfmt -d input.sh cmp stdout input.sh.filediff ! stderr . ! shfmt -d input.sh input.sh stdout -count=2 'input.sh.orig' env FORCE_COLOR=true stdin input.sh ! shfmt -d stdout '\x1b\[31m- foo' ! stderr . env FORCE_COLOR= ! shfmt -d . cmp stdout input.sh.filediff ! stderr . -- input.sh -- foo bar -- input.sh.golden -- foo bar -- input.sh.stdindiff -- --- .orig +++ @@ -1,4 +1,3 @@ - foo +foo - bar -- input.sh.filediff -- --- input.sh.orig +++ input.sh @@ -1,4 +1,3 @@ - foo +foo - bar sh-3.4.3/cmd/shfmt/testdata/scripts/editorconfig.txt000066400000000000000000000050741420425111700225370ustar00rootroot00000000000000cp input.sh input.sh.orig # Using stdin should use EditorConfig. stdin input.sh shfmt cmp stdout input.sh.golden ! stderr . # Verify that -filename works well with EditorConfig. stdin stdin-filename-bash shfmt stdin stdin-filename-bash ! shfmt -filename=foo-posix.sh stderr '^foo-posix.sh:.* arrays are a bash' # Using a file path should use EditorConfig, including with the use of flags # like -l. shfmt input.sh cmp stdout input.sh.golden ! stderr . shfmt -l input.sh stdout 'input\.sh' ! stderr . # Using any formatting option should skip all EditorConfig usage. shfmt -p input.sh cmp stdout input.sh.orig ! stderr . shfmt -l -p input.sh ! stdout . ! stderr . shfmt -sr input.sh cmp stdout input.sh.orig ! stderr . # Check that EditorConfig files merge properly. shfmt morespaces/input.sh cmp stdout morespaces/input.sh.golden ! stderr . # Check a folder with all other knobs. shfmt -l otherknobs ! stdout . ! stderr . # Ignore directories when walking, if they match ignore=true. shfmt -l ignored stdout 'regular\.sh' ! stdout 'ignored\.sh' ! 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 [shell_variant_posix.sh] shell_variant = posix [shell_variant_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 -- otherknobs/shell_variant_posix.sh -- let badsyntax+ -- otherknobs/shell_variant_mksh.sh -- coproess |& -- 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 } -- ignored/.editorconfig -- root = true [third_party/**] ignore = true [1_lone_ignored.sh] ignore = true [2_dir_ignored] ignore = true -- ignored/third_party/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 sh-3.4.3/cmd/shfmt/testdata/scripts/flags.txt000066400000000000000000000025061420425111700211540ustar00rootroot00000000000000# https://github.com/rogpeppe/go-internal/issues/93 # ! 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 shfmt -version stdout 'devel|v3' ! stderr . ! shfmt -ln=bash -p stderr 'cannot coexist' ! shfmt -ln=bad stderr 'unknown shell language' ! shfmt -tojson file stderr '-tojson can only be used with stdin' ! shfmt -filename=foo file stderr '-filename can only be used with stdin' # Check all the -ln variations. stdin input-posix ! shfmt stdin input-posix shfmt -ln=posix stdout 'let' stdin input-posix shfmt -p stdout 'let' stdin input-posix ! shfmt -ln=mksh stdin input-posix ! shfmt -ln=bash stdin input-mksh shfmt -ln=mksh stdout 'coprocess' stdin input-mksh ! shfmt # Ensure that the default "bash" language works with and without flags. stdin input-bash shfmt stdout loop stdin input-bash shfmt -i 4 stdout loop # TODO: introduce stdin lang detection #stdin -filename=input.mksh input-mksh #shfmt #stdout 'coprocess' # #stdin input-mksh-shebang #shfmt #stdout 'coprocess' -- input-posix -- let a+ -- input-bash -- for ((;;)); do loop; done -- input-mksh -- coprocess |& -- input-mksh-shebang -- #!/bin/mksh coprocess |& sh-3.4.3/cmd/shfmt/testdata/scripts/tojson.txt000066400000000000000000000057241420425111700214010ustar00rootroot00000000000000stdin empty.sh shfmt -tojson cmp stdout empty.sh.json ! stderr . stdin simple.sh shfmt -tojson cmp stdout simple.sh.json stdin arithmetic.sh shfmt -tojson cmp stdout arithmetic.sh.json stdin comment.sh shfmt -tojson cmp stdout comment.sh.json -- empty.sh -- -- empty.sh.json -- { "End": { "Col": 0, "Line": 0, "Offset": 0 }, "Last": [], "Name": "\u003cstandard input\u003e", "Pos": { "Col": 0, "Line": 0, "Offset": 0 }, "Stmts": [] } -- simple.sh -- foo -- simple.sh.json -- { "End": { "Col": 4, "Line": 1, "Offset": 3 }, "Last": [], "Name": "\u003cstandard input\u003e", "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Stmts": [ { "Background": false, "Cmd": { "Args": [ { "End": { "Col": 4, "Line": 1, "Offset": 3 }, "Parts": [ { "End": { "Col": 4, "Line": 1, "Offset": 3 }, "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Type": "Lit", "Value": "foo" } ], "Pos": { "Col": 1, "Line": 1, "Offset": 0 } } ], "Assigns": [], "End": { "Col": 4, "Line": 1, "Offset": 3 }, "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Type": "CallExpr" }, "Comments": [], "Coprocess": false, "End": { "Col": 4, "Line": 1, "Offset": 3 }, "Negated": false, "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Redirs": [] } ] } -- arithmetic.sh -- ((2)) -- arithmetic.sh.json -- { "End": { "Col": 6, "Line": 1, "Offset": 5 }, "Last": [], "Name": "\u003cstandard input\u003e", "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Stmts": [ { "Background": false, "Cmd": { "End": { "Col": 6, "Line": 1, "Offset": 5 }, "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Type": "ArithmCmd", "Unsigned": false, "X": { "End": { "Col": 4, "Line": 1, "Offset": 3 }, "Parts": [ { "End": { "Col": 4, "Line": 1, "Offset": 3 }, "Pos": { "Col": 3, "Line": 1, "Offset": 2 }, "Type": "Lit", "Value": "2" } ], "Pos": { "Col": 3, "Line": 1, "Offset": 2 }, "Type": "Word" } }, "Comments": [], "Coprocess": false, "End": { "Col": 6, "Line": 1, "Offset": 5 }, "Negated": false, "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Redirs": [] } ] } -- comment.sh -- # -- comment.sh.json -- { "End": { "Col": 2, "Line": 1, "Offset": 1 }, "Last": [ { "End": { "Col": 2, "Line": 1, "Offset": 1 }, "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Text": "" } ], "Name": "\u003cstandard input\u003e", "Pos": { "Col": 1, "Line": 1, "Offset": 0 }, "Stmts": [] } sh-3.4.3/cmd/shfmt/testdata/scripts/walk.txt000066400000000000000000000052451420425111700210210ustar00rootroot00000000000000mkdir symlink/reallylongdir [symlink] symlink symlink/reallylongdir/symlink-file -> modify/ext-shebang.sh [symlink] symlink symlink/symlink-dir -> symlink/reallylongdir [symlink] symlink symlink/symlink-none -> symlink/reallylongdir/nonexistent shfmt -f . cmpenv stdout find.golden ! stderr . # try to format missing paths ! shfmt nonexistent stderr -count=1 nonexistent ! 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 ! shfmt . stdout 'foo' stdout 'bin/env' stderr -count=1 'parse-error\.sh' # format an entire directory with -l and -w ! shfmt -l -w . cmpenv stdout modify.golden stderr -count=1 'parse-error\.sh' # parse-error again, but now as a lone file ! shfmt error/parse-error.sh stderr -count=1 'parse-error\.sh' # format files directly which we would ignore when walking directories shfmt none/ext-shebang.other stdout 'foo' shfmt none/noext-noshebang stdout 'foo' # -f on files should still check extension and shebang shfmt -f modify/ext.sh modify/shebang-1 none/ext-shebang.other none/noext-noshebang stdout -count=2 '^modify' ! stdout '^none' -- find.golden -- error${/}parse-error.sh modify${/}dir${/}ext.sh modify${/}ext-shebang.sh modify${/}ext.bash modify${/}ext.sh modify${/}shebang-1 modify${/}shebang-2 modify${/}shebang-3 modify${/}shebang-4 modify${/}shebang-5 modify${/}shebang-args modify${/}shebang-space modify${/}shebang-tabs -- modify.golden -- modify${/}dir${/}ext.sh modify${/}ext-shebang.sh modify${/}ext.bash modify${/}ext.sh modify${/}shebang-1 modify${/}shebang-2 modify${/}shebang-3 modify${/}shebang-4 modify${/}shebang-5 modify${/}shebang-args modify${/}shebang-space modify${/}shebang-tabs -- modify/shebang-1 -- #!/bin/sh foo -- modify/shebang-2 -- #!/bin/bash foo -- modify/shebang-3 -- #!/usr/bin/sh foo -- modify/shebang-4 -- #!/usr/bin/env bash foo -- modify/shebang-5 -- #!/bin/env sh foo -- 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 -- 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( sh-3.4.3/expand/000077500000000000000000000000001420425111700134075ustar00rootroot00000000000000sh-3.4.3/expand/arith.go000066400000000000000000000076701420425111700150570ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package expand import ( "fmt" "strconv" "mvdan.cc/sh/v3/syntax" ) func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) { switch x := expr.(type) { case *syntax.Word: str, err := Literal(cfg, x) 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 atoi(str), nil case *syntax.ParenArithm: return Arithm(cfg, x.X) case *syntax.UnaryArithm: switch x.Op { case syntax.Inc, syntax.Dec: name := x.X.(*syntax.Word).Lit() old := atoi(cfg.envGet(name)) val := old if x.Op == syntax.Inc { val++ } else { val-- } if err := cfg.envSet(name, strconv.Itoa(val)); err != nil { return 0, err } if x.Post { return old, nil } return val, nil } val, err := Arithm(cfg, x.X) if err != nil { return 0, err } switch x.Op { case syntax.Not: return oneIf(val == 0), nil case syntax.BitNegation: return ^val, nil case syntax.Plus: return val, nil default: // syntax.Minus return -val, nil } case *syntax.BinaryArithm: switch x.Op { case syntax.Assgn, syntax.AddAssgn, syntax.SubAssgn, syntax.MulAssgn, syntax.QuoAssgn, syntax.RemAssgn, syntax.AndAssgn, syntax.OrAssgn, syntax.XorAssgn, syntax.ShlAssgn, syntax.ShrAssgn: return cfg.assgnArit(x) case syntax.TernQuest: // TernColon can't happen here cond, err := Arithm(cfg, x.X) if err != nil { return 0, err } b2 := x.Y.(*syntax.BinaryArithm) // must have Op==TernColon if cond == 1 { return Arithm(cfg, b2.X) } return Arithm(cfg, b2.Y) } left, err := Arithm(cfg, x.X) if err != nil { return 0, err } right, err := Arithm(cfg, x.Y) if err != nil { return 0, err } return binArit(x.Op, left, right), nil default: panic(fmt.Sprintf("unexpected arithm expr: %T", x)) } } func oneIf(b bool) int { if b { return 1 } return 0 } // atoi is just a shorthand for strconv.Atoi that ignores the error, // just like shells do. func atoi(s string) int { n, _ := strconv.Atoi(s) 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 } 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: val /= arg case syntax.RemAssgn: 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.Itoa(val)); err != nil { return 0, err } return 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 { switch op { case syntax.Add: return x + y case syntax.Sub: return x - y case syntax.Mul: return x * y case syntax.Quo: return x / y case syntax.Rem: return x % y case syntax.Pow: return intPow(x, y) case syntax.Eql: return oneIf(x == y) case syntax.Gtr: return oneIf(x > y) case syntax.Lss: return oneIf(x < y) case syntax.Neq: return oneIf(x != y) case syntax.Leq: return oneIf(x <= y) case syntax.Geq: return oneIf(x >= y) case syntax.And: return x & y case syntax.Or: return x | y case syntax.Xor: return x ^ y case syntax.Shr: return x >> uint(y) case syntax.Shl: return x << uint(y) case syntax.AndArit: return oneIf(x != 0 && y != 0) case syntax.OrArit: return oneIf(x != 0 || y != 0) default: // syntax.Comma // x is executed but its result discarded return y } } sh-3.4.3/expand/braces.go000066400000000000000000000036411420425111700152010ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package expand import ( "strconv" "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 from, err1 := strconv.Atoi(br.Elems[0].Lit()) to, err2 := strconv.Atoi(br.Elems[1].Lit()) 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 = 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}} } sh-3.4.3/expand/braces_test.go000066400000000000000000000065701420425111700162440ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // 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{-..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("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() any := false for _, part := range word.Parts { if _, any = part.(*syntax.BraceExp); any { break } } if any && !want { t.Fatalf("didn't want any BraceExp node, but found one") } else if !any && 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() } sh-3.4.3/expand/doc.go000066400000000000000000000002641420425111700145050ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information // Package expand contains code to perform various shell expansions. package expand sh-3.4.3/expand/environ.go000066400000000000000000000145461420425111700154300ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package expand import ( "runtime" "sort" "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 // 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) } // 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. // // It is the implementation's responsibility to handle variable // attributes correctly. For example, changing an exported variable's // value does not unexport it, and overwriting a name reference variable // should modify its target. // // 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 } type ValueKind uint8 const ( Unset ValueKind = iota String NameRef Indexed Associative ) // Variable describes a shell variable, which can have a number of attributes // and a value. // // A Variable is unset if its Kind field is Unset, which can be checked via // Variable.IsSet. The zero value of a Variable is thus a valid unset variable. // // If a variable is set, its Value field will be a []string if it is an indexed // array, a map[string]string if it's an associative array, or a string // otherwise. type Variable struct { Local bool Exported bool ReadOnly bool 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 returns whether the variable is set. An empty variable is set, but an // undeclared variable is not. func (v Variable) IsSet() bool { return v.Kind != Unset } // 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 i := 0; i < maxNameRefDepth; i++ { 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{Exported: true, Kind: String, Str: value} } func (f funcEnviron) Each(func(name string, vr Variable) bool) {} // ListEnviron returns an Environ with the supplied variables, in the form // "key=value". All variables will be exported. The last value in pairs is used // if multiple values are present. // // On Windows, where environment variable names are case-insensitive, the // resulting variable names will all be uppercase. func ListEnviron(pairs ...string) Environ { return listEnvironWithUpper(runtime.GOOS == "windows", pairs...) } // listEnvironWithUpper implements ListEnviron, but letting the tests specify // whether to uppercase all names or not. func listEnvironWithUpper(upper bool, pairs ...string) Environ { list := append([]string{}, pairs...) if upper { // Uppercase before sorting, so that we can remove duplicates // without the need for linear search nor a map. for i, s := range list { if sep := strings.IndexByte(s, '='); sep > 0 { list[i] = strings.ToUpper(s[:sep]) + s[sep:] } } } sort.SliceStable(list, func(i, j int) bool { isep := strings.IndexByte(list[i], '=') jsep := strings.IndexByte(list[j], '=') if isep < 0 { isep = 0 } else { isep += 1 } if jsep < 0 { jsep = 0 } else { jsep += 1 } return list[i][:isep] < list[j][:jsep] }) last := "" for i := 0; i < len(list); { s := list[i] sep := strings.IndexByte(s, '=') if sep <= 0 { // invalid element; remove it list = append(list[:i], list[i+1:]...) continue } name := s[:sep] if last == name { // duplicate; the last one wins list = append(list[:i-1], list[i:]...) continue } last = name i++ } return listEnviron(list) } // listEnviron is a sorted list of "name=value" strings. type listEnviron []string func (l listEnviron) Get(name string) Variable { prefix := name + "=" i := sort.SearchStrings(l, prefix) if i < len(l) && strings.HasPrefix(l[i], prefix) { return Variable{Exported: true, Kind: String, Str: strings.TrimPrefix(l[i], prefix)} } return Variable{} } func (l listEnviron) Each(fn func(name string, vr Variable) bool) { for _, pair := range l { i := strings.IndexByte(pair, '=') if i < 0 { // should never happen; see listEnvironWithUpper panic("expand.listEnviron: did not expect malformed name-value pair: " + pair) } name, value := pair[:i], pair[i+1:] if !fn(name, Variable{Exported: true, Kind: String, Str: value}) { return } } } sh-3.4.3/expand/environ_test.go000066400000000000000000000034271420425111700164630ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package expand import ( "reflect" "testing" ) func TestListEnviron(t *testing.T) { tests := []struct { name string upper bool pairs []string want []string }{ { name: "Empty", pairs: nil, want: []string{}, }, { name: "Simple", pairs: []string{"A=b", "c="}, want: []string{"A=b", "c="}, }, { name: "MissingEqual", pairs: []string{"A=b", "invalid", "c="}, want: []string{"A=b", "c="}, }, { name: "DuplicateNames", pairs: []string{"A=x", "A=b", "c=", "c=y"}, want: []string{"A=b", "c=y"}, }, { name: "NoName", pairs: []string{"=b", "=c"}, want: []string{}, }, { name: "EmptyElements", pairs: []string{"A=b", "", "", "c="}, want: []string{"A=b", "c="}, }, { name: "MixedCaseNoUpper", pairs: []string{"A=b1", "Path=foo", "a=b2"}, want: []string{"A=b1", "Path=foo", "a=b2"}, }, { name: "MixedCaseUpper", upper: true, pairs: []string{"A=b1", "Path=foo", "a=b2"}, want: []string{"A=b2", "PATH=foo"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { gotEnv := listEnvironWithUpper(tc.upper, tc.pairs...) got := []string(gotEnv.(listEnviron)) if !reflect.DeepEqual(got, tc.want) { t.Fatalf("ListEnviron(%t, %q) wanted %q, got %q", tc.upper, tc.pairs, tc.want, got) } }) } } func TestGetWithSameSubPrefix(t *testing.T) { gotEnv := ListEnviron("GREETING=text1", "GREETING2=text2") got := gotEnv.Get("GREETING2").String() if got != "text2" { t.Fatalf("ListEnviron.Get(GREETING2) wanted text2, got %q", got) } got = gotEnv.Get("GREETING").String() if got != "text1" { t.Fatalf("ListEnviron.Get(GREETING) wanted text1, got %q", got) } } sh-3.4.3/expand/expand.go000066400000000000000000000553021420425111700152220ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package expand import ( "bytes" "fmt" "io" "os" "os/user" "path/filepath" "regexp" "runtime" "strconv" "strings" "mvdan.cc/sh/v3/pattern" "mvdan.cc/sh/v3/syntax" ) // A Config specifies details about how shell expansion should be performed. The // zero value is a valid configuration. type Config struct { // Env is used to get and set environment variables when performing // shell expansions. Some special parameters are also expanded via this // interface, such as: // // * "#", "@", "*", "0"-"9" for the shell's parameters // * "?", "$", "PPID" for the shell's status and process // * "HOME foo" to retrieve user foo's home directory (if unset, // os/user.Lookup will be used) // // If nil, there are no environment variables set. Use // ListEnviron(os.Environ()...) to use the system's environment // variables. Env Environ // CmdSubst expands a command substitution node, writing its standard // output to the provided io.Writer. // // If nil, encountering a command substitution will result in an // UnexpectedCommandError. CmdSubst func(io.Writer, *syntax.CmdSubst) error // ProcSubst expands a process substitution node. // // Note that this feature is a work in progress, and the signature of // this field might change until #451 is completely fixed. ProcSubst func(*syntax.ProcSubst) (string, error) // TODO(v4): update to os.Readdir with fs.DirEntry. // We could possibly expose that as a preferred ReadDir2 before then, // to allow users to opt into better performance in v3. // ReadDir is used for file path globbing. If nil, globbing is disabled. // Use ioutil.ReadDir to use the filesystem directly. ReadDir func(string) ([]os.FileInfo, error) // GlobStar corresponds to the shell option that allows globbing with // "**". GlobStar bool // NullGlob corresponds to the shell option that allows globbing // patterns which match nothing to result in zero fields. NullGlob bool // NoUnset corresponds to the shell option that treats unset variables // as errors. NoUnset bool bufferAlloc bytes.Buffer // TODO: use 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{} func prepareConfig(cfg *Config) *Config { if cfg == nil { cfg = zeroConfig } if cfg.Env == nil { cfg.Env = FuncEnviron(func(string) string { return "" }) } cfg.ifs = " \t\n" if vr := cfg.Env.Get("IFS"); vr.IsSet() { cfg.ifs = vr.String() } 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() *bytes.Buffer { 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{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 within double quotes. It // is simlar 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, quoteDouble) if err != nil { return "", err } return cfg.fieldJoin(field), nil } const patMode = pattern.Filenames | pattern.Braces // Pattern expands a single shell word as a pattern, using syntax.QuotePattern // on any non-quoted parts of the input word. The result can be used on // syntax.TranslatePattern directly. // // The config specifies shell expansion options; nil behaves the same as an // empty config. func Pattern(cfg *Config, word *syntax.Word) (string, error) { cfg = prepareConfig(cfg) field, err := cfg.wordField(word.Parts, quoteNone) if err != nil { return "", err } buf := cfg.strBuilder() for _, part := range field { if part.quote > quoteNone { buf.WriteString(pattern.QuoteMeta(part.val, patMode)) } else { buf.WriteString(part.val) } } return buf.String(), nil } // Format expands a format string with a number of arguments, following the // shell's format specifications. These include printf(1), among others. // // The resulting string is returned, along with the number of arguments used. // // The config specifies shell expansion options; nil behaves the same as an // empty config. func Format(cfg *Config, format string, args []string) (string, int, error) { cfg = prepareConfig(cfg) buf := cfg.strBuilder() var fmts []byte initialArgs := len(args) formatLoop: for i := 0; i < len(format); i++ { // readDigits reads from 0 to max digits, either octal or // hexadecimal. readDigits := func(max int, hex bool) string { j := 0 for ; j < max; j++ { c := format[i+j] if (c >= '0' && c <= '9') || (hex && c >= 'a' && c <= 'f') || (hex && c >= 'A' && c <= 'F') { // valid octal or hex char } else { break } } digits := format[i : i+j] i += j - 1 // -1 since the outer loop does i++ return digits } c := format[i] switch { case c == '\\': // escaped i++ switch c = format[i]; c { case 'a': // bell buf.WriteByte('\a') case 'b': // backspace buf.WriteByte('\b') case 'e', 'E': // escape buf.WriteByte('\x1b') case 'f': // form feed buf.WriteByte('\f') case 'n': // new line buf.WriteByte('\n') case 'r': // carriage return buf.WriteByte('\r') case 't': // horizontal tab buf.WriteByte('\t') case 'v': // vertical tab buf.WriteByte('\v') case '\\', '\'', '"', '?': // just the character buf.WriteByte(c) case '0', '1', '2', '3', '4', '5', '6', '7': digits := readDigits(3, false) // if digits don't fit in 8 bits, 0xff via strconv n, _ := strconv.ParseUint(digits, 8, 8) buf.WriteByte(byte(n)) case 'x', 'u', 'U': i++ max := 2 if c == 'u' { max = 4 } else if c == 'U' { max = 8 } digits := readDigits(max, true) if len(digits) > 0 { // can't error n, _ := strconv.ParseUint(digits, 16, 32) if n == 0 { // If we're about to print \x00, // stop the entire loop, like bash. break formatLoop } if c == 'x' { // always as a single byte buf.WriteByte(byte(n)) } else { buf.WriteRune(rune(n)) } break } fallthrough default: // no escape sequence buf.WriteByte('\\') buf.WriteByte(c) } case len(fmts) > 0: switch c { case '%': buf.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] } } buf.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', 'd', 'i', 'u', 'o', 'x': arg := "" if len(args) > 0 { arg, args = args[0], args[1:] } var farg interface{} = arg 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' } } fmts = append(fmts, c) fmt.Fprintf(buf, 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: buf.WriteByte(c) } } if len(fmts) > 0 { return "", 0, fmt.Errorf("missing format char") } return buf.String(), 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 } buf := cfg.strBuilder() for _, part := range parts { buf.WriteString(part.val) } return buf.String() } func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob bool) { buf := cfg.strBuilder() for _, part := range parts { if part.quote > quoteNone { buf.WriteString(pattern.QuoteMeta(part.val, patMode)) continue } buf.WriteString(part.val) if pattern.HasMeta(part.val, patMode) { glob = true } } if glob { // only copy the string if it will be used escaped = buf.String() } return escaped, glob } // Fields expands a number of words as if they were arguments in a shell // command. This includes brace expansion, tilde expansion, parameter expansion, // command substitution, arithmetic expansion, and quote removal. func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) { cfg = prepareConfig(cfg) fields := make([]string, 0, len(words)) dir := cfg.envGet("PWD") for _, word := range words { word := *word // make a copy, since SplitBraces replaces the Parts slice afterBraces := []*syntax.Word{&word} if syntax.SplitBraces(&word) { afterBraces = Braces(&word) } for _, word2 := range afterBraces { wfields, err := cfg.wordFields(word2.Parts) if err != nil { return nil, err } for _, field := range wfields { path, doGlob := cfg.escapedGlobField(field) var matches []string if doGlob && cfg.ReadDir != nil { matches, err = cfg.glob(dir, path) if err != nil { return nil, err } if len(matches) > 0 || cfg.NullGlob { fields = append(fields, matches...) continue } } fields = append(fields, cfg.fieldJoin(field)) } } } return fields, nil } type fieldPart struct { val string quote quoteLevel } type quoteLevel uint const ( quoteNone quoteLevel = iota quoteDouble quoteSingle ) func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) { var field []fieldPart for i, wp := range wps { switch x := wp.(type) { case *syntax.Lit: s := x.Value if i == 0 && ql == quoteNone { if prefix, rest := cfg.expandUser(s); prefix != "" { // TODO: return two separate fieldParts, // like in wordFields? s = prefix + rest } } if ql == quoteDouble && strings.Contains(s, "\\") { buf := cfg.strBuilder() for i := 0; i < len(s); i++ { b := s[i] if b == '\\' && i+1 < len(s) { switch s[i+1] { case '"', '\\', '$', '`': // special chars continue } } buf.WriteByte(b) } s = buf.String() } if i := strings.IndexByte(s, '\x00'); i >= 0 { s = s[:i] } field = append(field, fieldPart{val: s}) case *syntax.SglQuoted: fp := fieldPart{quote: quoteSingle, val: x.Value} if x.Dollar { fp.val, _, _ = Format(cfg, fp.val, nil) } field = append(field, fp) case *syntax.DblQuoted: wfield, err := cfg.wordField(x.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(x) if err != nil { return nil, err } field = append(field, fieldPart{val: val}) case *syntax.CmdSubst: val, err := cfg.cmdSubst(x) if err != nil { return nil, err } field = append(field, fieldPart{val: val}) case *syntax.ArithmExp: n, err := Arithm(cfg, x.X) if err != nil { return nil, err } field = append(field, fieldPart{val: strconv.Itoa(n)}) case *syntax.ProcSubst: path, err := cfg.ProcSubst(x) if err != nil { return nil, err } field = append(field, fieldPart{val: path}) default: panic(fmt.Sprintf("unhandled word part: %T", x)) } } return field, nil } func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) { if cfg.CmdSubst == nil { return "", UnexpectedCommandError{Node: cs} } buf := cfg.strBuilder() if err := cfg.CmdSubst(buf, cs); err != nil { return "", err } out := buf.String() if strings.IndexByte(out, '\x00') >= 0 { 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) { for i, field := range strings.FieldsFunc(val, cfg.ifsRune) { if i > 0 { flush() } curField = append(curField, fieldPart{val: field}) } } for i, wp := range wps { switch x := wp.(type) { case *syntax.Lit: s := x.Value if i == 0 { prefix, rest := cfg.expandUser(s) curField = append(curField, fieldPart{ quote: quoteSingle, val: prefix, }) s = rest } if strings.Contains(s, "\\") { buf := cfg.strBuilder() for i := 0; i < len(s); i++ { b := s[i] if b == '\\' { if i++; i >= len(s) { break } b = s[i] } buf.WriteByte(b) } s = buf.String() } curField = append(curField, fieldPart{val: s}) case *syntax.SglQuoted: allowEmpty = true fp := fieldPart{quote: quoteSingle, val: x.Value} if x.Dollar { fp.val, _, _ = Format(cfg, fp.val, nil) } curField = append(curField, fp) case *syntax.DblQuoted: if len(x.Parts) == 1 { pe, _ := x.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(x.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(x) if err != nil { return nil, err } splitAdd(val) case *syntax.CmdSubst: val, err := cfg.cmdSubst(x) if err != nil { return nil, err } splitAdd(val) case *syntax.ArithmExp: n, err := Arithm(cfg, x.X) if err != nil { return nil, err } curField = append(curField, fieldPart{val: strconv.Itoa(n)}) case *syntax.ProcSubst: path, err := cfg.ProcSubst(x) if err != nil { return nil, err } splitAdd(path) default: panic(fmt.Sprintf("unhandled word part: %T", x)) } } 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 if it was in the form of ${*}, ${@}, ${foo[*], ${foo[@]}, // or ${!foo@}. func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string { if pe == nil || pe.Length || pe.Width { return nil } if pe.Excl { if pe.Names == syntax.NamesPrefixWords { return cfg.namesByPrefix(pe.Param.Value) } return nil } name := pe.Param.Value switch name { case "*": return []string{cfg.ifsJoin(cfg.Env.Get(name).List)} case "@": return cfg.Env.Get(name).List } switch nodeLit(pe.Index) { case "@": if vr := cfg.Env.Get(name); vr.Kind == Indexed { return vr.List } case "*": if vr := cfg.Env.Get(name); vr.Kind == Indexed { return []string{cfg.ifsJoin(vr.List)} } } return nil } func (cfg *Config) expandUser(field string) (prefix, rest string) { if len(field) == 0 || field[0] != '~' { return "", field } name := field[1:] if i := strings.Index(name, "/"); 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 ", otherwise fall back to // os/user. There isn't a way to lookup user home dirs without cgo. if vr := cfg.Env.Get("HOME " + name); vr.IsSet() { return vr.String(), rest } u, err := user.Lookup(name) if err != nil { return "", field } return u.HomeDir, rest } func findAllIndex(pat, name string, n int) [][]int { expr, err := pattern.Regexp(pat, 0) if err != nil { return nil } rx := regexp.MustCompile(expr) return rx.FindAllStringIndex(name, n) } var rxGlobStar = regexp.MustCompile(".*") // pathJoin2 is a simpler version of filepath.Join without cleaning the result, // since that's needed for globbing. func pathJoin2(elem1, elem2 string) string { if elem1 == "" { return elem2 } if strings.HasSuffix(elem1, string(filepath.Separator)) { return elem1 + elem2 } return elem1 + string(filepath.Separator) + elem2 } // pathSplit splits a file path into its elements, retaining empty ones. Before // splitting, slashes are replaced with filepath.Separator, so that splitting // Unix paths on Windows works as well. func pathSplit(path string) []string { path = filepath.FromSlash(path) return strings.Split(path, string(filepath.Separator)) } func (cfg *Config) glob(base, pat string) ([]string, error) { parts := pathSplit(pat) matches := []string{""} if filepath.IsAbs(pat) { if parts[0] == "" { // unix-like matches[0] = string(filepath.Separator) } else { // windows (for some reason it won't work without the // trailing separator) matches[0] = parts[0] + string(filepath.Separator) } parts = parts[1:] } // TODO: as an optimization, we could do chunks of the path all at once, // like doing a single stat for "/foo/bar" in "/foo/bar/*". for i, part := range parts { 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, patMode): var newMatches []string for _, dir := range matches { match := dir if !filepath.IsAbs(match) { match = filepath.Join(base, match) } match = pathJoin2(match, part) info, err := os.Stat(match) if err != nil { continue } if wantDir && !info.IsDir() { continue } newMatches = append(newMatches, pathJoin2(dir, part)) } matches = newMatches continue case part == "**" && cfg.GlobStar: for i, match := range matches { // "a/**" should match "a/ a/b a/b/cfg ..."; note // how the zero-match case has a trailing // separator. matches[i] = pathJoin2(match, "") } // expand all the possible levels of ** latest := matches for { var newMatches []string for _, dir := range latest { var err error newMatches, err = cfg.globDir(base, dir, rxGlobStar, wantDir, newMatches) if err != nil { return nil, err } } if len(newMatches) == 0 { // not another level of directories to // try; stop break } matches = append(matches, newMatches...) latest = newMatches } continue } expr, err := pattern.Regexp(part, pattern.Filenames) if err != nil { // If any glob part is not a valid pattern, don't glob. return nil, nil } rx := regexp.MustCompile("^" + expr + "$") var newMatches []string for _, dir := range matches { newMatches, err = cfg.globDir(base, dir, rx, wantDir, newMatches) if err != nil { return nil, err } } matches = newMatches } return matches, nil } func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, matches []string) ([]string, error) { fullDir := dir if !filepath.IsAbs(dir) { fullDir = filepath.Join(base, dir) } infos, err := cfg.ReadDir(fullDir) if err != nil { return nil, err } for _, info := range infos { name := info.Name() if !wantDir { // no filtering } else if mode := info.Mode(); mode&os.ModeSymlink != 0 { // TODO: is there a way to do this without the // extra syscall? if _, err := cfg.ReadDir(filepath.Join(fullDir, name)); err != nil { // symlink pointing to non-directory continue } } else if !mode.IsDir() { // definitely not a directory continue } if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' { continue } if rx.MatchString(name) { matches = append(matches, pathJoin2(dir, name)) } } return matches, nil } // ReadFields TODO write doc. // // 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 } sh-3.4.3/expand/expand_test.go000066400000000000000000000030521420425111700162540ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package expand import ( "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 j := 0; j < 2; j++ { 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) } } } } sh-3.4.3/expand/param.go000066400000000000000000000216111420425111700150370ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package expand import ( "fmt" "regexp" "sort" "strconv" "strings" "unicode" "unicode/utf8" "mvdan.cc/sh/v3/pattern" "mvdan.cc/sh/v3/syntax" ) func nodeLit(node syntax.Node) string { if word, ok := node.(*syntax.Word); ok { return word.Lit() } return "" } type UnsetParameterError struct { Node *syntax.ParamExp Message string } func (u UnsetParameterError) Error() string { return fmt.Sprintf("%s: %s", u.Node.Param.Value, u.Message) } func overridingUnset(pe *syntax.ParamExp) bool { if pe.Exp == nil { return false } switch pe.Exp.Op { case syntax.AlternateUnset, syntax.AlternateUnsetOrNull, syntax.DefaultUnset, syntax.DefaultUnsetOrNull, syntax.ErrorUnset, syntax.ErrorUnsetOrNull, syntax.AssignUnset, syntax.AssignUnsetOrNull: return true } return false } func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { oldParam := cfg.curParam cfg.curParam = pe defer func() { cfg.curParam = oldParam }() name := pe.Param.Value index := pe.Index switch name { case "@", "*": index = &syntax.Word{Parts: []syntax.WordPart{ &syntax.Lit{Value: name}, }} } var vr Variable switch name { case "LINENO": // This is the only parameter expansion that the environment // interface cannot satisfy. line := uint64(cfg.curParam.Pos().Line()) vr = Variable{Kind: String, Str: strconv.FormatUint(line, 10)} default: vr = cfg.Env.Get(name) } orig := vr _, vr = vr.Resolve(cfg.Env) if cfg.NoUnset && vr.Kind == Unset && !overridingUnset(pe) { 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 Unset: elems = nil indexAllElements = true case Indexed: indexAllElements = true callVarInd = false elems = vr.List slicePos := func(n int) int { if n < 0 { n = len(elems) + n if n < 0 { n = len(elems) } } else if n > len(elems) { n = len(elems) } return n } if pe.Slice != nil && pe.Slice.Offset != nil { elems = elems[slicePos(sliceOffset):] } if pe.Slice != nil && pe.Slice.Length != nil { elems = elems[:slicePos(sliceLen)] } str = strings.Join(elems, " ") } } if callVarInd { var err error str, err = cfg.varInd(vr, index) if err != nil { return "", err } } if !indexAllElements { elems = []string{str} } switch { case pe.Length: n := len(elems) switch nodeLit(index) { case "@", "*": default: n = utf8.RuneCountInString(str) } str = strconv.Itoa(n) case pe.Excl: var strs []string switch { case pe.Names != 0: strs = cfg.namesByPrefix(pe.Param.Value) case orig.Kind == NameRef: strs = append(strs, orig.Str) case vr.Kind == Indexed: for i, e := range vr.List { if e != "" { strs = append(strs, strconv.Itoa(i)) } } case vr.Kind == Associative: for k := range vr.Map { strs = append(strs, k) } case !syntax.ValidName(str): return "", fmt.Errorf("invalid indirect expansion") default: vr = cfg.Env.Get(str) strs = append(strs, vr.String()) } sort.Strings(strs) str = strings.Join(strs, " ") case pe.Slice != nil: if callVarInd { slicePos := func(n int) int { if n < 0 { n = len(str) + n if n < 0 { n = len(str) } } else if n > len(str) { n = len(str) } return n } if pe.Slice.Offset != nil { str = str[slicePos(sliceOffset):] } if pe.Slice.Length != nil { str = str[:slicePos(sliceLen)] } } else { // elems are already sliced } case pe.Repl != nil: orig, err := Pattern(cfg, pe.Repl.Orig) if err != nil { return "", err } 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) buf := cfg.strBuilder() last := 0 for _, loc := range locs { buf.WriteString(str[last:loc[0]]) buf.WriteString(with) last = loc[1] } buf.WriteString(str[last:]) str = buf.String() case pe.Exp != nil: arg, err := Literal(cfg, pe.Exp.Word) if err != nil { return "", err } switch op := pe.Exp.Op; op { case syntax.AlternateUnsetOrNull: if str == "" { break } fallthrough case syntax.AlternateUnset: if vr.IsSet() { str = arg } case syntax.DefaultUnset: if vr.IsSet() { break } fallthrough case syntax.DefaultUnsetOrNull: if str == "" { str = arg } case syntax.ErrorUnset: if vr.IsSet() { break } fallthrough case syntax.ErrorUnsetOrNull: if str == "" { return "", UnsetParameterError{ Node: pe, Message: arg, } } case syntax.AssignUnset: if vr.IsSet() { break } fallthrough case syntax.AssignUnsetOrNull: if str == "" { if err := cfg.envSet(name, arg); err != nil { return "", err } str = arg } case syntax.RemSmallPrefix, syntax.RemLargePrefix, syntax.RemSmallSuffix, syntax.RemLargeSuffix: suffix := op == syntax.RemSmallSuffix || op == syntax.RemLargeSuffix small := op == syntax.RemSmallPrefix || op == syntax.RemSmallSuffix for i, elem := range elems { elems[i] = removePattern(elem, arg, suffix, small) } str = strings.Join(elems, " ") case syntax.UpperFirst, syntax.UpperAll, syntax.LowerFirst, syntax.LowerAll: caseFunc := unicode.ToLower if op == syntax.UpperFirst || op == syntax.UpperAll { caseFunc = unicode.ToUpper } all := op == syntax.UpperAll || op == syntax.LowerAll // empty string means '?'; nothing to do there expr, err := pattern.Regexp(arg, 0) if err != nil { return str, nil } rx := regexp.MustCompile(expr) for i, elem := range elems { rs := []rune(elem) for ri, r := range rs { if rx.MatchString(string(r)) { rs[ri] = caseFunc(r) if !all { break } } } elems[i] = string(rs) } str = strings.Join(elems, " ") case syntax.OtherParamOps: switch arg { case "Q": str, err = syntax.Quote(str, syntax.LangBash) if err != nil { // Is this even possible? If a user runs into this panic, // it's most likely a bug we need to fix. panic(err) } case "E": tail := str var rns []rune for tail != "" { var rn rune rn, _, tail, _ = strconv.UnquoteChar(tail, 0) rns = append(rns, rn) } str = string(rns) case "P", "A", "a": panic(fmt.Sprintf("unhandled @%s param expansion", arg)) default: panic(fmt.Sprintf("unexpected @%s param expansion", arg)) } } } return str, nil } func removePattern(str, pat string, fromEnd, shortest bool) string { var mode pattern.Mode if shortest { mode |= pattern.Shortest } expr, err := pattern.Regexp(pat, mode) if err != nil { return str } switch { case fromEnd && shortest: // use .* to get the right-most shortest match expr = ".*(" + expr + ")$" case fromEnd: // simple suffix expr = "(" + expr + ")$" default: // simple prefix expr = "^(" + expr + ")" } // no need to check error as Translate returns one rx := regexp.MustCompile(expr) if loc := rx.FindStringSubmatchIndex(str); loc != nil { // remove the original pattern (the submatch) str = str[:loc[2]] + str[loc[3]:] } return str } func (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, error) { if idx == nil { return vr.String(), nil } switch vr.Kind { case String: n, err := Arithm(cfg, idx) if err != nil { return "", err } if n == 0 { return vr.Str, nil } case Indexed: switch nodeLit(idx) { case "*", "@": return strings.Join(vr.List, " "), nil } i, err := Arithm(cfg, idx) if err != nil { return "", err } if i < 0 { return "", fmt.Errorf("negative array index") } if i < len(vr.List) { return vr.List[i], nil } case Associative: switch lit := nodeLit(idx); lit { case "@", "*": strs := make([]string, 0, len(vr.Map)) for _, val := range vr.Map { strs = append(strs, val) } sort.Strings(strs) 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 cfg.Env.Each(func(name string, vr Variable) bool { if strings.HasPrefix(name, prefix) { names = append(names, name) } return true }) return names } sh-3.4.3/fileutil/000077500000000000000000000000001420425111700137455ustar00rootroot00000000000000sh-3.4.3/fileutil/file.go000066400000000000000000000050041420425111700152120ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information // Package fileutil contains code to work with shell files, also known // as shell scripts. package fileutil import ( "io/fs" "os" "regexp" "strings" ) var ( shebangRe = regexp.MustCompile(`^#!\s?/(usr/)?bin/(env\s+)?(sh|bash)\s`) extRe = regexp.MustCompile(`\.(sh|bash)$`) ) // HasShebang reports whether bs begins with a valid sh or bash shebang. // It supports variations with /usr and env. func HasShebang(bs []byte) bool { return shebangRe.Match(bs) } // 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 os.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 os.FileInfo) ScriptConfidence { // TODO: once we drop support for Go 1.16, // make use of this Go 1.17 API instead: // return CouldBeScript2(fs.FileInfoToDirEntry(info)) name := info.Name() switch { case info.IsDir(), name[0] == '.': return ConfNotScript case info.Mode()&os.ModeSymlink != 0: return ConfNotScript case extRe.MatchString(name): return ConfIsScript case strings.IndexByte(name, '.') > 0: return ConfNotScript // different extension default: return ConfIfShebang } } // CouldBeScript2 reports how likely a directory entry is to be a shell script. // It discards directories, symlinks, hidden files and files with non-shell // extensions. func CouldBeScript2(entry fs.DirEntry) ScriptConfidence { name := entry.Name() switch { case entry.IsDir(), name[0] == '.': return ConfNotScript case entry.Type()&os.ModeSymlink != 0: return ConfNotScript case extRe.MatchString(name): return ConfIsScript case strings.IndexByte(name, '.') > 0: return ConfNotScript // different extension default: return ConfIfShebang } } sh-3.4.3/go.mod000066400000000000000000000007571420425111700132470ustar00rootroot00000000000000module mvdan.cc/sh/v3 go 1.16 require ( github.com/creack/pty v1.1.15 github.com/frankban/quicktest v1.13.1 github.com/google/renameio v1.0.1 github.com/kr/pretty v0.3.0 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 golang.org/x/term v0.0.0-20210916214954-140adaaadfaf mvdan.cc/editorconfig v0.2.0 ) sh-3.4.3/go.sum000066400000000000000000000064141420425111700132700ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/frankban/quicktest v1.13.1 h1:xVm/f9seEhZFL9+n5kv5XLrGwy6elc4V9v/XFY2vmd8= github.com/frankban/quicktest v1.13.1/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU= github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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 h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451 h1:d1PiN4RxzIFXCJTvRkvSkKqwtRAl5ZV4lATKtQI0B7I= github.com/rogpeppe/go-internal v1.8.1-0.20210923151022-86f73c517451/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210925032602-92d5a993a665 h1:QOQNt6vCjMpXE7JSK5VvAzJC1byuN3FgTNSBwf+CJgI= golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210916214954-140adaaadfaf h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI= golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= mvdan.cc/editorconfig v0.2.0 h1:XL+7ys6ls/RKrkUNFQvEwIvNHh+JKx8Mj1pUV5wQxQE= mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= sh-3.4.3/interp/000077500000000000000000000000001420425111700134315ustar00rootroot00000000000000sh-3.4.3/interp/api.go000066400000000000000000000372731420425111700145450ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information // Package interp implements an interpreter that executes shell // programs. It aims to support POSIX, but its support is not complete // yet. It also supports some Bash features. package interp import ( "context" "errors" "fmt" "io" "math/rand" "os" "path/filepath" "strconv" "sync" "time" "golang.org/x/sync/errgroup" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) // A Runner interprets shell programs. It can be reused, but it is not safe for // concurrent use. You should typically use New to build a new Runner. // // 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. // // To create a Runner, use New. Runner's exported fields are meant to be // configured via runner options; once a Runner has been created, the fields // should be treated as read-only. type Runner struct { // Env specifies the initial environment for the interpreter, which must // be non-nil. Env expand.Environ writeEnv expand.WriteEnviron // Dir specifies the working directory of the command, which must be an // absolute path. Dir string // Params are the current shell parameters, e.g. from running a shell // file or calling a function. Accessible via the $@/$* family of vars. 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 // execHandler is a function responsible for executing programs. It must be non-nil. execHandler ExecHandlerFunc // openHandler is a function responsible for opening files. It must be non-nil. openHandler OpenHandlerFunc stdin io.Reader 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 // rand is used mainly to generate temporary files. rand *rand.Rand // wgProcSubsts allows waiting for any process substitution sub-shells // to finish running. wgProcSubsts sync.WaitGroup filename string // only if Node was a File // >0 to break or continue out of N enclosing loops breakEnclosing, contnEnclosing int inLoop bool inFunc bool inSource bool noErrExit bool // track if a sourced script set positional parameters sourceSetParams bool err error // current shell exit code or fatal error handlingTrap bool // whether we're currently in a trap callback shellExited bool // whether the shell needs to exit // The current and last exit status code. They can only be different if // the interpreter is in the middle of running a statement. In that // scenario, 'exit' is the status code for the statement being run, and // 'lastExit' corresponds to the previous statement that was run. exit int lastExit int bgShells errgroup.Group opts runnerOpts origDir string origParams []string origOpts runnerOpts origStdin io.Reader 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 } type alias struct { args []*syntax.Word blank bool } func (r *Runner) optByFlag(flag byte) *bool { for i, opt := range &shellOptsTable { if opt.flag == flag { return &r.opts[i] } } return nil } // New creates a new Runner, applying a number of options. If applying any of // the options results in an error, it is returned. // // Any unset options fall back to their defaults. For example, not supplying the // environment falls back to the process's environment, and not supplying the // standard output writer means that the output will be discarded. func New(opts ...RunnerOption) (*Runner, error) { r := &Runner{ usedNew: true, execHandler: DefaultExecHandler(2 * time.Second), openHandler: DefaultOpenHandler(), } r.dirStack = r.dirBootstrap[:0] 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 is a function which can be passed to New to alter Runner behaviour. // To apply option to existing Runner call it directly, // for example interp.Params("-e")(runner). type RunnerOption func(*Runner) error // Env sets the interpreter's environment. If nil, a copy of the current // process's environment is used. func Env(env expand.Environ) RunnerOption { return func(r *Runner) error { if env == nil { env = expand.ListEnviron(os.Environ()...) } r.Env = env return nil } } // Dir sets the interpreter's working directory. If empty, the process's current // directory is used. func Dir(path string) RunnerOption { return func(r *Runner) error { if path == "" { path, err := os.Getwd() if err != nil { return fmt.Errorf("could not get current dir: %w", err) } r.Dir = path return nil } path, err := filepath.Abs(path) if err != nil { return fmt.Errorf("could not get absolute dir: %w", err) } info, err := os.Stat(path) if err != nil { return fmt.Errorf("could not stat: %w", err) } if !info.IsDir() { return fmt.Errorf("%s is not a directory", path) } r.Dir = path return nil } } // Params populates the shell options and parameters. For example, Params("-e", // "--", "foo") will set the "-e" option and the parameters ["foo"], and // Params("+e") will unset the "-e" option and leave the parameters untouched. // // This is similar to what the interpreter's "set" builtin does. func Params(args ...string) RunnerOption { return func(r *Runner) error { fp := flagParser{remaining: args} for fp.more() { flag := fp.flag() enable := flag[0] == '-' if flag[1] != 'o' { opt := r.optByFlag(flag[1]) if opt == nil { return fmt.Errorf("invalid option: %q", flag) } *opt = enable continue } value := fp.value() if value == "" && enable { for i, opt := range &shellOptsTable { r.printOptLine(opt.name, r.opts[i]) } continue } if value == "" && !enable { for i, opt := range &shellOptsTable { setFlag := "+o" if r.opts[i] { setFlag = "-o" } r.outf("set %s %s\n", setFlag, opt.name) } continue } opt := r.optByName(value, false) if opt == nil { return fmt.Errorf("invalid option: %q", value) } *opt = enable } if args := fp.args(); args != nil { // If "--" wasn't given and there were zero arguments, // we don't want to override the current parameters. r.Params = args // Record whether a sourced script sets the parameters. if r.inSource { r.sourceSetParams = true } } return nil } } // ExecHandler sets command execution handler. See ExecHandlerFunc for more info. func ExecHandler(f ExecHandlerFunc) RunnerOption { return func(r *Runner) error { r.execHandler = f return nil } } // OpenHandler sets file open handler. See OpenHandlerFunc for more info. func OpenHandler(f OpenHandlerFunc) RunnerOption { return func(r *Runner) error { r.openHandler = f return nil } } // StdIO configures an interpreter's standard input, standard output, and // standard error. If out or err are nil, they default to a writer that discards // the output. func StdIO(in io.Reader, out, err io.Writer) RunnerOption { return func(r *Runner) error { r.stdin = in if out == nil { out = io.Discard } r.stdout = out if err == nil { err = io.Discard } r.stderr = err return nil } } func (r *Runner) optByName(name string, bash bool) *bool { if bash { for i, optName := range bashOptsTable { if optName == name { return &r.opts[len(shellOptsTable)+i] } } } for i, opt := range &shellOptsTable { if opt.name == name { return &r.opts[i] } } return nil } type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool var shellOptsTable = [...]struct { flag byte name string }{ // sorted alphabetically by name; use a space for the options // that have no flag form {'a', "allexport"}, {'e', "errexit"}, {'n', "noexec"}, {'f', "noglob"}, {'u', "nounset"}, {' ', "pipefail"}, } var bashOptsTable = [...]string{ // sorted alphabetically by name "expand_aliases", "globstar", "nullglob", } // To access the shell options arrays without a linear search when we // know which option we're after at compile time. First come the shell options, // then the bash options. const ( optAllExport = iota optErrExit optNoExec optNoGlob optNoUnset optPipeFail optExpandAliases optGlobStar optNullGlob ) // Reset returns a runner to its initial state, right before the first call to // Run or Reset. // // Typically, this function only needs to be called if a runner is reused to run // multiple programs non-incrementally. Not calling Reset between each run will // mean that the shell state will be kept, including variables, options, and the // current directory. func (r *Runner) Reset() { if !r.usedNew { panic("use interp.New to construct a Runner") } if !r.didReset { r.origDir = r.Dir r.origParams = r.Params r.origOpts = r.opts r.origStdin = r.stdin r.origStdout = r.stdout r.origStderr = r.stderr } // reset the internal state *r = Runner{ Env: r.Env, execHandler: r.execHandler, openHandler: r.openHandler, // These can be set by functions like Dir or Params, but // builtins can overwrite them; reset the fields to whatever the // constructor set up. Dir: r.origDir, Params: r.origParams, opts: r.origOpts, stdin: r.origStdin, stdout: r.origStdout, stderr: r.origStderr, origDir: r.origDir, origParams: r.origParams, origOpts: r.origOpts, origStdin: r.origStdin, origStdout: r.origStdout, origStderr: r.origStderr, // emptied below, to reuse the space Vars: r.Vars, dirStack: r.dirStack[:0], usedNew: r.usedNew, } if r.Vars == nil { r.Vars = make(map[string]expand.Variable) } else { for k := range r.Vars { delete(r.Vars, k) } } // TODO(v4): Use the supplied Env directly if it implements enough methods. r.writeEnv = &overlayEnviron{parent: r.Env} if !r.writeEnv.Get("HOME").IsSet() { home, _ := os.UserHomeDir() r.setVarString("HOME", home) } if !r.writeEnv.Get("UID").IsSet() { r.setVar("UID", nil, expand.Variable{ Kind: expand.String, ReadOnly: true, Str: strconv.Itoa(os.Getuid()), }) } if !r.writeEnv.Get("GID").IsSet() { r.setVar("GID", nil, expand.Variable{ Kind: expand.String, ReadOnly: true, Str: strconv.Itoa(os.Getgid()), }) } r.setVarString("PWD", r.Dir) r.setVarString("IFS", " \t\n") r.setVarString("OPTIND", "1") r.dirStack = append(r.dirStack, r.Dir) r.didReset = true } // exitStatus is a non-zero status code resulting from running a shell node. type exitStatus uint8 func (s exitStatus) Error() string { return fmt.Sprintf("exit status %d", s) } // NewExitStatus creates an error which contains the specified exit status code. func NewExitStatus(status uint8) error { return exitStatus(status) } // IsExitStatus checks whether error contains an exit status and returns it. func IsExitStatus(err error) (status uint8, ok bool) { var s exitStatus if errors.As(err, &s) { return uint8(s), true } return 0, false } // Run interprets a node, which can be a *File, *Stmt, or Command. If a non-nil // error is returned, it will typically contain a command's exit status, which // can be retrieved with IsExitStatus. // // Run can be called multiple times synchronously to interpret programs // incrementally. To reuse a Runner without keeping the internal shell state, // call Reset. // // Calling Run on an entire *File implies an exit, meaning that an exit trap may // run. func (r *Runner) Run(ctx context.Context, node syntax.Node) error { if !r.didReset { r.Reset() } r.fillExpandConfig(ctx) r.err = nil r.shellExited = false r.filename = "" switch x := node.(type) { case *syntax.File: r.filename = x.Name r.stmts(ctx, x.Stmts) if !r.shellExited { r.exitShell(ctx, r.exit) } case *syntax.Stmt: r.stmt(ctx, x) case syntax.Command: r.cmd(ctx, x) default: return fmt.Errorf("node can only be File, Stmt, or Command: %T", x) } if r.exit != 0 { r.setErr(NewExitStatus(uint8(r.exit))) } if r.Vars != nil { r.writeEnv.Each(func(name string, vr expand.Variable) bool { r.Vars[name] = vr return true }) } return r.err } // Exited reports whether the last Run call should exit an entire shell. This // can be triggered by the "exit" built-in command, for example. // // Note that this state is overwritten at every Run call, so it should be // checked immediately after each Run call. func (r *Runner) Exited() bool { return r.shellExited } // Subshell makes a copy of the given Runner, suitable for use concurrently // with the original. The copy will have the same environment, including // variables and functions, but they can all be modified without affecting the // original. // // Subshell is not safe to use concurrently with Run. Orchestrating this is // left up to the caller; no locking is performed. // // To replace e.g. stdin/out/err, do StdIO(r.stdin, r.stdout, r.stderr)(r) on // the copy. func (r *Runner) Subshell() *Runner { if !r.didReset { r.Reset() } // Keep in sync with the Runner type. Manually copy fields, to not copy // sensitive ones like errgroup.Group, and to do deep copies of slices. r2 := &Runner{ Dir: r.Dir, Params: r.Params, execHandler: r.execHandler, openHandler: r.openHandler, stdin: r.stdin, stdout: r.stdout, stderr: r.stderr, filename: r.filename, opts: r.opts, usedNew: r.usedNew, exit: r.exit, lastExit: r.lastExit, origStdout: r.origStdout, // used for process substitutions } // Env vars and funcs are copied, since they might be modified. // TODO(v4): lazy copying? it would probably be enough to add a // copyOnWrite bool field to Variable, then a Modify method that must be // used when one needs to modify a variable. ideally with some way to // catch direct modifications without the use of Modify and panic, // perhaps via a check when getting or setting vars at some level. oenv := &overlayEnviron{parent: expand.ListEnviron()} r.writeEnv.Each(func(name string, vr expand.Variable) bool { vr2 := vr // Make deeper copies of List and Map, but ensure that they remain nil // if they are nil in vr. vr2.List = append([]string(nil), vr.List...) if vr.Map != nil { vr2.Map = make(map[string]string, len(vr.Map)) for k, vr := range vr.Map { vr2.Map[k] = vr } } oenv.Set(name, vr2) return true }) r2.writeEnv = oenv r2.Funcs = make(map[string]*syntax.Stmt, len(r.Funcs)) for k, v := range r.Funcs { r2.Funcs[k] = v } r2.Vars = make(map[string]expand.Variable) if l := len(r.alias); l > 0 { r2.alias = make(map[string]alias, l) for k, v := range r.alias { r2.alias[k] = v } } r2.dirStack = append(r2.dirBootstrap[:0], r.dirStack...) r2.fillExpandConfig(r.ectx) r2.didReset = true return r2 } sh-3.4.3/interp/builtin.go000066400000000000000000000500531420425111700154310ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package interp import ( "bytes" "context" "errors" "fmt" "io" "os" "path/filepath" "strconv" "strings" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) func isBuiltin(name string) bool { switch name { case "true", ":", "false", "exit", "set", "shift", "unset", "echo", "printf", "break", "continue", "pwd", "cd", "wait", "builtin", "trap", "type", "source", ".", "command", "dirs", "pushd", "popd", "umask", "alias", "unalias", "fg", "bg", "getopts", "eval", "test", "[", "exec", "return", "read", "shopt": return true } return false } func oneIf(b bool) int { if b { return 1 } return 0 } // atoi is just a shorthand for strconv.Atoi that ignores the error, // just like shells do. func atoi(s string) int { n, _ := strconv.Atoi(s) return n } func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, args []string) int { switch name { case "true", ":": case "false": return 1 case "exit": exit := 0 switch len(args) { case 0: exit = r.lastExit case 1: n, err := strconv.Atoi(args[0]) if err != nil { r.errf("invalid exit status code: %q\n", args[0]) return 2 } exit = n default: r.errf("exit cannot take multiple arguments\n") return 1 } r.exitShell(ctx, exit) return exit case "set": if err := Params(args...)(r); err != nil { r.errf("set: %v\n", err) return 2 } r.updateExpandOpts() case "shift": n := 1 switch len(args) { case 0: case 1: if n2, err := strconv.Atoi(args[0]); err == nil { n = n2 break } fallthrough default: r.errf("usage: shift [n]\n") return 2 } if n >= len(r.Params) { r.Params = nil } else { r.Params = r.Params[n:] } case "unset": vars := true funcs := true unsetOpts: for i, arg := range args { switch arg { case "-v": funcs = false case "-f": vars = false default: args = args[i:] break unsetOpts } } for _, arg := range args { if vars && r.lookupVar(arg).IsSet() { r.delVar(arg) } else if _, ok := r.Funcs[arg]; ok && funcs { delete(r.Funcs, arg) } } case "echo": newline, doExpand := true, false echoOpts: for len(args) > 0 { switch args[0] { case "-n": newline = false case "-e": doExpand = true case "-E": // default default: break echoOpts } args = args[1:] } for i, arg := range args { if i > 0 { r.out(" ") } if doExpand { arg, _, _ = expand.Format(r.ecfg, arg, nil) } r.out(arg) } if newline { r.out("\n") } case "printf": if len(args) == 0 { r.errf("usage: printf format [arguments]\n") return 2 } format, args := args[0], args[1:] for { s, n, err := expand.Format(r.ecfg, format, args) if err != nil { r.errf("%v\n", err) return 1 } r.out(s) args = args[n:] if n == 0 || len(args) == 0 { break } } case "break", "continue": if !r.inLoop { r.errf("%s is only useful in a loop", name) break } enclosing := &r.breakEnclosing if name == "continue" { enclosing = &r.contnEnclosing } switch len(args) { case 0: *enclosing = 1 case 1: if n, err := strconv.Atoi(args[0]); err == nil { *enclosing = n break } fallthrough default: r.errf("usage: %s [n]\n", name) return 2 } case "pwd": evalSymlinks := false for len(args) > 0 { switch args[0] { case "-L": evalSymlinks = false case "-P": evalSymlinks = true default: r.errf("invalid option: %q\n", args[0]) return 2 } args = args[1:] } pwd := r.envGet("PWD") if evalSymlinks { var err error pwd, err = filepath.EvalSymlinks(pwd) if err != nil { r.setErr(err) return 1 } } r.outf("%s\n", pwd) case "cd": var path string switch len(args) { case 0: path = r.envGet("HOME") case 1: path = args[0] default: r.errf("usage: cd [dir]\n") return 2 } return r.changeDir(path) case "wait": if len(args) > 0 { panic("wait with args not handled yet") } err := r.bgShells.Wait() if _, ok := IsExitStatus(err); err != nil && !ok { r.setErr(err) } case "builtin": if len(args) < 1 { break } if !isBuiltin(args[0]) { return 1 } return r.builtinCode(ctx, pos, args[0], args[1:]) case "type": anyNotFound := false mode := "" fp := flagParser{remaining: args} for fp.more() { switch flag := fp.flag(); flag { case "-a", "-f", "-P", "--help": r.errf("command: NOT IMPLEMENTED\n") return 3 case "-p", "-t": mode = flag default: r.errf("command: invalid option %q\n", flag) return 2 } } args := fp.args() for _, arg := range args { if mode == "-p" { if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil { r.outf("%s\n", path) } else { anyNotFound = true } continue } if syntax.IsKeyword(arg) { if mode == "-t" { r.out("keyword\n") } else { r.outf("%s is a shell keyword\n", arg) } continue } if als, ok := r.alias[arg]; ok && r.opts[optExpandAliases] { var buf bytes.Buffer if len(als.args) > 0 { printer := syntax.NewPrinter() printer.Print(&buf, &syntax.CallExpr{ Args: als.args, }) } if als.blank { buf.WriteByte(' ') } if mode == "-t" { r.out("alias\n") } else { r.outf("%s is aliased to `%s'\n", arg, &buf) } continue } if _, ok := r.Funcs[arg]; ok { if mode == "-t" { r.out("function\n") } else { r.outf("%s is a function\n", arg) } continue } if isBuiltin(arg) { if mode == "-t" { r.out("builtin\n") } else { r.outf("%s is a shell builtin\n", arg) } continue } if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil { if mode == "-t" { r.out("file\n") } else { r.outf("%s is %s\n", arg, path) } continue } if mode != "-t" { r.errf("type: %s: not found\n", arg) } anyNotFound = true } if anyNotFound { return 1 } case "eval": src := strings.Join(args, " ") p := syntax.NewParser() file, err := p.Parse(strings.NewReader(src), "") if err != nil { r.errf("eval: %v\n", err) return 1 } r.stmts(ctx, file.Stmts) return r.exit case "source", ".": if len(args) < 1 { r.errf("%v: source: need filename\n", pos) return 2 } path, err := scriptFromPathDir(r.Dir, r.writeEnv, args[0]) if err != nil { // If the script was not found in PATH or there was any error, pass // the source path to the open handler so it has a chance to look // at files it manages (eg: virtual filesystem), and also allow // it to look for the sourced script in the current directory. path = args[0] } f, err := r.open(ctx, path, os.O_RDONLY, 0, false) if err != nil { r.errf("source: %v\n", err) return 1 } defer f.Close() p := syntax.NewParser() file, err := p.Parse(f, path) if err != nil { r.errf("source: %v\n", err) return 1 } // Keep the current versions of some fields we might modify. oldParams := r.Params oldSourceSetParams := r.sourceSetParams oldInSource := r.inSource // If we run "source file args...", set said args as parameters. // Otherwise, keep the current parameters. sourceArgs := len(args[1:]) > 0 if sourceArgs { r.Params = args[1:] r.sourceSetParams = false } // We want to track if the sourced file explicitly sets the // parameters. r.sourceSetParams = false r.inSource = true // know that we're inside a sourced script. r.stmts(ctx, file.Stmts) // If we modified the parameters and the sourced file didn't // explicitly set them, we restore the old ones. if sourceArgs && !r.sourceSetParams { r.Params = oldParams } r.sourceSetParams = oldSourceSetParams r.inSource = oldInSource if code, ok := r.err.(returnStatus); ok { r.err = nil return int(code) } return r.exit case "[": if len(args) == 0 || args[len(args)-1] != "]" { r.errf("%v: [: missing matching ]\n", pos) return 2 } args = args[:len(args)-1] fallthrough case "test": parseErr := false p := testParser{ rem: args, err: func(err error) { r.errf("%v: %v\n", pos, err) parseErr = true }, } p.next() expr := p.classicTest("[", false) if parseErr { return 2 } return oneIf(r.bashTest(ctx, expr, true) == "") case "exec": // TODO: Consider unix.Exec, i.e. actually replacing // the process. It's in theory what a shell should do, // but in practice it would kill the entire Go process // and it's not available on Windows. if len(args) == 0 { r.keepRedirs = true break } r.exitShell(ctx, 1) r.exec(ctx, args) return r.exit case "command": show := false fp := flagParser{remaining: args} for fp.more() { switch flag := fp.flag(); flag { case "-v": show = true default: r.errf("command: invalid option %q\n", flag) return 2 } } args := fp.args() if len(args) == 0 { break } if !show { if isBuiltin(args[0]) { return r.builtinCode(ctx, pos, args[0], args[1:]) } r.exec(ctx, args) return r.exit } last := 0 for _, arg := range args { last = 0 if r.Funcs[arg] != nil || isBuiltin(arg) { r.outf("%s\n", arg) } else if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil { r.outf("%s\n", path) } else { last = 1 } } return last case "dirs": for i := len(r.dirStack) - 1; i >= 0; i-- { r.outf("%s", r.dirStack[i]) if i > 0 { r.out(" ") } } r.out("\n") case "pushd": change := true if len(args) > 0 && args[0] == "-n" { change = false args = args[1:] } swap := func() string { oldtop := r.dirStack[len(r.dirStack)-1] top := r.dirStack[len(r.dirStack)-2] r.dirStack[len(r.dirStack)-1] = top r.dirStack[len(r.dirStack)-2] = oldtop return top } switch len(args) { case 0: if !change { break } if len(r.dirStack) < 2 { r.errf("pushd: no other directory\n") return 1 } newtop := swap() if code := r.changeDir(newtop); code != 0 { return code } r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) case 1: if change { if code := r.changeDir(args[0]); code != 0 { return code } r.dirStack = append(r.dirStack, r.Dir) } else { r.dirStack = append(r.dirStack, args[0]) swap() } r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) default: r.errf("pushd: too many arguments\n") return 2 } case "popd": change := true if len(args) > 0 && args[0] == "-n" { change = false args = args[1:] } switch len(args) { case 0: if len(r.dirStack) < 2 { r.errf("popd: directory stack empty\n") return 1 } oldtop := r.dirStack[len(r.dirStack)-1] r.dirStack = r.dirStack[:len(r.dirStack)-1] if change { newtop := r.dirStack[len(r.dirStack)-1] if code := r.changeDir(newtop); code != 0 { return code } } else { r.dirStack[len(r.dirStack)-1] = oldtop } r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) default: r.errf("popd: invalid argument\n") return 2 } case "return": if !r.inFunc && !r.inSource { r.errf("return: can only be done from a func or sourced script\n") return 1 } code := 0 switch len(args) { case 0: case 1: code = atoi(args[0]) default: r.errf("return: too many arguments\n") return 2 } r.setErr(returnStatus(code)) case "read": var prompt string raw := false fp := flagParser{remaining: args} for fp.more() { switch flag := fp.flag(); flag { case "-r": raw = true case "-p": prompt = fp.value() if prompt == "" { r.errf("read: -p: option requires an argument\n") return 2 } default: r.errf("read: invalid option %q\n", flag) return 2 } } args := fp.args() for _, name := range args { if !syntax.ValidName(name) { r.errf("read: invalid identifier %q\n", name) return 2 } } if prompt != "" { r.out(prompt) } line, err := r.readLine(raw) if err != nil { return 1 } if len(args) == 0 { args = append(args, "REPLY") } values := expand.ReadFields(r.ecfg, string(line), len(args), raw) for i, name := range args { val := "" if i < len(values) { val = values[i] } r.setVarString(name, val) } return 0 case "getopts": if len(args) < 2 { r.errf("getopts: usage: getopts optstring name [arg ...]\n") return 2 } optind, _ := strconv.Atoi(r.envGet("OPTIND")) if optind-1 != r.optState.argidx { if optind < 1 { optind = 1 } r.optState = getopts{argidx: optind - 1} } optstr := args[0] name := args[1] if !syntax.ValidName(name) { r.errf("getopts: invalid identifier: %q\n", name) return 2 } args = args[2:] if len(args) == 0 { args = r.Params } diagnostics := !strings.HasPrefix(optstr, ":") opt, optarg, done := r.optState.next(optstr, args) r.setVarString(name, string(opt)) r.delVar("OPTARG") switch { case opt == '?' && diagnostics && !done: r.errf("getopts: illegal option -- %q\n", optarg) case opt == ':' && diagnostics: r.errf("getopts: option requires an argument -- %q\n", optarg) default: if optarg != "" { r.setVarString("OPTARG", optarg) } } if optind-1 != r.optState.argidx { r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10)) } return oneIf(done) case "shopt": mode := "" posixOpts := false fp := flagParser{remaining: args} for fp.more() { switch flag := fp.flag(); flag { case "-s", "-u": mode = flag case "-o": posixOpts = true case "-p", "-q": panic(fmt.Sprintf("unhandled shopt flag: %s", flag)) default: r.errf("shopt: invalid option %q\n", flag) return 2 } } args := fp.args() if len(args) == 0 { if !posixOpts { for i, name := range bashOptsTable { r.printOptLine(name, r.opts[len(shellOptsTable)+i]) } break } for i, opt := range &shellOptsTable { r.printOptLine(opt.name, r.opts[i]) } break } for _, arg := range args { opt := r.optByName(arg, !posixOpts) if opt == nil { r.errf("shopt: invalid option name %q\n", arg) return 1 } switch mode { case "-s", "-u": *opt = mode == "-s" default: // "" r.printOptLine(arg, *opt) } } r.updateExpandOpts() case "alias": show := func(name string, als alias) { var buf bytes.Buffer if len(als.args) > 0 { printer := syntax.NewPrinter() printer.Print(&buf, &syntax.CallExpr{ Args: als.args, }) } if als.blank { buf.WriteByte(' ') } r.outf("alias %s='%s'\n", name, &buf) } if len(args) == 0 { for name, als := range r.alias { show(name, als) } } for _, name := range args { i := strings.IndexByte(name, '=') if i < 1 { // don't save an empty name als, ok := r.alias[name] if !ok { r.errf("alias: %q not found\n", name) continue } show(name, als) continue } // TODO: parse any CallExpr perhaps, or even any Stmt parser := syntax.NewParser() var words []*syntax.Word src := name[i+1:] if err := parser.Words(strings.NewReader(src), func(w *syntax.Word) bool { words = append(words, w) return true }); err != nil { r.errf("alias: could not parse %q: %v", src, err) continue } name = name[:i] if r.alias == nil { r.alias = make(map[string]alias) } r.alias[name] = alias{ args: words, blank: strings.TrimRight(src, " \t") != src, } } case "unalias": for _, name := range args { delete(r.alias, name) } case "trap": fp := flagParser{remaining: args} callback := "-" for fp.more() { switch flag := fp.flag(); flag { case "-l", "-p": r.errf("trap: %q: NOT IMPLEMENTED flag\n", flag) return 2 case "-": // default signal default: r.errf("trap: %q: invalid option\n", flag) r.errf("trap: usage: trap [-lp] [[arg] signal_spec ...]\n") return 2 } } args := fp.args() switch len(args) { case 0: // Print non-default signals if r.callbackExit != "" { r.outf("trap -- %q EXIT\n", r.callbackExit) } if r.callbackErr != "" { r.outf("trap -- %q ERR\n", r.callbackErr) } case 1: // assume it's a signal, the default will be restored default: callback = args[0] args = args[1:] } // For now, treat both empty and - the same since ERR and EXIT have no // default callback. if callback == "-" { callback = "" } for _, arg := range args { switch arg { case "ERR": r.callbackErr = callback case "EXIT": r.callbackExit = callback default: r.errf("trap: %s: invalid signal specification\n", arg) return 2 } } default: // "umask", "fg", "bg", panic(fmt.Sprintf("unhandled builtin: %s", name)) } return 0 } func (r *Runner) printOptLine(name string, enabled bool) { status := "off" if enabled { status = "on" } r.outf("%s\t%s\n", name, status) } func (r *Runner) readLine(raw bool) ([]byte, error) { if r.stdin == nil { return nil, errors.New("interp: can't read, there's no stdin") } var line []byte esc := false for { var buf [1]byte n, err := r.stdin.Read(buf[:]) if n > 0 { b := buf[0] switch { case !raw && b == '\\': line = append(line, b) esc = !esc case !raw && b == '\n' && esc: // line continuation line = line[len(line)-1:] esc = false case b == '\n': return line, nil default: line = append(line, b) esc = false } } if err == io.EOF && len(line) > 0 { return line, nil } if err != nil { return nil, err } } } func (r *Runner) changeDir(path string) int { if path == "" { path = "." } path = r.absPath(path) info, err := r.stat(path) if err != nil || !info.IsDir() { return 1 } if !hasPermissionToDir(info) { return 1 } r.Dir = path r.setVarString("OLDPWD", r.envGet("PWD")) r.setVarString("PWD", path) return 0 } func absPath(dir, path string) string { if path == "" { return "" } if !filepath.IsAbs(path) { path = filepath.Join(dir, path) } return filepath.Clean(path) } func (r *Runner) absPath(path string) string { return absPath(r.Dir, path) } // flagParser is used to parse builtin flags. // // It's similar to the getopts implementation, but with some key differences. // First, the API is designed for Go loops, making it easier to use directly. // Second, it doesn't require the awkward ":ab" syntax that getopts uses. // Third, it supports "-a" flags as well as "+a". type flagParser struct { current string remaining []string } func (p *flagParser) more() bool { if p.current != "" { // We're still parsing part of "-ab". return true } if len(p.remaining) == 0 { // Nothing left. p.remaining = nil return false } arg := p.remaining[0] if arg == "--" { // We explicitly stop parsing flags. p.remaining = p.remaining[1:] return false } if len(arg) == 0 || (arg[0] != '-' && arg[0] != '+') { // The next argument is not a flag. return false } // More flags to come. return true } func (p *flagParser) flag() string { arg := p.current if arg == "" { arg = p.remaining[0] p.remaining = p.remaining[1:] } else { p.current = "" } if len(arg) > 2 { // We have "-ab", so return "-a" and keep "-b". p.current = arg[:1] + arg[2:] arg = arg[:2] } return arg } func (p *flagParser) value() string { if len(p.remaining) == 0 { return "" } arg := p.remaining[0] p.remaining = p.remaining[1:] return arg } func (p *flagParser) args() []string { return p.remaining } type getopts struct { argidx int runeidx int } func (g *getopts) next(optstr string, args []string) (opt rune, optarg string, done bool) { if len(args) == 0 || g.argidx >= len(args) { return '?', "", true } arg := []rune(args[g.argidx]) if len(arg) < 2 || arg[0] != '-' || arg[1] == '-' { return '?', "", true } opts := arg[1:] opt = opts[g.runeidx] if g.runeidx+1 < len(opts) { g.runeidx++ } else { g.argidx++ g.runeidx = 0 } i := strings.IndexRune(optstr, opt) if i < 0 { // invalid option return '?', string(opt), false } if i+1 < len(optstr) && optstr[i+1] == ':' { if g.argidx >= len(args) { // missing argument return ':', string(opt), false } optarg = args[g.argidx] g.argidx++ g.runeidx = 0 } return opt, optarg, false } sh-3.4.3/interp/example_test.go000066400000000000000000000037001420425111700164520ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package interp_test import ( "context" "fmt" "io" "os" "runtime" "strings" "time" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" ) func Example() { src := ` foo=abc for i in 1 2 3; do foo+=$i done let bar=(2 + 3) echo $foo $bar echo $GLOBAL ` file, _ := syntax.NewParser().Parse(strings.NewReader(src), "") runner, _ := interp.New( interp.Env(expand.ListEnviron("GLOBAL=global_value")), interp.StdIO(nil, os.Stdout, os.Stdout), ) runner.Run(context.TODO(), file) // Output: // abc123 5 // global_value } func ExampleExecHandler() { src := "echo foo; join ! foo bar baz; missing-program bar" file, _ := syntax.NewParser().Parse(strings.NewReader(src), "") exec := func(ctx context.Context, args []string) error { hc := interp.HandlerCtx(ctx) if args[0] == "join" { fmt.Fprintln(hc.Stdout, strings.Join(args[2:], args[1])) return nil } if _, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]); err != nil { fmt.Printf("%s is not installed\n", args[0]) return interp.NewExitStatus(1) } return interp.DefaultExecHandler(2*time.Second)(ctx, args) } runner, _ := interp.New( interp.StdIO(nil, os.Stdout, os.Stdout), interp.ExecHandler(exec), ) runner.Run(context.TODO(), file) // Output: // foo // foo!bar!baz // missing-program is not installed } func ExampleOpenHandler() { src := "echo foo; echo bar >/dev/null" file, _ := syntax.NewParser().Parse(strings.NewReader(src), "") open := func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { if runtime.GOOS == "windows" && path == "/dev/null" { path = "NUL" } return interp.DefaultOpenHandler()(ctx, path, flag, perm) } runner, _ := interp.New( interp.StdIO(nil, os.Stdout, os.Stdout), interp.OpenHandler(open), ) runner.Run(context.TODO(), file) // Output: // foo } sh-3.4.3/interp/handler.go000066400000000000000000000175251420425111700154070ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package interp import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "syscall" "time" "mvdan.cc/sh/v3/expand" ) // HandlerCtx returns HandlerContext value stored in ctx. // It panics if ctx has no HandlerContext stored. func HandlerCtx(ctx context.Context) HandlerContext { hc, ok := ctx.Value(handlerCtxKey{}).(HandlerContext) if !ok { panic("interp.HandlerCtx: no HandlerContext in ctx") } return hc } type handlerCtxKey struct{} // HandlerContext is the data passed to all the handler functions via a context value. // It contains some of the current state of the Runner. type HandlerContext struct { // Env is a read-only version of the interpreter's environment, // including environment variables, global variables, and local function // variables. Env expand.Environ // Dir is the interpreter's current directory. Dir string // Stdin is the interpreter's current standard input reader. Stdin io.Reader // Stdout is the interpreter's current standard output writer. Stdout io.Writer // Stderr is the interpreter's current standard error writer. Stderr io.Writer } // ExecHandlerFunc is a handler which executes simple command. It is // called for all CallExpr nodes where the first argument is neither a // declared function nor a builtin. // // Returning nil error sets commands exit status to 0. Other exit statuses // can be set with NewExitStatus. Any other error will halt an interpreter. type ExecHandlerFunc func(ctx context.Context, args []string) error // DefaultExecHandler returns an ExecHandlerFunc used by default. // It finds binaries in PATH and executes them. // When context is cancelled, interrupt signal is sent to running processes. // KillTimeout is a duration to wait before sending kill signal. // A negative value means that a kill signal will be sent immediately. // On Windows, the kill signal is always sent immediately, // because Go doesn't currently support sending Interrupt on Windows. // Runner.New sets killTimeout to 2 seconds by default. func DefaultExecHandler(killTimeout time.Duration) ExecHandlerFunc { return func(ctx context.Context, args []string) error { hc := HandlerCtx(ctx) path, err := LookPathDir(hc.Dir, hc.Env, args[0]) if err != nil { fmt.Fprintln(hc.Stderr, err) return NewExitStatus(127) } cmd := exec.Cmd{ Path: path, Args: args, Env: execEnv(hc.Env), Dir: hc.Dir, Stdin: hc.Stdin, Stdout: hc.Stdout, Stderr: hc.Stderr, } err = cmd.Start() if err == nil { if done := ctx.Done(); done != nil { go func() { <-done if killTimeout <= 0 || runtime.GOOS == "windows" { _ = cmd.Process.Signal(os.Kill) return } // TODO: don't temporarily leak this goroutine // if the program stops itself with the // interrupt. go func() { time.Sleep(killTimeout) _ = cmd.Process.Signal(os.Kill) }() _ = cmd.Process.Signal(os.Interrupt) }() } err = cmd.Wait() } switch x := err.(type) { case *exec.ExitError: // started, but errored - default to 1 if OS // doesn't have exit statuses if status, ok := x.Sys().(syscall.WaitStatus); ok { if status.Signaled() { if ctx.Err() != nil { return ctx.Err() } return NewExitStatus(uint8(128 + status.Signal())) } return NewExitStatus(uint8(status.ExitStatus())) } return NewExitStatus(1) case *exec.Error: // did not start fmt.Fprintf(hc.Stderr, "%v\n", err) return NewExitStatus(127) default: return err } } } func checkStat(dir, file string, checkExec bool) (string, error) { if !filepath.IsAbs(file) { file = filepath.Join(dir, file) } info, err := os.Stat(file) if err != nil { return "", err } m := info.Mode() if m.IsDir() { return "", fmt.Errorf("is a directory") } if checkExec && runtime.GOOS != "windows" && m&0o111 == 0 { return "", fmt.Errorf("permission denied") } return file, nil } func winHasExt(file string) bool { i := strings.LastIndex(file, ".") if i < 0 { return false } return strings.LastIndexAny(file, `:\/`) < i } // findExecutable returns the path to an existing executable file. func findExecutable(dir, file string, exts []string) (string, error) { if len(exts) == 0 { // non-windows return checkStat(dir, file, true) } if winHasExt(file) { if file, err := checkStat(dir, file, true); err == nil { return file, nil } } for _, e := range exts { f := file + e if f, err := checkStat(dir, f, true); err == nil { return f, nil } } return "", fmt.Errorf("not found") } // findFile returns the path to an existing file. func findFile(dir, file string, _ []string) (string, error) { return checkStat(dir, file, false) } // LookPath is deprecated. See LookPathDir. func LookPath(env expand.Environ, file string) (string, error) { return LookPathDir(env.Get("PWD").String(), env, file) } // LookPathDir is similar to os/exec.LookPath, with the difference that it uses the // provided environment. env is used to fetch relevant environment variables // such as PWD and PATH. // // If no error is returned, the returned path must be valid. func LookPathDir(cwd string, env expand.Environ, file string) (string, error) { return lookPathDir(cwd, env, file, findExecutable) } // findAny defines a function to pass to lookPathDir. type findAny = func(dir string, file string, exts []string) (string, error) func lookPathDir(cwd string, env expand.Environ, file string, find findAny) (string, error) { if find == nil { panic("no find function found") } pathList := filepath.SplitList(env.Get("PATH").String()) if len(pathList) == 0 { pathList = []string{""} } chars := `/` if runtime.GOOS == "windows" { chars = `:\/` } exts := pathExts(env) if strings.ContainsAny(file, chars) { return find(cwd, file, exts) } for _, elem := range pathList { var path string switch elem { case "", ".": // otherwise "foo" won't be "./foo" path = "." + string(filepath.Separator) + file default: path = filepath.Join(elem, file) } if f, err := find(cwd, path, exts); err == nil { return f, nil } } return "", fmt.Errorf("%q: executable file not found in $PATH", file) } // scriptFromPathDir is similar to LookPathDir, with the difference that it looks // for both executable and non-executable files. func scriptFromPathDir(cwd string, env expand.Environ, file string) (string, error) { return lookPathDir(cwd, env, file, findFile) } func pathExts(env expand.Environ) []string { if runtime.GOOS != "windows" { return nil } pathext := env.Get("PATHEXT").String() if pathext == "" { return []string{".com", ".exe", ".bat", ".cmd"} } var exts []string for _, e := range strings.Split(strings.ToLower(pathext), `;`) { if e == "" { continue } if e[0] != '.' { e = "." + e } exts = append(exts, e) } return exts } // OpenHandlerFunc is a handler which opens files. It is // called for all files that are opened directly by the shell, such as // in redirects. Files opened by executed programs are not included. // // The path parameter may be relative to the current directory, which can be // fetched via HandlerCtx. // // Use a return error of type *os.PathError to have the error printed to // stderr and the exit status set to 1. If the error is of any other type, the // interpreter will come to a stop. type OpenHandlerFunc func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) // DefaultOpenHandler returns an OpenHandlerFunc used by default. It uses os.OpenFile to open files. func DefaultOpenHandler() OpenHandlerFunc { return func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { mc := HandlerCtx(ctx) if path != "" && !filepath.IsAbs(path) { path = filepath.Join(mc.Dir, path) } return os.OpenFile(path, flag, perm) } } sh-3.4.3/interp/handler_test.go000066400000000000000000000152111420425111700164340ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package interp import ( "bufio" "bytes" "context" "fmt" "io" "os" "runtime" "strconv" "strings" "sync" "syscall" "testing" "time" "mvdan.cc/sh/v3/syntax" ) func blacklistBuiltinExec(name string) ExecHandlerFunc { return func(ctx context.Context, args []string) error { if args[0] == name { return fmt.Errorf("%s: blacklisted builtin", name) } return testExecHandler(ctx, args) } } func blacklistAllExec(ctx context.Context, args []string) error { return fmt.Errorf("blacklisted: %s", args[0]) } func blacklistNondevOpen(ctx context.Context, path string, flags int, mode os.FileMode) (io.ReadWriteCloser, error) { if path != "/dev/null" { return nil, fmt.Errorf("non-dev: %s", path) } return testOpenHandler(ctx, path, flags, mode) } // runnerCtx allows us to give handler functions access to the Runner, if needed. var runnerCtx = new(int) func execBuiltin(ctx context.Context, args []string) error { runner, ok := ctx.Value(runnerCtx).(*Runner) if ok && runner.Exited() { return fmt.Errorf("exec builtin: %s", args[0]) } return nil } var modCases = []struct { name string exec ExecHandlerFunc open OpenHandlerFunc src string want string }{ { name: "ExecBlacklist", exec: blacklistBuiltinExec("sleep"), src: "echo foo; sleep 1", want: "foo\nsleep: blacklisted builtin", }, { name: "ExecWhitelist", exec: blacklistBuiltinExec("faa"), src: "a=$(echo foo | sed 's/o/a/g'); echo $a; $a args", want: "faa\nfaa: blacklisted builtin", }, { name: "ExecSubshell", exec: blacklistAllExec, src: "(malicious)", want: "blacklisted: malicious", }, { name: "ExecPipe", exec: blacklistAllExec, src: "malicious | echo foo", want: "foo\nblacklisted: malicious", }, { name: "ExecCmdSubst", exec: blacklistAllExec, src: "a=$(malicious)", want: "blacklisted: malicious\nexit status 1", }, { name: "ExecBackground", exec: blacklistAllExec, src: "{ malicious; true; } & { malicious; true; } & wait", want: "blacklisted: malicious", }, { name: "ExecBuiltin", exec: execBuiltin, src: "exec /bin/sh", want: "exec builtin: /bin/sh", }, { name: "OpenForbidNonDev", open: blacklistNondevOpen, src: "echo foo >/dev/null; echo bar >/tmp/x", want: "non-dev: /tmp/x", }, } func TestRunnerHandlers(t *testing.T) { t.Parallel() p := syntax.NewParser() for _, tc := range modCases { t.Run(tc.name, func(t *testing.T) { file := parse(t, p, tc.src) var cb concBuffer r, err := New(StdIO(nil, &cb, &cb)) if tc.exec != nil { ExecHandler(tc.exec)(r) } if tc.open != nil { OpenHandler(tc.open)(r) } if err != nil { t.Fatal(err) } ctx := context.WithValue(context.Background(), runnerCtx, r) if err := r.Run(ctx, file); err != nil { cb.WriteString(err.Error()) } got := cb.String() if got != tc.want { t.Fatalf("want:\n%s\ngot:\n%s", tc.want, got) } }) } } type readyBuffer struct { buf bytes.Buffer seenReady sync.WaitGroup } func (b *readyBuffer) Write(p []byte) (n int, err error) { if string(p) == "ready\n" { b.seenReady.Done() return len(p), nil } return b.buf.Write(p) } func TestKillTimeout(t *testing.T) { if testing.Short() { t.Skip("sleeps and timeouts are slow") } if runtime.GOOS == "windows" { t.Skip("skipping trap tests on windows") } t.Parallel() tests := []struct { src string want string killTimeout time.Duration forcedKill bool }{ // killed immediately { `sh -c "trap 'echo trapped; exit 0' INT; echo ready; for i in \$(seq 1 100); do sleep 0.01; done"`, "", -1, true, }, // interrupted first, and stops itself in time { `sh -c "trap 'echo trapped; exit 0' INT; echo ready; for i in \$(seq 1 100); do sleep 0.01; done"`, "trapped\n", time.Second, false, }, // interrupted first, but does not stop itself in time { `sh -c "trap 'echo trapped; for i in \$(seq 1 100); do sleep 0.01; done' INT; echo ready; for i in \$(seq 1 100); do sleep 0.01; done"`, "trapped\n", 20 * time.Millisecond, true, }, } for _, test := range tests { test := test t.Run("", func(t *testing.T) { t.Parallel() file := parse(t, nil, test.src) attempt := 0 for { var rbuf readyBuffer rbuf.seenReady.Add(1) ctx, cancel := context.WithCancel(context.Background()) r, err := New( StdIO(nil, &rbuf, &rbuf), ExecHandler(DefaultExecHandler(test.killTimeout)), ) if err != nil { t.Fatal(err) } go func() { rbuf.seenReady.Wait() cancel() }() err = r.Run(ctx, file) if test.forcedKill { if _, ok := IsExitStatus(err); ok || err == nil { t.Error("command was not force-killed") } } else { if err != nil && err != context.Canceled && err != context.DeadlineExceeded { t.Errorf("execution errored: %v", err) } } got := rbuf.buf.String() if got != test.want { if attempt < 3 && got == "" && test.killTimeout > 0 { attempt++ test.killTimeout *= 2 continue } t.Fatalf("want:\n%s\ngot:\n%s", test.want, got) } break } }) } } func TestKillSignal(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping signal tests on windows") } tests := []struct { signal os.Signal want error }{ {syscall.SIGINT, NewExitStatus(130)}, // 128 + 2 {syscall.SIGKILL, NewExitStatus(137)}, // 128 + 9 {syscall.SIGTERM, NewExitStatus(143)}, // 128 + 15 } // pid_and_hang is implemented in TestMain; we use it to have the // interpreter spawn a process, and easily grab its PID to send it a // signal directly. The program prints its PID and hangs forever. file := parse(t, nil, "GOSH_CMD=pid_and_hang $GOSH_PROG") for _, test := range tests { test := test t.Run(fmt.Sprintf("signal-%d", test.signal), func(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() outReader, outWriter := io.Pipe() stderr := new(bytes.Buffer) r, _ := New(StdIO(nil, outWriter, stderr)) errch := make(chan error, 1) go func() { errch <- r.Run(ctx, file) outWriter.Close() }() br := bufio.NewReader(outReader) line, err := br.ReadString('\n') if err != nil { t.Fatal(err) } pid, err := strconv.Atoi(strings.TrimSpace(line)) if err != nil { t.Fatal(err) } proc, err := os.FindProcess(pid) if err != nil { t.Fatal(err) } if err := proc.Signal(test.signal); err != nil { t.Fatal(err) } if got := <-errch; got != test.want { t.Fatalf("want error %v, got %v. stderr: %s", test.want, got, stderr) } }) } } sh-3.4.3/interp/interp_test.go000066400000000000000000002456031420425111700163320ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package interp import ( "bytes" "context" "fmt" "io" "math/bits" "os" "os/exec" "path/filepath" "regexp" "runtime" "sort" "strconv" "strings" "sync" "testing" "time" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) // runnerRunTimeout is the context timeout used by any tests calling Runner.Run. // The timeout saves us from hangs or burning too much CPU if there are bugs. // All the test cases are designed to be inexpensive and stop in a very short // amount of time, so 5s should be plenty even for busy machines. const runnerRunTimeout = 5 * time.Second // Some program which should be in $PATH. Needs to run before runTests is // initialized (so an init function wouldn't work), because runTest uses it. var pathProg = func() string { if runtime.GOOS == "windows" { return "cmd" } return "sh" }() func parse(tb testing.TB, parser *syntax.Parser, src string) *syntax.File { if parser == nil { parser = syntax.NewParser() } file, err := parser.Parse(strings.NewReader(src), "") if err != nil { tb.Fatal(err) } return file } func BenchmarkRun(b *testing.B) { b.ReportAllocs() b.StopTimer() src := ` echo a b c d echo ./$foo/etc $(echo foo bar) foo="bar" x=y : fn() { local a=b for i in 1 2 3; do echo $i | cat done } [[ $foo == bar ]] && fn echo a{b,c}d *.go let i=(2 + 3) ` file := parse(b, nil, src) r, _ := New() ctx := context.Background() b.StartTimer() for i := 0; i < b.N; i++ { r.Reset() if err := r.Run(ctx, file); err != nil { b.Fatal(err) } } } var hasBash50 bool func TestMain(m *testing.M) { if os.Getenv("GOSH_PROG") != "" { switch os.Getenv("GOSH_CMD") { case "pid_and_hang": fmt.Println(os.Getpid()) time.Sleep(time.Hour) case "foo_null_bar": fmt.Println("foo\x00bar") os.Exit(1) case "lookpath": _, err := exec.LookPath(pathProg) if err != nil { fmt.Println(err) os.Exit(1) } fmt.Printf("%s found\n", pathProg) os.Exit(0) } r := strings.NewReader(os.Args[1]) file, err := syntax.NewParser().Parse(r, "") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } runner, _ := New( StdIO(os.Stdin, os.Stdout, os.Stderr), OpenHandler(testOpenHandler), ExecHandler(testExecHandler), ) ctx := context.Background() if err := runner.Run(ctx, file); err != nil { if status, ok := IsExitStatus(err); ok { os.Exit(int(status)) } fmt.Fprintln(os.Stderr, err) os.Exit(1) } os.Exit(0) } prog, err := os.Executable() if err != nil { panic(err) } os.Setenv("GOSH_PROG", prog) // Set the locale to computer-friendly English and UTF-8. // Some systems like Arch miss C.UTF8, so fall back to the US English locale. if out, _ := exec.Command("locale", "-a").Output(); strings.Contains( strings.ToLower(string(out)), "c.utf", ) { os.Setenv("LANGUAGE", "C.UTF-8") os.Setenv("LC_ALL", "C.UTF-8") } else { os.Setenv("LANGUAGE", "en_US.UTF-8") os.Setenv("LC_ALL", "en_US.UTF-8") } os.Unsetenv("CDPATH") hasBash50 = checkBash() wd, err := os.Getwd() if err != nil { panic(err) } os.Setenv("GO_TEST_DIR", wd) os.Setenv("INTERP_GLOBAL", "value") os.Setenv("MULTILINE_INTERP_GLOBAL", "\nwith\nnewlines\n\n") // Double check that env vars on Windows are case insensitive. if runtime.GOOS == "windows" { os.Setenv("mixedCase_INTERP_GLOBAL", "value") } else { os.Setenv("MIXEDCASE_INTERP_GLOBAL", "value") } os.Setenv("PATH_PROG", pathProg) // To print env vars. Only a builtin on Windows. if runtime.GOOS == "windows" { os.Setenv("ENV_PROG", "cmd /c set") } else { os.Setenv("ENV_PROG", "env") } for _, s := range []string{"a", "b", "c", "d", "foo", "bar"} { os.Unsetenv(s) } exit := m.Run() os.Exit(exit) } func checkBash() bool { out, err := exec.Command("bash", "-c", "echo -n $BASH_VERSION").Output() if err != nil { return false } return strings.HasPrefix(string(out), "5.1") } // concBuffer wraps a bytes.Buffer in a mutex so that concurrent writes // to it don't upset the race detector. type concBuffer struct { buf bytes.Buffer sync.Mutex } func (c *concBuffer) Write(p []byte) (int, error) { c.Lock() n, err := c.buf.Write(p) c.Unlock() return n, err } func (c *concBuffer) WriteString(s string) (int, error) { c.Lock() n, err := c.buf.WriteString(s) c.Unlock() return n, err } func (c *concBuffer) String() string { c.Lock() s := c.buf.String() c.Unlock() return s } func (c *concBuffer) Reset() { c.Lock() c.buf.Reset() c.Unlock() } type runTest struct { in, want string } var runTests = []runTest{ // no-op programs {"", ""}, {"true", ""}, {":", ""}, {"exit", ""}, {"exit 0", ""}, {"{ :; }", ""}, {"(:)", ""}, // exit status codes {"exit 1", "exit status 1"}, {"exit -1", "exit status 255"}, {"exit 300", "exit status 44"}, {"false", "exit status 1"}, {"false foo", "exit status 1"}, {"! false", ""}, {"true foo", ""}, {": foo", ""}, {"! true", "exit status 1"}, {"false; true", ""}, {"false; exit", "exit status 1"}, {"exit; echo foo", ""}, {"exit 0; echo foo", ""}, {"printf", "usage: printf format [arguments]\nexit status 2 #JUSTERR"}, {"break", "break is only useful in a loop #JUSTERR"}, {"continue", "continue is only useful in a loop #JUSTERR"}, {"cd a b", "usage: cd [dir]\nexit status 2 #JUSTERR"}, {"shift a", "usage: shift [n]\nexit status 2 #JUSTERR"}, { "shouldnotexist", "\"shouldnotexist\": executable file not found in $PATH\nexit status 127 #JUSTERR", }, { "for i in 1; do continue a; done", "usage: continue [n]\nexit status 2 #JUSTERR", }, { "for i in 1; do break a; done", "usage: break [n]\nexit status 2 #JUSTERR", }, {"false; a=b", ""}, {"false; false &", ""}, // we don't need to follow bash error strings {"exit a", "invalid exit status code: \"a\"\nexit status 2 #JUSTERR"}, {"exit 1 2", "exit cannot take multiple arguments\nexit status 1 #JUSTERR"}, // echo {"echo", "\n"}, {"echo a b c", "a b c\n"}, {"echo -n foo", "foo"}, {`echo -e '\t'`, "\t\n"}, {`echo -E '\t'`, "\\t\n"}, {"echo -x foo", "-x foo\n"}, {"echo -e -x -e foo", "-x -e foo\n"}, // printf {"printf foo", "foo"}, {"printf %%", "%"}, {"printf %", "missing format char\nexit status 1 #JUSTERR"}, {"printf %; echo foo", "missing format char\nfoo\n #IGNORE"}, {"printf %1", "missing format char\nexit status 1 #JUSTERR"}, {"printf %+", "missing format char\nexit status 1 #JUSTERR"}, {"printf %B foo", "invalid format char: B\nexit status 1 #JUSTERR"}, {"printf %12-s foo", "invalid format char: -\nexit status 1 #JUSTERR"}, {"printf ' %s \n' bar", " bar \n"}, {"printf '\\A'", "\\A"}, {"printf %s foo", "foo"}, {"printf %s", ""}, {"printf %d,%i 3 4", "3,4"}, {"printf %d", "0"}, {"printf %d,%d 010 0x10", "8,16"}, {"printf %c,%c,%c foo àa", "f,\xc3,\x00"}, // TODO: use a rune? {"printf %3s a", " a"}, {"printf %3i 1", " 1"}, {"printf %+i%+d 1 -3", "+1-3"}, {"printf %-5x 10", "a "}, {"printf %02x 1", "01"}, {"printf 'a% 5s' a", "a a"}, {"printf 'nofmt' 1 2 3", "nofmt"}, {"printf '%d_' 1 2 3", "1_2_3_"}, {"printf '%02d %02d\n' 1 2 3", "01 02\n03 00\n"}, // words and quotes {"echo foo ", "foo\n"}, {"echo ' foo '", " foo \n"}, {`echo " foo "`, " foo \n"}, {`echo a'b'c"d"e`, "abcde\n"}, {`a=" b c "; echo $a`, "b c\n"}, {`a=" b c "; echo "$a"`, " b c \n"}, {`echo "$(echo ' b c ')"`, " b c \n"}, {"echo ''", "\n"}, {`$(echo)`, ""}, {`echo -n '\\'`, `\\`}, {`echo -n "\\"`, `\`}, {`set -- a b c; x="$@"; echo "$x"`, "a b c\n"}, {`set -- b c; echo a"$@"d`, "ab cd\n"}, {`count() { echo $#; }; set --; count "$@"`, "0\n"}, {`count() { echo $#; }; set -- ""; count "$@"`, "1\n"}, {`count() { echo $#; }; set -- ""; shift; count "$@"`, "0\n"}, {`count() { echo $#; }; a=(); count "${a[@]}"`, "0\n"}, {`count() { echo $#; }; a=(""); count "${a[@]}"`, "1\n"}, {`echo $1 $3; set -- a b c; echo $1 $3`, "\na c\n"}, {`[[ $0 == "bash" || $0 == "gosh" ]]`, ""}, // dollar quotes {`echo $'foo\nbar'`, "foo\nbar\n"}, {`echo $'\r\t\\'`, "\r\t\\\n"}, {`echo $"foo\nbar"`, "foo\\nbar\n"}, {`echo $'%s'`, "%s\n"}, {`a=$'\r\t\\'; echo "$a"`, "\r\t\\\n"}, {`a=$"foo\nbar"; echo "$a"`, "foo\\nbar\n"}, {`echo $'\a\b\e\E\f\v'`, "\a\b\x1b\x1b\f\v\n"}, {`echo $'\\\'\"\?'`, "\\'\"?\n"}, {`echo $'\1\45\12345\777\9'`, "\x01%S45\xff\\9\n"}, {`echo $'\x\xf\x09\xAB'`, "\\x\x0f\x09\xab\n"}, {`echo $'\u\uf\u09\uABCD\u00051234'`, "\\u\u000f\u0009\uabcd\u00051234\n"}, {`echo $'\U\Uf\U09\UABCD\U00051234'`, "\\U\u000f\u0009\uabcd\U00051234\n"}, { "echo 'foo\x00bar'", "foobar\n", }, { "echo \"foo\x00bar\"", "foobar\n", }, { "echo $'foo\x00bar'", "foobar\n", }, { "echo $'foo\\x00bar'", "foo\n", }, { "echo $'foo\\xbar'", "foo\xbar\n", }, { "a='foo\x00bar'; eval \"echo -n ${a} ${a@Q}\";", "foobar foobar", }, { "a=$'foo\\x00bar'; eval \"echo -n ${a} ${a@Q}\";", "foo foo", }, { "i\x00f true; then echo foo\x00; \x00fi", "foo\n", }, { "echo $(GOSH_CMD=foo_null_bar $GOSH_PROG)", "foobar\n #IGNORE", }, // See the TODO where FOO_NULL_BAR is set. // { // "echo $FOO_NULL_BAR \"${FOO_NULL_BAR}\"", // "foo\n", // }, // escaped chars {"echo a\\b", "ab\n"}, {"echo a\\ b", "a b\n"}, {"echo \\$a", "$a\n"}, {"echo \"a\\b\"", "a\\b\n"}, {"echo 'a\\b'", "a\\b\n"}, {"echo \"a\\\nb\"", "ab\n"}, {"echo 'a\\\nb'", "a\\\nb\n"}, {`echo "\""`, "\"\n"}, {`echo \\`, "\\\n"}, {`echo \\\\`, "\\\\\n"}, {`echo \`, "\n"}, // vars {"foo=bar; echo $foo", "bar\n"}, {"foo=bar foo=etc; echo $foo", "etc\n"}, {"foo=bar; foo=etc; echo $foo", "etc\n"}, {"foo=bar; foo=; echo $foo", "\n"}, {"unset foo; echo $foo", "\n"}, {"foo=bar; unset foo; echo $foo", "\n"}, {"echo $INTERP_GLOBAL", "value\n"}, {"INTERP_GLOBAL=; echo $INTERP_GLOBAL", "\n"}, {"unset INTERP_GLOBAL; echo $INTERP_GLOBAL", "\n"}, {"echo $MIXEDCASE_INTERP_GLOBAL", "value\n"}, {"foo=bar; foo=x true; echo $foo", "bar\n"}, {"foo=bar; foo=x true; echo $foo", "bar\n"}, {"foo=bar; $ENV_PROG | grep '^foo='", "exit status 1"}, {"foo=bar $ENV_PROG | grep '^foo='", "foo=bar\n"}, {"foo=a foo=b $ENV_PROG | grep '^foo='", "foo=b\n"}, {"$ENV_PROG | grep '^INTERP_GLOBAL='", "INTERP_GLOBAL=value\n"}, {"INTERP_GLOBAL=new; $ENV_PROG | grep '^INTERP_GLOBAL='", "INTERP_GLOBAL=new\n"}, {"INTERP_GLOBAL=; $ENV_PROG | grep '^INTERP_GLOBAL='", "INTERP_GLOBAL=\n"}, {"unset INTERP_GLOBAL; $ENV_PROG | grep '^INTERP_GLOBAL='", "exit status 1"}, {"a=b; a+=c x+=y; echo $a $x", "bc y\n"}, {`a=" x y"; b=$a c="$a"; echo $b; echo $c`, "x y\nx y\n"}, {`a=" x y"; b=$a c="$a"; echo "$b"; echo "$c"`, " x y\n x y\n"}, {`arr=("foo" "bar" "lala" "foobar"); echo ${arr[@]:2}; echo ${arr[*]:2}`, "lala foobar\nlala foobar\n"}, {`arr=("foo" "bar" "lala" "foobar"); echo ${arr[@]:2:4}; echo ${arr[*]:1:4}`, "lala foobar\nbar lala foobar\n"}, {`arr=("foo" "bar"); echo ${arr[@]}; echo ${arr[*]}`, "foo bar\nfoo bar\n"}, {`arr=("foo"); echo ${arr[@]:99}`, "\n"}, {`echo ${arr[@]:1:99}; echo ${arr[*]:1:99}`, "\n\n"}, {`arr=(0 1 2 3 4 5 6 7 8 9 0 a b c d e f g h); echo ${arr[@]:3:4}`, "3 4 5 6\n"}, {`echo ${foo[@]}; echo ${foo[*]}`, "\n\n"}, // TODO: reenable once we figure out the broken pipe error //{`$ENV_PROG | while read line; do if test -z "$line"; then echo empty; fi; break; done`, ""}, // never begin with an empty element // inline variables have special scoping { "f() { echo $inline; inline=bar true; echo $inline; }; inline=foo f", "foo\nfoo\n", }, {"v=x; read v <<< 'y'; echo $v", "y\n"}, {"v=x; v=inline read v <<< 'y'; echo $v", "x\n"}, {"v=x; v=inline unset v; echo $v", "x\n"}, {"v=x; echo 'v=y' >f; v=inline source f; echo $v", "x\n"}, {"declare -n v=v2; v=inline true; echo $v $v2", "\n"}, {"f() { echo $v; }; v=x; v=y f; f", "y\nx\n"}, {"f() { echo $v; }; v=x; v+=y f; f", "xy\nx\n"}, {"f() { echo $v; }; declare -n v=v2; v2=x; v=y f; f", "y\nx\n"}, {"f() { echo ${v[@]}; }; v=(e1 e2); v=y f; f", "y\ne1 e2\n"}, // special vars {"echo $?; false; echo $?", "0\n1\n"}, {"for i in 1 2; do\necho $LINENO\necho $LINENO\ndone", "2\n3\n2\n3\n"}, {"[[ -n $$ && $$ -gt 0 ]]", ""}, {"[[ $$ -eq $PPID ]]", "exit status 1"}, // var manipulation {"echo ${#a} ${#a[@]}", "0 0\n"}, {"a=bar; echo ${#a} ${#a[@]}", "3 1\n"}, {"a=世界; echo ${#a}", "2\n"}, {"a=(a bcd); echo ${#a} ${#a[@]} ${#a[*]} ${#a[1]}", "1 2 2 3\n"}, { "a=($(echo a bcd)); echo ${#a} ${#a[@]} ${#a[*]} ${#a[1]}", "1 2 2 3\n", }, { "a=([0]=$(echo a b) $(echo c d)); echo ${#a} ${#a[@]} ${#a[*]} ${#a[0]}", "3 3 3 3\n", }, {"set -- a bc; echo ${#@} ${#*} $#", "2 2 2\n"}, { "echo ${!a}; echo more", "invalid indirect expansion\nexit status 1 #JUSTERR", }, { "a=b; echo ${!a}; b=c; echo ${!a}", "\nc\n", }, { "a=foo; echo ${a:1}; echo ${a: -1}; echo ${a: -10}; echo ${a:5}", "oo\no\n\n\n", }, { "a=foo; echo ${a::2}; echo ${a::-1}; echo ${a: -10}; echo ${a::5}", "fo\nfo\n\nfoo\n", }, { "a=abc; echo ${a:1:1}", "b\n", }, { "a=foo; echo ${a/no/x} ${a/o/i} ${a//o/i} ${a/fo/}", "foo fio fii o\n", }, { "a=foo; echo ${a/*/xx} ${a//?/na} ${a/o*}", "xx nanana f\n", }, { "a=12345; echo ${a//[42]} ${a//[^42]} ${a//[!42]}", "135 24 24\n", }, {"a=0123456789; echo ${a//[1-35-8]}", "049\n"}, {"a=]abc]; echo ${a//[]b]}", "ac\n"}, {"a=-abc-; echo ${a//[-b]}", "ac\n"}, {`a='x\y'; echo ${a//\\}`, "xy\n"}, {"a=']'; echo ${a//[}", "]\n"}, {"a=']'; echo ${a//[]}", "]\n"}, {"a=']'; echo ${a//[]]}", "\n"}, {"a='['; echo ${a//[[]}", "\n"}, {"a=']'; echo ${a//[xy}", "]\n"}, {"a='abc123'; echo ${a//[[:digit:]]}", "abc\n"}, {"a='[[:wrong:]]'; echo ${a//[[:wrong:]]}", "[[:wrong:]]\n"}, {"a='[[:wrong:]]'; echo ${a//[[:}", "[[:wrong:]]\n"}, {"a='abcx1y'; echo ${a//x[[:digit:]]y}", "abc\n"}, {`a=xyz; echo "${a/y/a b}"`, "xa bz\n"}, {"a='foo/bar'; echo ${a//o*a/}", "fr\n"}, { "echo ${a:-b}; echo $a; a=; echo ${a:-b}; a=c; echo ${a:-b}", "b\n\nb\nc\n", }, { "echo ${#:-never} ${?:-never} ${LINENO:-never}", "0 0 1\n", }, { "echo ${a-b}; echo $a; a=; echo ${a-b}; a=c; echo ${a-b}", "b\n\n\nc\n", }, { "echo ${a:=b}; echo $a; a=; echo ${a:=b}; a=c; echo ${a:=b}", "b\nb\nb\nc\n", }, { "echo ${a=b}; echo $a; a=; echo ${a=b}; a=c; echo ${a=b}", "b\nb\n\nc\n", }, { "echo ${a:+b}; echo $a; a=; echo ${a:+b}; a=c; echo ${a:+b}", "\n\n\nb\n", }, { "echo ${a+b}; echo $a; a=; echo ${a+b}; a=c; echo ${a+b}", "\n\nb\nb\n", }, { "a=b; echo ${a:?err1}; a=; echo ${a:?err2}; unset a; echo ${a:?err3}", "b\na: err2\nexit status 1 #JUSTERR", }, { "a=b; echo ${a?err1}; a=; echo ${a?err2}; unset a; echo ${a?err3}", "b\n\na: err3\nexit status 1 #JUSTERR", }, { "echo ${a:?%s}", "a: %s\nexit status 1 #JUSTERR", }, { "x=aaabccc; echo ${x#*a}; echo ${x##*a}", "aabccc\nbccc\n", }, { "x=(__a _b c_); echo ${x[@]#_}", "_a b c_\n", }, { "x=(a__ b_ _c); echo ${x[@]%%_}", "a_ b _c\n", }, { "x=aaabccc; echo ${x%c*}; echo ${x%%c*}", "aaabcc\naaab\n", }, { "x=aaabccc; echo ${x%%[bc}", "aaabccc\n", }, { "a='àÉñ bAr'; echo ${a^}; echo ${a^^}", "ÀÉñ bAr\nÀÉÑ BAR\n", }, { "a='àÉñ bAr'; echo ${a,}; echo ${a,,}", "àÉñ bAr\nàéñ bar\n", }, { "a='àÉñ bAr'; echo ${a^?}; echo ${a^^[br]}", "ÀÉñ bAr\nàÉñ BAR\n", }, { "a='àÉñ bAr'; echo ${a,?}; echo ${a,,[br]}", "àÉñ bAr\nàÉñ bAr\n", }, { "a=(àÉñ bAr); echo ${a[@]^}; echo ${a[*],,}", "ÀÉñ BAr\nàéñ bar\n", }, { "INTERP_X_1=a INTERP_X_2=b; echo ${!INTERP_X_*}", "INTERP_X_1 INTERP_X_2\n", }, { "INTERP_X_2=b INTERP_X_1=a; echo ${!INTERP_*}", "INTERP_GLOBAL INTERP_X_1 INTERP_X_2\n", }, { `INTERP_X_2=b INTERP_X_1=a; set -- ${!INTERP_*}; echo $#`, "3\n", }, { `INTERP_X_2=b INTERP_X_1=a; set -- "${!INTERP_*}"; echo $#`, "1\n", }, { `INTERP_X_2=b INTERP_X_1=a; set -- ${!INTERP_@}; echo $#`, "3\n", }, { `INTERP_X_2=b INTERP_X_1=a; set -- "${!INTERP_@}"; echo $#`, "3\n", }, { `a='b c'; eval "echo -n ${a} ${a@Q}"`, `b c b c`, }, { `a='"\n'; printf "%s %s" "${a}" "${a@E}"`, "\"\\n \"\n", }, { "declare a; a+=(b); echo ${a[@]} ${#a[@]}", "b 1\n", }, { `a=""; a+=(b); echo ${a[@]} ${#a[@]}`, "b 2\n", }, { "f() { local a; a=bad; a=good; echo $a; }; f", "good\n", }, // if { "if true; then echo foo; fi", "foo\n", }, { "if false; then echo foo; fi", "", }, { "if false; then echo foo; fi", "", }, { "if true; then echo foo; else echo bar; fi", "foo\n", }, { "if false; then echo foo; else echo bar; fi", "bar\n", }, { "if true; then false; fi", "exit status 1", }, { "if false; then :; else false; fi", "exit status 1", }, { "if false; then :; elif true; then echo foo; fi", "foo\n", }, { "if false; then :; elif false; then :; elif true; then echo foo; fi", "foo\n", }, { "if false; then :; elif false; then :; else echo foo; fi", "foo\n", }, // while { "while false; do echo foo; done", "", }, { "while true; do exit 1; done", "exit status 1", }, { "while true; do break; done", "", }, { "while true; do while true; do break 2; done; done", "", }, // until { "until true; do echo foo; done", "", }, { "until false; do exit 1; done", "exit status 1", }, { "until false; do break; done", "", }, // for { "for i in 1 2 3; do echo $i; done", "1\n2\n3\n", }, { "for i in 1 2 3; do echo $i; exit; done", "1\n", }, { "for i in 1 2 3; do echo $i; false; done", "1\n2\n3\nexit status 1", }, { "for i in 1 2 3; do echo $i; break; done", "1\n", }, { "for i in 1 2 3; do echo $i; continue; echo foo; done", "1\n2\n3\n", }, { "for i in 1 2; do for j in a b; do echo $i $j; continue 2; done; done", "1 a\n2 a\n", }, { "for ((i=0; i<3; i++)); do echo $i; done", "0\n1\n2\n", }, // for, with missing Init, Cond, Post { "i=0; for ((; i<3; i++)); do echo $i; done", "0\n1\n2\n", }, { "for ((i=0;; i++)); do if [ $i -ge 3 ]; then break; fi; echo $i; done", "0\n1\n2\n", }, { "for ((i=0; i<3;)); do echo $i; i=$((i+1)); done", "0\n1\n2\n", }, { "i=0; for ((;;)); do if [ $i -ge 3 ]; then break; fi; echo $i; i=$((i+1)); done", "0\n1\n2\n", }, // TODO: uncomment once expandEnv.Set starts returning errors // { // "readonly i; for ((i=0; i<3; i++)); do echo $i; done", // "0\n1\n2\n", // }, { "for ((i=5; i>0; i--)); do echo $i; break; done", "5\n", }, { "for i in 1 2; do for j in a b; do echo $i $j; done; break; done", "1 a\n1 b\n", }, { "for i in 1 2 3; do :; done; echo $i", "3\n", }, { "for ((i=0; i<3; i++)); do :; done; echo $i", "3\n", }, { "set -- a 'b c'; for i in; do echo $i; done", "", }, { "set -- a 'b c'; for i; do echo $i; done", "a\nb c\n", }, // block { "{ echo foo; }", "foo\n", }, { "{ false; }", "exit status 1", }, // subshell { "(echo foo)", "foo\n", }, { "(false)", "exit status 1", }, { "(exit 1)", "exit status 1", }, { "(foo=bar; echo $foo); echo $foo", "bar\n\n", }, { "(echo() { printf 'bar\n'; }; echo); echo", "bar\n\n", }, { "unset INTERP_GLOBAL & echo $INTERP_GLOBAL", "value\n", }, { "(fn() { :; }) & pwd >/dev/null", "", }, { "x[0]=x; (echo ${x[0]}; x[0]=y; echo ${x[0]}); echo ${x[0]}", "x\ny\nx\n", }, { `x[3]=x; (x[3]=y); echo ${x[3]}`, "x\n", }, { "shopt -s expand_aliases; alias f='echo x'\nf\n(f\nalias f='echo y'\neval f\n)\nf\n", "x\nx\ny\nx\n", }, { "set -- a; echo $1; (echo $1; set -- b; echo $1); echo $1", "a\na\nb\na\n", }, {"false; ( echo $? )", "1\n"}, // cd/pwd {"[[ fo~ == 'fo~' ]]", ""}, {`[[ 'ab\c' == *\\* ]]`, ""}, {`[[ foo/bar == foo* ]]`, ""}, {"[[ a == [ab ]]", "exit status 1"}, {`HOME='/*'; echo ~; echo "$HOME"`, "/*\n/*\n"}, {`test -d ~`, ""}, { `for flag in b c d e f g h k L p r s S u w x; do test -$flag ""; echo -n "$flag$? "; done`, `b1 c1 d1 e1 f1 g1 h1 k1 L1 p1 r1 s1 S1 u1 w1 x1 `, }, {`foo=~; test -d $foo`, ""}, {`foo=~; test -d "$foo"`, ""}, {`foo='~'; test -d $foo`, "exit status 1"}, {`foo='~'; [ $foo == '~' ]`, ""}, { `[[ ~ == "$HOME" ]] && [[ ~/foo == "$HOME/foo" ]]`, "", }, { `HOME=$PWD/home; mkdir home; touch home/f; [[ -e ~/f ]]`, "", }, { `HOME=$PWD/home; mkdir home; touch home/f; [[ ~/f -ef $HOME/f ]]`, "", }, { "[[ ~noexist == '~noexist' ]]", "", }, { `w="$HOME"; cd; [[ $PWD == "$w" ]]`, "", }, { `mkdir test.cd; cd test.cd; cd ''; [[ "$PWD" == "$OLDPWD" ]]`, "", }, { `HOME=/foo; echo $HOME`, "/foo\n", }, { "cd noexist", "exit status 1 #JUSTERR", }, { "mkdir -p a/b && cd a && cd b && cd ../..", "", }, { ">a && cd a", "exit status 1 #JUSTERR", }, { `[[ $PWD == "$(pwd)" ]]`, "", }, { "PWD=changed; [[ $PWD == changed ]]", "", }, { "PWD=changed; mkdir a; cd a; [[ $PWD == changed ]]", "exit status 1", }, { `mkdir %s; old="$PWD"; cd %s; [[ $old == "$PWD" ]]`, "exit status 1", }, { `old="$PWD"; mkdir a; cd a; cd ..; [[ $old == "$PWD" ]]`, "", }, { `[[ $PWD == "$OLDPWD" ]]`, "exit status 1", }, { `old="$PWD"; mkdir a; cd a; [[ $old == "$OLDPWD" ]]`, "", }, { `mkdir a; ln -s a b; [[ $(cd a && pwd) == "$(cd b && pwd)" ]]; echo $?`, "1\n", }, { `pwd -a`, "invalid option: \"-a\"\nexit status 2 #JUSTERR", }, { `pwd -L -P -a`, "invalid option: \"-a\"\nexit status 2 #JUSTERR", }, { `mkdir a; ln -s a b; [[ "$(cd a && pwd -P)" == "$(cd b && pwd -P)" ]]`, "", }, { `mkdir a; ln -s a b; [[ "$(cd a && pwd -P)" == "$(cd b && pwd -L)" ]]; echo $?`, "1\n", }, // dirs/pushd/popd {"set -- $(dirs); echo $# ${#DIRSTACK[@]}", "1 1\n"}, {"pushd", "pushd: no other directory\nexit status 1 #JUSTERR"}, {"pushd -n", ""}, {"pushd foo bar", "pushd: too many arguments\nexit status 2 #JUSTERR"}, {"pushd does-not-exist; set -- $(dirs); echo $#", "1\n #IGNORE"}, {"mkdir a; pushd a >/dev/null; set -- $(dirs); echo $#", "2\n"}, {"mkdir a; set -- $(pushd a); echo $#", "2\n"}, { `mkdir a; pushd a >/dev/null; set -- $(dirs); [[ $1 == "$HOME" ]]`, "exit status 1", }, { `mkdir a; pushd a >/dev/null; [[ ${DIRSTACK[0]} == "$HOME" ]]`, "exit status 1", }, { `old=$(dirs); mkdir a; pushd a >/dev/null; pushd >/dev/null; set -- $(dirs); [[ $1 == "$old" ]]`, "", }, { `old=$(dirs); mkdir a; pushd a >/dev/null; pushd -n >/dev/null; set -- $(dirs); [[ $1 == "$old" ]]`, "exit status 1", }, { "mkdir a; pushd a >/dev/null; pushd >/dev/null; rm -r a; pushd", "exit status 1 #JUSTERR", }, { `old=$(dirs); mkdir a; pushd -n a >/dev/null; set -- $(dirs); [[ $1 == "$old" ]]`, "", }, { `old=$(dirs); mkdir a; pushd -n a >/dev/null; pushd >/dev/null; set -- $(dirs); [[ $1 == "$old" ]]`, "exit status 1", }, {"popd", "popd: directory stack empty\nexit status 1 #JUSTERR"}, {"popd -n", "popd: directory stack empty\nexit status 1 #JUSTERR"}, {"popd foo", "popd: invalid argument\nexit status 2 #JUSTERR"}, {"old=$(dirs); mkdir a; pushd a >/dev/null; set -- $(popd); echo $#", "1\n"}, { `old=$(dirs); mkdir a; pushd a >/dev/null; popd >/dev/null; [[ $(dirs) == "$old" ]]`, "", }, {"old=$(dirs); mkdir a; pushd a >/dev/null; set -- $(popd -n); echo $#", "1\n"}, { `old=$(dirs); mkdir a; pushd a >/dev/null; popd -n >/dev/null; [[ $(dirs) == "$old" ]]`, "exit status 1", }, { "mkdir a; pushd a >/dev/null; pushd >/dev/null; rm -r a; popd", "exit status 1 #JUSTERR", }, // binary cmd { "true && echo foo || echo bar", "foo\n", }, { "false && echo foo || echo bar", "bar\n", }, // func { "foo() { echo bar; }; foo", "bar\n", }, { "foo() { echo $1; }; foo", "\n", }, { "foo() { echo $1; }; foo a b", "a\n", }, { "foo() { echo $1; bar c d; echo $2; }; bar() { echo $2; }; foo a b", "a\nd\nb\n", }, { `foo() { echo $#; }; foo; foo 1 2 3; foo "a b"; echo $#`, "0\n3\n1\n0\n", }, { `foo() { for a in $*; do echo "$a"; done }; foo 'a 1' 'b 2'`, "a\n1\nb\n2\n", }, { `foo() { for a in "$*"; do echo "$a"; done }; foo 'a 1' 'b 2'`, "a 1 b 2\n", }, { `foo() { for a in "foo$*"; do echo "$a"; done }; foo 'a 1' 'b 2'`, "fooa 1 b 2\n", }, { `foo() { for a in $@; do echo "$a"; done }; foo 'a 1' 'b 2'`, "a\n1\nb\n2\n", }, { `foo() { for a in "$@"; do echo "$a"; done }; foo 'a 1' 'b 2'`, "a 1\nb 2\n", }, // alias (note the input newlines) { "alias foo; alias foo=echo; alias foo; alias foo=; alias foo", "alias: \"foo\" not found\nalias foo='echo'\nalias foo=''\n #IGNORE", }, { "shopt -s expand_aliases; alias foo=echo\nfoo foo; foo bar", "foo\nbar\n", }, { "shopt -s expand_aliases; alias true=echo\ntrue foo; unalias true\ntrue bar", "foo\n", }, { "shopt -s expand_aliases; alias echo='echo a'\necho b c", "a b c\n", }, { "shopt -s expand_aliases; alias foo='echo '\nfoo foo; foo bar", "echo\nbar\n", }, // case { "case b in x) echo foo ;; a|b) echo bar ;; esac", "bar\n", }, { "case b in x) echo foo ;; y|z) echo bar ;; esac", "", }, { "case foo in bar) echo foo ;; *) echo bar ;; esac", "bar\n", }, { "case foo in *o*) echo bar ;; esac", "bar\n", }, { "case foo in '*') echo x ;; f*) echo y ;; esac", "y\n", }, // exec { "$GOSH_PROG 'echo foo'", "foo\n", }, { "$GOSH_PROG 'echo foo >&2' >/dev/null", "foo\n", }, { "echo foo | $GOSH_PROG 'cat >&2' >/dev/null", "foo\n", }, { "$GOSH_PROG 'exit 1'", "exit status 1", }, { "exec >/dev/null; echo foo", "", }, // return {"return", "return: can only be done from a func or sourced script\nexit status 1 #JUSTERR"}, {"f() { return; }; f", ""}, {"f() { return 2; }; f", "exit status 2"}, {"f() { echo foo; return; echo bar; }; f", "foo\n"}, {"f1() { :; }; f2() { f1; return; }; f2", ""}, {"echo 'return' >a; source a", ""}, {"echo 'return' >a; source a; return", "return: can only be done from a func or sourced script\nexit status 1 #JUSTERR"}, {"echo 'return 2' >a; source a", "exit status 2"}, {"echo 'echo foo; return; echo bar' >a; source a", "foo\n"}, // command {"command", ""}, {"command -o echo", "command: invalid option \"-o\"\nexit status 2 #JUSTERR"}, {"command -vo echo", "command: invalid option \"-o\"\nexit status 2 #JUSTERR"}, {"echo() { :; }; echo foo", ""}, {"echo() { :; }; command echo foo", "foo\n"}, {"command -v does-not-exist", "exit status 1"}, {"foo() { :; }; command -v foo", "foo\n"}, {"foo() { :; }; command -v does-not-exist foo", "foo\n"}, {"command -v echo", "echo\n"}, {"[[ $(command -v $PATH_PROG) == $PATH_PROG ]]", "exit status 1"}, // cmd substitution { "echo foo $(printf bar)", "foo bar\n", }, { "echo foo $(echo bar)", "foo bar\n", }, { "$(echo echo foo bar)", "foo bar\n", }, { "for i in 1 $(echo 2 3) 4; do echo $i; done", "1\n2\n3\n4\n", }, { "echo 1$(echo 2 3)4", "12 34\n", }, { `mkdir d; [[ $(cd d && pwd) == "$(pwd)" ]]`, "exit status 1", }, { "a=sub true & { a=main $ENV_PROG | grep '^a='; }", "a=main\n", }, { "echo foo >f; echo $(cat f); echo $(f; echo $(/dev/null; cat; }", "foo\n", }, // redirects { "echo foo >&1 | sed 's/o/a/g'", "faa\n", }, { "echo foo >&2 | sed 's/o/a/g'", "foo\n", }, { // TODO: why does bash need a block here? "{ echo foo >&2; } |& sed 's/o/a/g'", "faa\n", }, { "echo foo >/dev/null; echo bar", "bar\n", }, { ">a; echo foo >>b; wc -c >b; cat b", "foo\n0\n", }, { "echo foo >a; a; wc -c >a; echo bar &>>a; wc -c &2; } &>/dev/null", "", }, { "sed 's/o/a/g' <a |& grep -q 'is a directory'", " #IGNORE bash prints a warning", }, { "echo foo 1>&1 | sed 's/o/a/g'", "faa\n", }, { "echo foo 2>&2 |& sed 's/o/a/g'", "faa\n", }, { "printf 2>&1 | sed 's/.*usage.*/foo/'", "foo\n", }, { "mkdir a && cd a && echo foo >b && cd .. && cat a/b", "foo\n", }, // background/wait {"wait", ""}, {"{ true; } & wait", ""}, {"{ exit 1; } & wait", ""}, { "{ echo foo; } & wait; echo bar", "foo\nbar\n", }, { "{ echo foo & wait; } & wait; echo bar", "foo\nbar\n", }, {`mkdir d; old=$PWD; cd d & wait; [[ $old == "$PWD" ]]`, ""}, { "f() { echo 1; }; { sleep 0.01s; f; } & f() { echo 2; }; wait", "1\n", }, // bash test { "[[ a ]]", "", }, { "[[ '' ]]", "exit status 1", }, { "[[ '' ]]; [[ a ]]", "", }, { "[[ ! (a == b) ]]", "", }, { "[[ a != b ]]", "", }, { "[[ a && '' ]]", "exit status 1", }, { "[[ a || '' ]]", "", }, { "[[ a > 3 ]]", "", }, { "[[ a < 3 ]]", "exit status 1", }, { "[[ 3 == 03 ]]", "exit status 1", }, { "[[ a -eq b ]]", "", }, { "[[ 3 -eq 03 ]]", "", }, { "[[ 3 -ne 4 ]]", "", }, { "[[ 3 -le 4 ]]", "", }, { "[[ 3 -ge 4 ]]", "exit status 1", }, { "[[ 3 -ge 3 ]]", "", }, { "[[ 3 -lt 4 ]]", "", }, { "[[ 3 -gt 4 ]]", "exit status 1", }, { "[[ 3 -gt 3 ]]", "exit status 1", }, { "[[ a -nt a || a -ot a ]]", "exit status 1", }, { "touch -d @1 a b; [[ a -nt b || a -ot b ]]", "exit status 1", }, { "touch -d @1 a; touch -d @2 b; [[ a -nt b ]]", "exit status 1", }, { "touch -d @1 a; touch -d @2 b; [[ a -ot b ]]", "", }, { "[[ a -ef b ]]", "exit status 1", }, { ">a >b; [[ a -ef b ]]", "exit status 1", }, { ">a; [[ a -ef a ]]", "", }, { ">a; ln a b; [[ a -ef b ]]", "", }, { ">a; ln -s a b; [[ a -ef b ]]", "", }, { "[[ -z 'foo' || -n '' ]]", "exit status 1", }, { "[[ -z '' && -n 'foo' ]]", "", }, { "a=x b=''; [[ -v a && -v b && ! -v c ]]", "", }, { "[[ abc == *b* ]]", "", }, { "[[ abc != *b* ]]", "exit status 1", }, { "[[ *b = '*b' ]]", "", }, { "[[ ab == a. ]]", "exit status 1", }, { `x='*b*'; [[ abc == $x ]]`, "", }, { `x='*b*'; [[ abc == "$x" ]]`, "exit status 1", }, { `[[ abc == \a\bc ]]`, "", }, { "[[ abc != *b'*' ]]", "", }, { "[[ a =~ b ]]", "exit status 1", }, { "[[ foo =~ foo && foo =~ .* && foo =~ f.o ]]", "", }, { "[[ foo =~ oo ]] && echo foo; [[ foo =~ ^oo$ ]] && echo bar || true", "foo\n", }, { "[[ a =~ [ ]]", "exit status 2", }, { "[[ -e a ]] && echo x; >a; [[ -e a ]] && echo y", "y\n", }, { "ln -s b a; [[ -e a ]] && echo x; >b; [[ -e a ]] && echo y", "y\n", }, { "[[ -f a ]] && echo x; >a; [[ -f a ]] && echo y", "y\n", }, { "[[ -e a ]] && echo x; mkdir a; [[ -e a ]] && echo y", "y\n", }, { "[[ -d a ]] && echo x; mkdir a; [[ -d a ]] && echo y", "y\n", }, { "[[ -r a ]] && echo x; >a; [[ -r a ]] && echo y", "y\n", }, { "[[ -w a ]] && echo x; >a; [[ -w a ]] && echo y", "y\n", }, { "[[ -s a ]] && echo x; echo body >a; [[ -s a ]] && echo y", "y\n", }, { "[[ -L a ]] && echo x; ln -s b a; [[ -L a ]] && echo y;", "y\n", }, { "mkdir a; cd a; test -f b && echo x; >b; test -f b && echo y", "y\n", }, { ">a; [[ -b a ]] && echo block; [[ -c a ]] && echo char; true", "", }, { "[[ -e /dev/sda ]] || { echo block; exit; }; [[ -b /dev/sda ]] && echo block; [[ -c /dev/sda ]] && echo char; true", "block\n", }, { "[[ -e /dev/nvme0n1 ]] || { echo block; exit; }; [[ -b /dev/nvme0n1 ]] && echo block; [[ -c /dev/nvme0n1 ]] && echo char; true", "block\n", }, { "[[ -e /dev/tty ]] || { echo char; exit; }; [[ -b /dev/tty ]] && echo block; [[ -c /dev/tty ]] && echo char; true", "char\n", }, {"[[ -t 1 ]]", "exit status 1"}, {"[[ -t 1234 ]]", "exit status 1"}, {"[[ -o wrong ]]", "exit status 1"}, {"[[ -o errexit ]]", "exit status 1"}, {"set -e; [[ -o errexit ]]", ""}, {"[[ -o noglob ]]", "exit status 1"}, {"set -f; [[ -o noglob ]]", ""}, {"[[ -o allexport ]]", "exit status 1"}, {"set -a; [[ -o allexport ]]", ""}, {"[[ -o nounset ]]", "exit status 1"}, {"set -u; [[ -o nounset ]]", ""}, {"[[ -o noexec ]]", "exit status 1"}, {"set -n; [[ -o noexec ]]", ""}, // actually does nothing, but oh well {"[[ -o pipefail ]]", "exit status 1"}, {"set -o pipefail; [[ -o pipefail ]]", ""}, // classic test { "[", "1:1: [: missing matching ]\nexit status 2 #JUSTERR", }, { "[ a", "1:1: [: missing matching ]\nexit status 2 #JUSTERR", }, { "[ a b c ]", "1:1: not a valid test operator: b\nexit status 2 #JUSTERR", }, { "[ a -a ]", "1:1: -a must be followed by an expression\nexit status 2 #JUSTERR", }, {"[ a ]", ""}, {"[ -n ]", ""}, {"[ '-n' ]", ""}, {"[ -z ]", ""}, {"[ ! ]", ""}, {"[ a != b ]", ""}, {"[ ! a '==' a ]", "exit status 1"}, {"[ a -a 0 -gt 1 ]", "exit status 1"}, {"[ 0 -gt 1 -o 1 -gt 0 ]", ""}, {"[ 3 -gt 4 ]", "exit status 1"}, {"[ 3 -lt 4 ]", ""}, { "[ -e a ] && echo x; >a; [ -e a ] && echo y", "y\n", }, { "test 3 -gt 4", "exit status 1", }, { "test 3 -lt 4", "", }, { "test 3 -lt", "1:1: -lt must be followed by a word\nexit status 2 #JUSTERR", }, { "touch -d @1 a; touch -d @2 b; [ a -nt b ]", "exit status 1", }, { "touch -d @1 a; touch -d @2 b; [ a -ot b ]", "", }, { ">a; [ a -ef a ]", "", }, {"[ 3 -eq 04 ]", "exit status 1"}, {"[ 3 -eq 03 ]", ""}, {"[ 3 -ne 03 ]", "exit status 1"}, {"[ 3 -le 4 ]", ""}, {"[ 3 -ge 4 ]", "exit status 1"}, { "[ -d a ] && echo x; mkdir a; [ -d a ] && echo y", "y\n", }, { "[ -r a ] && echo x; >a; [ -r a ] && echo y", "y\n", }, { "[ -w a ] && echo x; >a; [ -w a ] && echo y", "y\n", }, { "[ -s a ] && echo x; echo body >a; [ -s a ] && echo y", "y\n", }, { "[ -L a ] && echo x; ln -s b a; [ -L a ] && echo y;", "y\n", }, { ">a; [ -b a ] && echo block; [ -c a ] && echo char; true", "", }, {"[ -t 1 ]", "exit status 1"}, {"[ -t 1234 ]", "exit status 1"}, {"[ -o wrong ]", "exit status 1"}, {"[ -o errexit ]", "exit status 1"}, {"set -e; [ -o errexit ]", ""}, {"a=x b=''; [ -v a -a -v b -a ! -v c ]", ""}, {"[ a = a ]", ""}, {"[ a != a ]", "exit status 1"}, {"[ abc = ab* ]", "exit status 1"}, {"[ abc != ab* ]", ""}, // arithm { "echo $((1 == +1))", "1\n", }, { "echo $((!0))", "1\n", }, { "echo $((!3))", "0\n", }, { "echo $((~0))", "-1\n", }, { "echo $((~3))", "-4\n", }, { "echo $((1 + 2 - 3))", "0\n", }, { "echo $((-1 * 6 / 2))", "-3\n", }, { "a=2; echo $(( a + $a + c ))", "4\n", }, { "a=b; b=c; c=5; echo $((a % 3))", "2\n", }, { "echo $((2 > 2 || 2 < 2))", "0\n", }, { "echo $((2 >= 2 && 2 <= 2))", "1\n", }, { "echo $(((1 & 2) != (1 | 2)))", "1\n", }, { "echo $a; echo $((a = 3 ^ 2)); echo $a", "\n1\n1\n", }, { "echo $((a += 1, a *= 2, a <<= 2, a >> 1))", "4\n", }, { "echo $((a -= 10, a /= 2, a >>= 1, a << 1))", "-6\n", }, { "echo $((a |= 3, a &= 1, a ^= 8, a %= 5, a))", "4\n", }, { "echo $((a = 3, ++a, a--))", "4\n", }, { "echo $((2 ** 3)) $((1234 ** 4567))", "8 0\n", }, { "echo $((1 ? 2 : 3)) $((0 ? 2 : 3))", "2 3\n", }, { "((1))", "", }, { "((3 == 4))", "exit status 1", }, { "let i=(3+4); let i++; echo $i; let i--; echo $i", "8\n7\n", }, { "let 3==4", "exit status 1", }, { "a=1; let a++; echo $a", "2\n", }, { "a=$((1 + 2)); echo $a", "3\n", }, { "x=3; echo $(($x)) $((x))", "3 3\n", }, { "set -- 1; echo $(($@))", "1\n", }, { "a=b b=a; echo $(($a))", "0\n #IGNORE bash prints a warning", }, // set/shift { "echo $#; set foo bar; echo $#", "0\n2\n", }, { "shift; set a b c; shift; echo $@", "b c\n", }, { "shift 2; set a b c; shift 2; echo $@", "c\n", }, { `echo $#; set '' ""; echo $#`, "0\n2\n", }, { "set -- a b; echo $#", "2\n", }, { "set -U", "set: invalid option: \"-U\"\nexit status 2 #JUSTERR", }, { "set -e; false; echo foo", "exit status 1", }, { "set -e; shouldnotexist; echo foo", "\"shouldnotexist\": executable file not found in $PATH\nexit status 127 #JUSTERR", }, { "set -e; set +e; false; echo foo", "foo\n", }, { "set -e; ! false; echo foo", "foo\n", }, { "set -e; ! true; echo foo", "foo\n", }, { "set -e; if false; then echo foo; fi", "", }, { "set -e; while false; do echo foo; done", "", }, { "set -e; false || true", "", }, { "set -e; false && true; true", "", }, { "false | :", "", }, { "set -o pipefail; false | :", "exit status 1", }, { "set -o pipefail; true | false | true | :", "exit status 1", }, { "set -o pipefail; set -M 2>/dev/null | false", "exit status 1", }, { "set -f; >a.x; echo *.x;", "*.x\n", }, { "set -f; set +f; >a.x; echo *.x;", "a.x\n", }, { "set -a; foo=bar; $ENV_PROG | grep ^foo=", "foo=bar\n", }, { "set -a; foo=(b a r); $ENV_PROG | grep ^foo=", "exit status 1", }, { "foo=bar; set -a; $ENV_PROG | grep ^foo=", "exit status 1", }, { "a=b; echo $a; set -u; echo $a", "b\nb\n", }, { "echo $a; set -u; echo $a; echo extra", "\na: unbound variable\nexit status 1 #JUSTERR", }, { "foo=bar; set -u; echo ${foo/bar/}", "\n", }, { "foo=bar; set -u; echo ${foo#bar}", "\n", }, { "set -u; echo ${foo/bar/}", "foo: unbound variable\nexit status 1 #JUSTERR", }, { "set -u; echo ${foo#bar}", "foo: unbound variable\nexit status 1 #JUSTERR", }, // TODO: detect this case as unset // { // "set -u; foo=(bar); echo $foo; echo ${foo[3]}", // "bar\nfoo: unbound variable\nexit status 1 #JUSTERR", // }, { "set -u; foo=(''); echo ${foo[0]}", "\n", }, { "set -u; echo ${#foo}", "foo: unbound variable\nexit status 1 #JUSTERR", }, { "set -u; echo ${foo+bar}", "\n", }, { "set -u; echo ${foo:+bar}", "\n", }, { "set -u; echo ${foo-bar}", "bar\n", }, { "set -u; echo ${foo:-bar}", "bar\n", }, { "set -u; echo ${foo=bar}", "bar\n", }, { "set -u; echo ${foo:=bar}", "bar\n", }, { "set -u; echo ${foo?bar}", "foo: bar\nexit status 1 #JUSTERR", }, { "set -u; echo ${foo:?bar}", "foo: bar\nexit status 1 #JUSTERR", }, { "set -ue; set -ueo pipefail", "", }, {"set -n; echo foo", ""}, {"set -n; [ wrong", ""}, {"set -n; set +n; echo foo", ""}, { "set -o foobar", "set: invalid option: \"foobar\"\nexit status 2 #JUSTERR", }, {"set -o noexec; echo foo", ""}, {"set +o noexec; echo foo", "foo\n"}, {"set -e; set -o | grep -E 'errexit|noexec' | wc -l", "2\n"}, {"set -e; set -o | grep -E 'errexit|noexec' | grep 'on$' | wc -l", "1\n"}, { "set -a; set +o", `set -o allexport set +o errexit set +o noexec set +o noglob set +o nounset set +o pipefail #IGNORE`, }, // unset { "a=1; echo $a; unset a; echo $a", "1\n\n", }, { "notinpath() { echo func; }; notinpath; unset -f notinpath; notinpath", "func\n\"notinpath\": executable file not found in $PATH\nexit status 127 #JUSTERR", }, { "a=1; a() { echo func; }; unset -f a; echo $a", "1\n", }, { "a=1; a() { echo func; }; unset -v a; a; echo $a", "func\n\n", }, { "notinpath=1; notinpath() { echo func; }; notinpath; echo $notinpath; unset notinpath; notinpath; echo $notinpath; unset notinpath; notinpath", "func\n1\nfunc\n\n\"notinpath\": executable file not found in $PATH\nexit status 127 #JUSTERR", }, { "unset PATH; [[ $PATH == '' ]]", "", }, { "readonly a=1; echo $a; unset a; echo $a", "1\na: readonly variable\n1\n #IGNORE bash prints a warning", }, { "f() { local a=1; echo $a; unset a; echo $a; }; f", "1\n\n", }, { `a=b eval 'echo $a; unset a; echo $a'`, "b\n\n", }, { `$(unset INTERP_GLOBAL); echo $INTERP_GLOBAL; unset INTERP_GLOBAL; echo $INTERP_GLOBAL`, "value\n\n", }, { `x=orig; f() { local x=local; unset x; x=still_local; }; f; echo $x`, "orig\n", }, { `x=orig; f() { local x=local; unset x; [[ -v x ]] && echo set || echo unset; }; f`, "unset\n", }, // shopt {"set -e; shopt -o | grep -E 'errexit|noexec' | wc -l", "2\n"}, {"set -e; shopt -o | grep -E 'errexit|noexec' | grep 'on$' | wc -l", "1\n"}, {"shopt -s -o noexec; echo foo", ""}, {"shopt -so noexec; echo foo", ""}, {"shopt -u -o noexec; echo foo", "foo\n"}, {"shopt -u globstar; shopt globstar | grep 'off$' | wc -l", "1\n"}, {"shopt -s globstar; shopt globstar | grep 'off$' | wc -l", "0\n"}, // IFS {`echo -n "$IFS"`, " \t\n"}, {`a="x:y:z"; IFS=:; echo $a`, "x y z\n"}, {`a=(x y z); IFS=-; echo ${a[*]}`, "x y z\n"}, {`a=(x y z); IFS=-; echo ${a[@]}`, "x y z\n"}, {`a=(x y z); IFS=-; echo "${a[*]}"`, "x-y-z\n"}, {`a=(x y z); IFS=-; echo "${a[@]}"`, "x y z\n"}, {`a=" x y z"; IFS=; echo $a`, " x y z\n"}, {`a=(x y z); IFS=; echo "${a[*]}"`, "xyz\n"}, {`a=(x y z); IFS=-; echo "${!a[@]}"`, "0 1 2\n"}, {`set -- x y z; IFS=-; echo $*`, "x y z\n"}, {`set -- x y z; IFS=-; echo "$*"`, "x-y-z\n"}, {`set -- x y z; IFS=; echo $*`, "x y z\n"}, {`set -- x y z; IFS=; echo "$*"`, "xyz\n"}, // builtin {"builtin", ""}, {"builtin noexist", "exit status 1 #JUSTERR"}, {"builtin echo foo", "foo\n"}, { "echo() { printf 'bar\n'; }; echo foo; builtin echo foo", "bar\nfoo\n", }, // type {"type", ""}, {"type for", "for is a shell keyword\n"}, {"type echo", "echo is a shell builtin\n"}, {"echo() { :; }; type echo | grep 'is a function'", "echo is a function\n"}, {"type $PATH_PROG | grep -q -E ' is (/|[A-Z]:)'", ""}, {"type noexist", "type: noexist: not found\nexit status 1 #JUSTERR"}, {"PATH=/; type $PATH_PROG", "type: " + pathProg + ": not found\nexit status 1 #JUSTERR"}, {"shopt -s expand_aliases; alias foo='bar baz'\ntype foo", "foo is aliased to `bar baz'\n"}, {"alias foo='bar baz'\ntype foo", "type: foo: not found\nexit status 1 #JUSTERR"}, {"type -p $PATH_PROG | grep -q -E '^(/|[A-Z]:)'", ""}, {"PATH=/; type -p $PATH_PROG", "exit status 1"}, {"shopt -s expand_aliases; alias foo='bar'; type -t foo", "alias\n"}, {"type -t case", "keyword\n"}, {"foo(){ :; }; type -t foo", "function\n"}, {"type -t type", "builtin\n"}, {"type -t $PATH_PROG", "file\n"}, {"type -t inexisting_dfgsdgfds", "exit status 1"}, // trap {"trap 'echo at_exit' EXIT; true", "at_exit\n"}, {"trap 'echo on_err' ERR; false; echo FAIL", "on_err\nFAIL\n"}, {"trap 'echo at_exit' EXIT; trap - EXIT; echo OK", "OK\n"}, {"set -e; trap 'echo A' ERR EXIT; false; echo FAIL", "A\nA\nexit status 1"}, {"trap 'foobar' UNKNOWN", "trap: UNKNOWN: invalid signal specification\nexit status 2 #JUSTERR"}, // TODO: our builtin appears to not receive the piped bytes? // {"trap 'echo on_err' ERR; trap | grep -q '.*echo on_err.*'", "trap -- \"echo on_err\" ERR\n"}, {"trap 'false' ERR EXIT; false", "exit status 1"}, // eval {"eval", ""}, {"eval ''", ""}, {"eval echo foo", "foo\n"}, {"eval 'echo foo'", "foo\n"}, {"eval 'exit 1'", "exit status 1"}, {"eval '('", "eval: 1:1: reached EOF without matching ( with )\nexit status 1 #JUSTERR"}, {"set a b; eval 'echo $@'", "a b\n"}, {"eval 'a=foo'; echo $a", "foo\n"}, {`a=b eval "echo $a"`, "\n"}, {`a=b eval 'echo $a'`, "b\n"}, {`eval 'echo "\$a"'`, "$a\n"}, {`a=b eval 'x=y eval "echo \$a \$x"'`, "b y\n"}, {`a=b eval 'a=y eval "echo $a \$a"'`, "b y\n"}, {"a=b eval '(echo $a)'", "b\n"}, // source { "source", "1:1: source: need filename\nexit status 2 #JUSTERR", }, { "echo 'echo foo' >a; source a; . a", "foo\nfoo\n", }, { "echo 'echo $@' >a; source a; source a b c; echo $@", "\nb c\n\n", }, { "echo 'foo=bar' >a; source a; echo $foo", "bar\n", }, // source from PATH { "mkdir test; echo 'echo foo' >test/a; PATH=$PWD/test source a; . test/a", "foo\nfoo\n", }, // source with set and shift { "echo 'set -- d e f' >a; source a; echo $@", "d e f\n", }, { "echo 'echo $@' >a; set -- b c; source a; echo $@", "b c\nb c\n", }, { "echo 'echo $@' >a; set -- b c; source a d e; echo $@", "d e\nb c\n", }, { "echo 'shift; echo $@' >a; set -- b c; source a d e; echo $@", "e\nb c\n", }, { "echo 'shift' >a; set -- b c; source a; echo $@", "c\n", }, { "echo 'shift; set -- $@' >a; set -- b c; source a d e; echo $@", "e\n", }, { "echo 'set -- g f'>b; echo 'set -- d e f; echo $@; source b;' >a; source a; echo $@", "d e f\ng f\n", }, { "echo 'set -- g f'>b; echo 'echo $@; set -- d e f; source b;' >a; source a b c; echo $@", "b c\ng f\n", }, { "echo 'shift; echo $@' >b; echo 'shift; echo $@; source b' >a; source a b c d; echo $@", "c d\nd\n\n", }, { "echo 'set -- b c d' >b; echo 'source b' >a; set -- a; source a; echo $@", "b c d\n", }, { "echo 'echo $@' >b; echo 'set -- b c d; source b' >a; set -- a; source a; echo $@", "b c d\nb c d\n", }, { "echo 'shift; echo $@' >b; echo 'shift; echo $@; source b c d' >a; set -- a b; source a; echo $@", "b\nd\nb\n", }, { "echo 'set -- a b c' >b; echo 'echo $@; source b; echo $@' >a; source a; echo $@", "\na b c\na b c\n", }, // indexed arrays { "a=foo; echo ${a[0]} ${a[@]} ${a[x]}; echo ${a[1]}", "foo foo foo\n\n", }, { "a=(); echo ${a[0]} ${a[@]} ${a[x]} ${a[1]}", "\n", }, { "a=(b c); echo $a; echo ${a[0]}; echo ${a[1]}; echo ${a[x]}", "b\nb\nc\nb\n", }, { "a=(b c); echo ${a[@]}; echo ${a[*]}", "b c\nb c\n", }, { "a=(1 2 3); echo ${a[2-1]}; echo $((a[1+1]))", "2\n3\n", }, { "a=(1 2) x=(); a+=b x+=c; echo ${a[@]}; echo ${x[@]}", "1b 2\nc\n", }, { "a=(1 2) x=(); a+=(b c) x+=(d e); echo ${a[@]}; echo ${x[@]}", "1 2 b c\nd e\n", }, { "a=bbb; a+=(c d); echo ${a[@]}", "bbb c d\n", }, { `a=('a 1' 'b 2'); for e in ${a[@]}; do echo "$e"; done`, "a\n1\nb\n2\n", }, { `a=('a 1' 'b 2'); for e in "${a[*]}"; do echo "$e"; done`, "a 1 b 2\n", }, { `a=('a 1' 'b 2'); for e in "${a[@]}"; do echo "$e"; done`, "a 1\nb 2\n", }, { `a=([1]=y [0]=x); echo ${a[0]}`, "x\n", }, { `a=(y); a[2]=x; echo ${a[2]}`, "x\n", }, { `a="y"; a[2]=x; echo ${a[2]}`, "x\n", }, { `declare -a a=(x y); echo ${a[1]}`, "y\n", }, { `a=b; echo "${a[@]}"`, "b\n", }, { `a=(b); echo ${a[3]}`, "\n", }, { `a=(b); echo ${a[-2]}`, "negative array index\nexit status 1 #JUSTERR", }, // associative arrays { `a=foo; echo ${a[""]} ${a["x"]}`, "foo foo\n", }, { `declare -A a=(); echo ${a[0]} ${a[@]} ${a[1]} ${a["x"]}`, "\n", }, { `declare -A a=([x]=b [y]=c); echo $a; echo ${a[0]}; echo ${a["x"]}; echo ${a["_"]}`, "\n\nb\n\n", }, { `declare -A a=([x]=b [y]=c); for e in ${a[@]}; do echo $e; done | sort`, "b\nc\n", }, { `declare -A a=([y]=b [x]=c); for e in ${a[*]}; do echo $e; done | sort`, "b\nc\n", }, { `declare -A a=([x]=a); a["y"]=d; a["x"]=c; for e in ${a[@]}; do echo $e; done | sort`, "c\nd\n", }, { `declare -A a=([x]=a); a[y]=d; a[x]=c; for e in ${a[@]}; do echo $e; done | sort`, "c\nd\n", }, { // cheating a little; bash just did a=c `a=(["x"]=b ["y"]=c); echo ${a["y"]}`, "c\n", }, { `declare -A a=(['x']=b); echo ${a['x']} ${a[$'x']} ${a[$"x"]}`, "b b b\n", }, { `a=(['x']=b); echo ${a['y']}`, "\n #IGNORE bash requires -A", }, // weird assignments {"a=b; a=(c d); echo ${a[@]}", "c d\n"}, {"a=(b c); a=d; echo ${a[@]}", "d c\n"}, {"declare -A a=([x]=b [y]=c); a=d; for e in ${a[@]}; do echo $e; done | sort", "b\nc\nd\n"}, {"i=3; a=b; a[i]=x; echo ${a[@]}", "b x\n"}, {"i=3; declare a=(b); a[i]=x; echo ${!a[@]}", "0 3\n"}, {"i=3; declare -A a=(['x']=b); a[i]=x; for e in ${!a[@]}; do echo $e; done | sort", "i\nx\n"}, // declare {"declare -B foo", "declare: invalid option \"-B\"\nexit status 2 #JUSTERR"}, {"a=b; declare a; echo $a; declare a=; echo $a", "b\n\n"}, {"a=b; declare a; echo $a", "b\n"}, { "declare a=b c=(1 2); echo $a; echo ${c[@]}", "b\n1 2\n", }, {"a=x; declare $a; echo $a $x", "x\n"}, {"a=x=y; declare $a; echo $a $x", "x=y y\n"}, {"a='x=(y)'; declare $a; echo $a $x", "x=(y) (y)\n"}, {"a='x=b y=c'; declare $a; echo $x $y", "b c\n"}, {"declare =bar", "declare: invalid name \"\"\nexit status 1 #JUSTERR"}, {"declare $unset=$unset", "declare: invalid name \"\"\nexit status 1 #JUSTERR"}, // export {"declare foo=bar; $ENV_PROG | grep '^foo='", "exit status 1"}, {"declare -x foo=bar; $ENV_PROG | grep '^foo='", "foo=bar\n"}, {"export foo=bar; $ENV_PROG | grep '^foo='", "foo=bar\n"}, {"foo=bar; export foo; $ENV_PROG | grep '^foo='", "foo=bar\n"}, {"export foo=bar; foo=baz; $ENV_PROG | grep '^foo='", "foo=baz\n"}, {"export foo=bar; readonly foo=baz; $ENV_PROG | grep '^foo='", "foo=baz\n"}, {"export foo=(1 2); $ENV_PROG | grep '^foo='", "exit status 1"}, {"declare -A foo=([a]=b); export foo; $ENV_PROG | grep '^foo='", "exit status 1"}, {"export foo=(b c); foo=x; $ENV_PROG | grep '^foo='", "exit status 1"}, {"foo() { bar=foo; export bar; }; foo; $ENV_PROG | grep ^bar=", "bar=foo\n"}, {"foo() { export bar; }; bar=foo; foo; $ENV_PROG | grep ^bar=", "bar=foo\n"}, {"foo() { export bar; }; foo; bar=foo; $ENV_PROG | grep ^bar=", "bar=foo\n"}, {"foo() { export bar=foo; }; foo; readonly bar; $ENV_PROG | grep ^bar=", "bar=foo\n"}, // local { "local a=b", "local: can only be used in a function\nexit status 1 #JUSTERR", }, { "local a=b 2>/dev/null; echo $a", "\n", }, { "{ local a=b; }", "local: can only be used in a function\nexit status 1 #JUSTERR", }, { "echo 'local a=b' >a; source a", "local: can only be used in a function\nexit status 1 #JUSTERR", }, { "echo 'local a=b' >a; f() { source a; }; f; echo $a", "\n", }, { "f() { local a=b; }; f; echo $a", "\n", }, { "a=x; f() { local a=b; }; f; echo $a", "x\n", }, { "a=x; f() { echo $a; local a=b; echo $a; }; f", "x\nb\n", }, { "f1() { local a=b; }; f2() { f1; echo $a; }; f2", "\n", }, { "f() { a=1; declare b=2; export c=3; readonly d=4; declare -g e=5; }; f; echo $a $b $c $d $e", "1 3 4 5\n", }, { `f() { local x; [[ -v x ]] && echo set || echo unset; }; f`, "unset\n", }, { `f() { local x=; [[ -v x ]] && echo set || echo unset; }; f`, "set\n", }, { `export x=before; f() { local x; export x=after; $ENV_PROG | grep '^x='; }; f; echo $x`, "x=after\nbefore\n", }, // unset global from inside function {"f() { unset foo; echo $foo; }; foo=bar; f", "\n"}, {"f() { unset foo; }; foo=bar; f; echo $foo", "\n"}, // name references {"declare -n foo=bar; bar=etc; [[ -R foo ]]", ""}, {"declare -n foo=bar; bar=etc; [ -R foo ]", ""}, {"nameref foo=bar; bar=etc; [[ -R foo ]]", " #IGNORE"}, {"declare foo=bar; bar=etc; [[ -R foo ]]", "exit status 1"}, { "declare -n foo=bar; bar=etc; echo $foo; bar=zzz; echo $foo", "etc\nzzz\n", }, { "declare -n foo=bar; bar=(x y); echo ${foo[1]}; bar=(a b); echo ${foo[1]}", "y\nb\n", }, { "declare -n foo=bar; bar=etc; echo $foo; unset bar; echo $foo", "etc\n\n", }, { "declare -n a1=a2 a2=a3 a3=a4; a4=x; echo $a1 $a3", "x x\n", }, { "declare -n foo=bar bar=foo; echo $foo", "\n #IGNORE", }, { "declare -n foo=bar; echo $foo", "\n", }, { "declare -n foo=bar; echo ${!foo}", "bar\n", }, { "declare -n foo=bar; bar=etc; echo $foo; echo ${!foo}", "etc\nbar\n", }, { "declare -n foo=bar; bar=etc; foo=xxx; echo $foo $bar", "xxx xxx\n", }, { "declare -n foo=bar; foo=xxx; echo $foo $bar", "xxx xxx\n", }, // TODO: figure this one out //{ // "declare -n foo=bar bar=baz; foo=xxx; echo $foo $bar; echo $baz", // "xxx xxx\nxxx\n", //}, // read-only vars {"declare -r foo=bar; echo $foo", "bar\n"}, {"readonly foo=bar; echo $foo", "bar\n"}, {"readonly foo=bar; export foo; echo $foo", "bar\n"}, {"readonly foo=bar; readonly bar=foo; export foo bar; echo $bar", "foo\n"}, { "a=b; a=c; echo $a; readonly a; a=d", "c\na: readonly variable\nexit status 1 #JUSTERR", }, { "declare -r foo=bar; foo=etc", "foo: readonly variable\nexit status 1 #JUSTERR", }, { "declare -r foo=bar; export foo=", "foo: readonly variable\nexit status 1 #JUSTERR", }, { "readonly foo=bar; foo=etc", "foo: readonly variable\nexit status 1 #JUSTERR", }, { "foo() { bar=foo; readonly bar; }; foo; bar=bar", "bar: readonly variable\nexit status 1 #JUSTERR", }, { "foo() { readonly bar; }; foo; bar=foo", "bar: readonly variable\nexit status 1 #JUSTERR", }, { "foo() { readonly bar=foo; }; foo; export bar; $ENV_PROG | grep '^bar='", "bar=foo\n", }, // multiple var modes at once { "declare -r -x foo=bar; $ENV_PROG | grep '^foo='", "foo=bar\n", }, { "declare -r -x foo=bar; foo=x", "foo: readonly variable\nexit status 1 #JUSTERR", }, // globbing {"echo .", ".\n"}, {"echo ..", "..\n"}, {"echo ./.", "./.\n"}, { ">a.x >b.x >c.x; echo *.x; rm a.x b.x c.x", "a.x b.x c.x\n", }, { `>a.x; echo '*.x' "*.x"; rm a.x`, "*.x *.x\n", }, { `>a.x >b.y; echo *'.'x; rm a.x`, "a.x\n", }, { `>a.x; echo *'.x' "a."* '*'.x; rm a.x`, "a.x a.x *.x\n", }, { "echo *.x; echo foo *.y bar", "*.x\nfoo *.y bar\n", }, { "mkdir a; >a/b.x; echo */*.x | sed 's@\\\\@/@g'; cd a; echo *.x", "a/b.x\nb.x\n", }, { "mkdir -p a/b/c; echo a/* | sed 's@\\\\@/@g'", "a/b\n", }, { ">.hidden >a; echo *; echo .h*; rm .hidden a", "a\n.hidden\n", }, { `mkdir d; >d/.hidden >d/a; set -- "$(echo d/*)" "$(echo d/.h*)"; echo ${#1} ${#2}; rm -r d`, "3 9\n", }, { "mkdir -p a/b/c; echo a/** | sed 's@\\\\@/@g'", "a/b\n", }, { "shopt -s globstar; mkdir -p a/b/c; echo a/** | sed 's@\\\\@/@g'", "a/ a/b a/b/c\n", }, { "shopt -s globstar; mkdir -p a/b/c; echo **/c | sed 's@\\\\@/@g'", "a/b/c\n", }, { "shopt -s nullglob; touch existing-1; echo missing-* existing-*", "existing-1\n", }, { "cat <x-f; echo x-*/ | sed 's@\\\\@/@g'", "x-d1/ x-d2/\n", }, { "mkdir x-d1 x-d2; >x-f; echo ././x-*/// | sed 's@\\\\@/@g'", "././x-d1/ ././x-d2/\n", }, { "mkdir -p x-d1/a x-d2/b; >x-f; echo x-*/* | sed 's@\\\\@/@g'", "x-d1/a x-d2/b\n", }, { "mkdir -p foo/bar; ln -s foo sym; echo sy*/; echo sym/b*", "sym/\nsym/bar\n", }, { ">foo; ln -s foo sym; echo sy*; echo sy*/", "sym\nsy*/\n", }, { "mkdir x-d; >x-f; test -d $PWD/x-*/", "", }, { "mkdir dir; >dir/x-f; ln -s dir sym; cd sym; test -f $PWD/x-*", "", }, // brace expansion; more exhaustive tests in the syntax package {"echo a}b", "a}b\n"}, {"echo {a,b{c,d}", "{a,bc {a,bd\n"}, {"echo a{b}", "a{b}\n"}, {"echo a{à,世界}", "aà a世界\n"}, {"echo a{b,c}d{e,f}g", "abdeg abdfg acdeg acdfg\n"}, {"echo a{b{x,y},c}d", "abxd abyd acd\n"}, {"echo a{1..", "a{1..\n"}, {"echo a{1..2}b{4..5}c", "a1b4c a1b5c a2b4c a2b5c\n"}, {"echo a{c..f}", "ac ad ae af\n"}, {"echo a{4..1..1}", "a4 a3 a2 a1\n"}, // tilde expansion { "[[ '~/foo' == ~/foo ]] || [[ ~/foo == '~/foo' ]]", "exit status 1", }, { "case '~/foo' in ~/foo) echo match ;; esac", "", }, { "a=~/foo; [[ $a == '~/foo' ]]", "exit status 1", }, { `a=$(echo "~/foo"); [[ $a == '~/foo' ]]`, "", }, // /dev/null {"echo foo >/dev/null", ""}, {"cat a; chmod 0755 a; [ -x a ] && echo y", "y\n", }, { "[[ -x a ]] && echo x; >a; chmod 0755 a; [[ -x a ]] && echo y", "y\n", }, { ">a; [ -k a ] && echo x; chmod +t a; [ -k a ] && echo y", "y\n", }, { ">a; [ -u a ] && echo x; chmod u+s a; [ -u a ] && echo y", "y\n", }, { ">a; [ -g a ] && echo x; chmod g+s a; [ -g a ] && echo y", "y\n", }, { ">a; [[ -k a ]] && echo x; chmod +t a; [[ -k a ]] && echo y", "y\n", }, { ">a; [[ -u a ]] && echo x; chmod u+s a; [[ -u a ]] && echo y", "y\n", }, { ">a; [[ -g a ]] && echo x; chmod g+s a; [[ -g a ]] && echo y", "y\n", }, { `mkdir a; chmod 0100 a; cd a`, "", }, // Note that these will succeed if we're root. { `mkdir a; chmod 0000 a; cd a && test $UID -ne 0`, "exit status 1 #JUSTERR", }, { `mkdir a; chmod 0222 a; cd a && test $UID -ne 0`, "exit status 1 #JUSTERR", }, { `mkdir a; chmod 0444 a; cd a && test $UID -ne 0`, "exit status 1 #JUSTERR", }, { `mkdir a; chmod 0010 a; cd a && test $UID -ne 0`, "exit status 1 #JUSTERR", }, { `mkdir a; chmod 0001 a; cd a && test $UID -ne 0`, "exit status 1 #JUSTERR", }, { `unset UID`, "UID: readonly variable\n #IGNORE", }, // GID is not set in bash { `unset GID`, "GID: readonly variable\n #IGNORE", }, { `[[ -z $GID ]] && echo "GID not set"`, "exit status 1 #JUSTERR #IGNORE", }, // Unix-y PATH { "PATH=; bash -c 'echo foo'", "\"bash\": executable file not found in $PATH\nexit status 127 #JUSTERR", }, { "cd /; sure/is/missing", "stat /sure/is/missing: no such file or directory\nexit status 127 #JUSTERR", }, { "echo '#!/bin/sh\necho b' >a; chmod 0755 a; PATH=; a", "b\n", }, { "mkdir c; cd c; echo '#!/bin/sh\necho b' >a; chmod 0755 a; PATH=; a", "b\n", }, { "mkdir c; echo '#!/bin/sh\necho b' >c/a; chmod 0755 c/a; c/a", "b\n", }, { "GOSH_CMD=lookpath $GOSH_PROG", "sh found\n", }, // error strings which are too different on Windows { "echo foo >/shouldnotexist/file", "open /shouldnotexist/file: no such file or directory\nexit status 1 #JUSTERR", }, { "set -e; echo foo >/shouldnotexist/file; echo foo", "open /shouldnotexist/file: no such file or directory\nexit status 1 #JUSTERR", }, // process substitution; named pipes (fifos) are a TODO for windows { "sed 's/o/e/g' <(echo foo bar)", "fee bar\n", }, { "cat <(echo foo) <(echo bar) <(echo baz)", "foo\nbar\nbaz\n", }, { "cat <(cat <(echo nested))", "nested\n", }, { "echo foo bar > >(sed 's/o/e/g')", "fee bar\n", }, { "echo foo bar | tee >(sed 's/o/e/g') >/dev/null", "fee bar\n", }, { "echo nested > >(cat > >(cat))", "nested\n", }, } var runTestsWindows = []runTest{ {"[[ -n $PPID || $PPID -gt 0 ]]", ""}, // os.Getppid can be 0 on windows {"cmd() { :; }; cmd /c 'echo foo'", ""}, {"cmd() { :; }; command cmd /c 'echo foo'", "foo\r\n"}, { "GOSH_CMD=lookpath $GOSH_PROG", "cmd found\n", }, } // These tests are specific to 64-bit architectures, and that's fine. We don't // need to add explicit versions for 32-bit. var runTests64bit = []runTest{ {"printf %i,%u -3 -3", "-3,18446744073709551613"}, {"printf %o -3", "1777777777777777777775"}, {"printf %x -3", "fffffffffffffffd"}, } func init() { if runtime.GOOS == "windows" { runTests = append(runTests, runTestsWindows...) } else { // Unix-y runTests = append(runTests, runTestsUnix...) } if bits.UintSize == 64 { runTests = append(runTests, runTests64bit...) } } // ln -s: wine doesn't implement symlinks; see https://bugs.winehq.org/show_bug.cgi?id=44948 var skipOnWindows = regexp.MustCompile(`ln -s`) // process substitutions seemflaky on mac; see https://github.com/mvdan/sh/issues/576 var skipOnMac = regexp.MustCompile(`>\(|<\(`) func skipIfUnsupported(tb testing.TB, src string) { switch { case runtime.GOOS == "windows" && skipOnWindows.MatchString(src): tb.Skipf("skipping non-portable test on windows") case runtime.GOOS == "darwin" && skipOnMac.MatchString(src): tb.Skipf("skipping non-portable test on mac") } } func TestRunnerRun(t *testing.T) { t.Parallel() p := syntax.NewParser() for _, c := range runTests { c := c t.Run("", func(t *testing.T) { skipIfUnsupported(t, c.in) // Parse first, as we reuse a single parser. file := parse(t, p, c.in) t.Parallel() tdir := t.TempDir() var cb concBuffer r, err := New(Dir(tdir), StdIO(nil, &cb, &cb), // TODO: why does this make some tests hang? // Env(expand.ListEnviron(append(os.Environ(), // "FOO_NULL_BAR=foo\x00bar")...)), OpenHandler(testOpenHandler), ExecHandler(testExecHandler), ) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r.Run(ctx, file); err != nil { cb.WriteString(err.Error()) } want := c.want if i := strings.Index(want, " #"); i >= 0 { want = want[:i] } if got := cb.String(); got != want { if len(got) > 80 { got = "…" + got[len(got)-80:] } t.Fatalf("wrong output in %q:\nwant: %q\ngot: %q", c.in, want, got) } }) } } func readLines(hc HandlerContext) ([][]byte, error) { bs, err := io.ReadAll(hc.Stdin) if err != nil { return nil, err } if runtime.GOOS == "windows" { bs = bytes.ReplaceAll(bs, []byte("\r\n"), []byte("\n")) } bs = bytes.TrimSuffix(bs, []byte("\n")) return bytes.Split(bs, []byte("\n")), nil } var testBuiltinsMap = map[string]func(HandlerContext, []string) error{ "cat": func(hc HandlerContext, args []string) error { if len(args) == 0 { if hc.Stdin == nil || hc.Stdout == nil { return nil } _, err := io.Copy(hc.Stdout, hc.Stdin) return err } for _, arg := range args { path := absPath(hc.Dir, arg) f, err := os.Open(path) if err != nil { return err } _, err = io.Copy(hc.Stdout, f) f.Close() if err != nil { return err } } return nil }, "wc": func(hc HandlerContext, args []string) error { bs, err := io.ReadAll(hc.Stdin) if err != nil { return err } if len(args) == 0 { fmt.Fprintf(hc.Stdout, "%7d", bytes.Count(bs, []byte("\n"))) fmt.Fprintf(hc.Stdout, "%8d", len(bytes.Fields(bs))) fmt.Fprintf(hc.Stdout, "%8d\n", len(bs)) } else if args[0] == "-c" { fmt.Fprintln(hc.Stdout, len(bs)) } else if args[0] == "-l" { fmt.Fprintln(hc.Stdout, bytes.Count(bs, []byte("\n"))) } return nil }, "sort": func(hc HandlerContext, args []string) error { lines, err := readLines(hc) if err != nil { return err } sort.Slice(lines, func(i, j int) bool { return bytes.Compare(lines[i], lines[j]) < 0 }) for _, line := range lines { fmt.Fprintf(hc.Stdout, "%s\n", line) } return nil }, "grep": func(hc HandlerContext, args []string) error { var rx *regexp.Regexp quiet := false for _, arg := range args { if arg == "-q" { quiet = true } else if arg == "-E" { } else if rx == nil { rx = regexp.MustCompile(arg) } else { return fmt.Errorf("unexpected arg: %q", arg) } } lines, err := readLines(hc) if err != nil { return err } any := false for _, line := range lines { if rx.Match(line) { if quiet { return nil } any = true fmt.Fprintf(hc.Stdout, "%s\n", line) } } if !any { return NewExitStatus(1) } return nil }, "sed": func(hc HandlerContext, args []string) error { f := hc.Stdin switch len(args) { case 1: case 2: var err error f, err = os.Open(absPath(hc.Dir, args[1])) if err != nil { return err } default: return fmt.Errorf("usage: sed pattern [file]") } expr := args[0] if expr == "" || expr[0] != 's' { return fmt.Errorf("unimplemented") } sep := expr[1] expr = expr[2:] from := expr[:strings.IndexByte(expr, sep)] expr = expr[len(from)+1:] to := expr[:strings.IndexByte(expr, sep)] bs, err := io.ReadAll(f) if err != nil { return err } rx := regexp.MustCompile(from) bs = rx.ReplaceAllLiteral(bs, []byte(to)) _, err = hc.Stdout.Write(bs) return err }, "mkdir": func(hc HandlerContext, args []string) error { for _, arg := range args { if arg == "-p" { continue } path := absPath(hc.Dir, arg) if err := os.MkdirAll(path, 0o777); err != nil { return err } } return nil }, "rm": func(hc HandlerContext, args []string) error { for _, arg := range args { if arg == "-r" { continue } path := absPath(hc.Dir, arg) if err := os.RemoveAll(path); err != nil { return err } } return nil }, "ln": func(hc HandlerContext, args []string) error { symbolic := args[0] == "-s" if symbolic { args = args[1:] } oldname := absPath(hc.Dir, args[0]) newname := absPath(hc.Dir, args[1]) if symbolic { return os.Symlink(oldname, newname) } return os.Link(oldname, newname) }, "touch": func(hc HandlerContext, args []string) error { newTime := time.Now() if args[0] == "-d" { if !strings.HasPrefix(args[1], "@") { return fmt.Errorf("unimplemented") } sec, err := strconv.ParseInt(args[1][1:], 10, 64) if err != nil { return err } newTime = time.Unix(sec, 0) args = args[2:] } for _, arg := range args { path := absPath(hc.Dir, arg) // create the file if it does not exist f, err := os.OpenFile(path, os.O_CREATE, 0o666) if err != nil { return err } f.Close() // change the modification and access time if err := os.Chtimes(path, newTime, newTime); err != nil { return err } } return nil }, "sleep": func(hc HandlerContext, args []string) error { // Note that, unlike GNU sleep, we don't assume a default unit // of seconds. for _, arg := range args { d, err := time.ParseDuration(arg) if err != nil { return err } time.Sleep(d) } return nil }, } func testExecHandler(ctx context.Context, args []string) error { if fn := testBuiltinsMap[args[0]]; fn != nil { return fn(HandlerCtx(ctx), args[1:]) } return DefaultExecHandler(2*time.Second)(ctx, args) } func testOpenHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { if runtime.GOOS == "windows" && path == "/dev/null" { path = "NUL" } return DefaultOpenHandler()(ctx, path, flag, perm) } func TestRunnerRunConfirm(t *testing.T) { if testing.Short() { t.Skip("calling bash is slow") } if !hasBash50 { t.Skip("bash 5.0 required to run") } t.Parallel() if runtime.GOOS == "windows" { // For example, it seems to treat environment variables as // case-sensitive, which isn't how Windows works. t.Skip("bash on Windows emulates Unix-y behavior") } for _, c := range runTests { c := c t.Run("", func(t *testing.T) { if strings.Contains(c.want, " #IGNORE") { return } skipIfUnsupported(t, c.in) t.Parallel() tdir := t.TempDir() ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() cmd := exec.CommandContext(ctx, "bash") cmd.Dir = tdir cmd.Stdin = strings.NewReader(c.in) out, err := cmd.CombinedOutput() if strings.Contains(c.want, " #JUSTERR") { // bash sometimes exits with status code 0 and // stderr "bash: ..." for an error fauxErr := bytes.HasPrefix(out, []byte("bash:")) if err == nil && !fauxErr { t.Fatalf("wanted bash to error in %q", c.in) } return } got := string(out) if err != nil { got += err.Error() } if got != c.want { t.Fatalf("wrong bash output in %q:\nwant: %q\ngot: %q", c.in, c.want, got) } }) } } func TestRunnerOpts(t *testing.T) { t.Parallel() withPath := func(strs ...string) func(*Runner) error { prefix := []string{ "PATH=" + os.Getenv("PATH"), "ENV_PROG=" + os.Getenv("ENV_PROG"), } return Env(expand.ListEnviron(append(prefix, strs...)...)) } opts := func(list ...RunnerOption) []RunnerOption { return list } cases := []struct { opts []RunnerOption in, want string }{ { nil, "$ENV_PROG | grep '^INTERP_GLOBAL='", "INTERP_GLOBAL=value\n", }, { opts(withPath()), "$ENV_PROG | grep '^INTERP_GLOBAL='", "exit status 1", }, { opts(withPath("INTERP_GLOBAL=bar")), "$ENV_PROG | grep '^INTERP_GLOBAL='", "INTERP_GLOBAL=bar\n", }, { opts(withPath("a=b")), "echo $a", "b\n", }, { opts(withPath("A=b")), "$ENV_PROG | grep '^A='; echo $A", "A=b\nb\n", }, { opts(withPath("A=b", "A=c")), "$ENV_PROG | grep '^A='; echo $A", "A=c\nc\n", }, { opts(withPath("HOME=")), "echo $HOME", "\n", }, { opts(withPath("PWD=foo")), "[[ $PWD == foo ]]", "exit status 1", }, { opts(Params("foo")), "echo $@", "foo\n", }, { opts(Params("-u", "--", "foo")), "echo $@; echo $unset", "foo\nunset: unbound variable\nexit status 1", }, { opts(Params("-u", "--", "foo")), "echo $@; echo ${unset:-default}", "foo\ndefault\n", }, { opts(Params("foo")), "set >/dev/null; echo $@", "foo\n", }, { opts(Params("foo")), "set -e; echo $@", "foo\n", }, { opts(Params("foo")), "set --; echo $@", "\n", }, { opts(Params("foo")), "set bar; echo $@", "bar\n", }, } p := syntax.NewParser() for _, c := range cases { t.Run("", func(t *testing.T) { skipIfUnsupported(t, c.in) file := parse(t, p, c.in) var cb concBuffer r, err := New(append(c.opts, StdIO(nil, &cb, &cb), OpenHandler(testOpenHandler), ExecHandler(testExecHandler), )...) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r.Run(ctx, file); err != nil { cb.WriteString(err.Error()) } if got := cb.String(); got != c.want { t.Fatalf("wrong output in %q:\nwant: %q\ngot: %q", c.in, c.want, got) } }) } } func TestRunnerContext(t *testing.T) { t.Parallel() cases := []string{ "", "while true; do true; done", "until false; do true; done", "sleep 1000", "while true; do true; done & wait", "sleep 1000 & wait", "(while true; do true; done)", "$(while true; do true; done)", "while true; do true; done | while true; do true; done", } p := syntax.NewParser() for _, in := range cases { t.Run("", func(t *testing.T) { file := parse(t, p, in) ctx, cancel := context.WithCancel(context.Background()) cancel() r, _ := New() errChan := make(chan error) go func() { errChan <- r.Run(ctx, file) }() timeout := 500 * time.Millisecond select { case err := <-errChan: if err != nil && err != ctx.Err() { t.Fatal("Runner did not use ctx.Err()") } case <-time.After(timeout): t.Fatalf("program was not killed in %s", timeout) } }) } } func TestRunnerAltNodes(t *testing.T) { t.Parallel() in := "echo foo" file := parse(t, nil, in) want := "foo\n" nodes := []syntax.Node{ file, file.Stmts[0], file.Stmts[0].Cmd, } for _, node := range nodes { var cb concBuffer r, _ := New(StdIO(nil, &cb, &cb)) ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r.Run(ctx, node); err != nil { cb.WriteString(err.Error()) } if got := cb.String(); got != want { t.Fatalf("wrong output in %q:\nwant: %q\ngot: %q", in, want, got) } } } func TestElapsedString(t *testing.T) { t.Parallel() tests := []struct { in time.Duration posix bool want string }{ {time.Nanosecond, false, "0m0.000s"}, {time.Millisecond, false, "0m0.001s"}, {time.Millisecond, true, "0.00"}, {2500 * time.Millisecond, false, "0m2.500s"}, {2500 * time.Millisecond, true, "2.50"}, { 10*time.Minute + 10*time.Second, false, "10m10.000s", }, { 10*time.Minute + 10*time.Second, true, "610.00", }, {31 * time.Second, false, "0m31.000s"}, {102 * time.Second, false, "1m42.000s"}, } for _, tc := range tests { t.Run(tc.in.String(), func(t *testing.T) { got := elapsedString(tc.in, tc.posix) if got != tc.want { t.Fatalf("wanted %q, got %q", tc.want, got) } }) } } func TestRunnerDir(t *testing.T) { t.Parallel() wd, err := os.Getwd() if err != nil { t.Fatal(err) } t.Run("Missing", func(t *testing.T) { _, err := New(Dir("missing")) if err == nil { t.Fatal("expected New to error when Dir is missing") } }) t.Run("NotDir", func(t *testing.T) { _, err := New(Dir("interp_test.go")) if err == nil { t.Fatal("expected New to error when Dir is not a dir") } }) t.Run("NotDirAbs", func(t *testing.T) { _, err := New(Dir(filepath.Join(wd, "interp_test.go"))) if err == nil { t.Fatal("expected New to error when Dir is not a dir") } }) t.Run("Relative", func(t *testing.T) { // On Windows, it's impossible to make a relative path from one // drive to another. Use the parent directory, as that's for // sure in the same drive as the current directory. rel := ".." + string(filepath.Separator) r, err := New(Dir(rel)) if err != nil { t.Fatal(err) } if !filepath.IsAbs(r.Dir) { t.Errorf("Runner.Dir is not absolute") } }) // Ensure that we treat symlinks and short paths properly, especially // with Dir and globbing. t.Run("SymlinkOrShortPath", func(t *testing.T) { tdir := t.TempDir() realDir := filepath.Join(tdir, "real-long-dir-name") realFile := filepath.Join(realDir, "realfile") if err := os.Mkdir(realDir, 0o777); err != nil { t.Fatal(err) } if err := os.WriteFile(realFile, []byte(""), 0o666); err != nil { t.Fatal(err) } var altDir string if runtime.GOOS == "windows" { short, err := shortPathName(realDir) if err != nil { t.Fatal(err) } altDir = short // We replace tdir later, and it might have been shortened. tdir = filepath.Dir(altDir) } else { altDir = filepath.Join(tdir, "symlink") if err := os.Symlink(realDir, altDir); err != nil { t.Fatal(err) } } var b bytes.Buffer r, err := New(Dir(altDir), StdIO(nil, &b, &b)) if err != nil { t.Fatal(err) } file := parse(t, nil, "echo $PWD $PWD/*") ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r.Run(ctx, file); err != nil { t.Fatal(err) } got := b.String() got = strings.ReplaceAll(got, tdir, "") got = strings.TrimSpace(got) want := `/symlink /symlink/realfile` if runtime.GOOS == "windows" { want = `\\REAL.{4} \\REAL.{4}\\realfile` } if !regexp.MustCompile(want).MatchString(got) { t.Fatalf("\nwant regexp: %q\ngot: %q", want, got) } }) } func TestRunnerIncremental(t *testing.T) { t.Parallel() file := parse(t, nil, "echo foo; false; echo bar; exit 0; echo baz") want := "foo\nbar\n" var b bytes.Buffer r, _ := New(StdIO(nil, &b, &b)) ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() for _, stmt := range file.Stmts { err := r.Run(ctx, stmt) if _, ok := IsExitStatus(err); !ok && err != nil { // Keep track of unexpected errors. b.WriteString(err.Error()) } if r.Exited() { break } } if got := b.String(); got != want { t.Fatalf("\nwant: %q\ngot: %q", want, got) } } func TestRunnerResetFields(t *testing.T) { t.Parallel() tdir := t.TempDir() logPath := filepath.Join(tdir, "log") logFile, err := os.Create(logPath) if err != nil { t.Fatal(err) } defer logFile.Close() r, _ := New( Params("-f", "--", "first", tdir, logPath), Dir(tdir), OpenHandler(testOpenHandler), ExecHandler(testExecHandler), ) // Check that using option funcs and Runner fields directly is still // kept by Reset. StdIO(nil, logFile, os.Stderr)(r) r.Env = expand.ListEnviron(append(os.Environ(), "GLOBAL=foo")...) file := parse(t, nil, ` # Params set 3 arguments [[ $# -eq 3 ]] || exit 10 [[ $1 == "first" ]] || exit 11 # Params set the -f option (noglob) [[ -o noglob ]] || exit 12 # $PWD was set via Dir, and should be equal to $2 [[ "$PWD" == "$2" ]] || exit 13 # stdout should go into the log file, which is at $3 echo line1 echo line2 [[ "$(wc -l <$3)" == "2" ]] || exit 14 # $GLOBAL was set directly via the Env field [[ "$GLOBAL" == "foo" ]] || exit 15 # Change all of the above within the script. Reset should undo this. set +f -- newargs cd exec >/dev/null 2>/dev/null GLOBAL= export GLOBAL= `) ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() for i := 0; i < 3; i++ { if err := r.Run(ctx, file); err != nil { t.Fatalf("run number %d: %v", i, err) } r.Reset() // empty the log file too logFile.Truncate(0) logFile.Seek(0, io.SeekStart) } } func TestRunnerManyResets(t *testing.T) { t.Parallel() r, _ := New() for i := 0; i < 5; i++ { r.Reset() } } func TestRunnerFilename(t *testing.T) { t.Parallel() want := "f.sh\n" file, _ := syntax.NewParser().Parse(strings.NewReader("echo $0"), "f.sh") var b bytes.Buffer r, _ := New(StdIO(nil, &b, &b)) ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r.Run(ctx, file); err != nil { t.Fatal(err) } if got := b.String(); got != want { t.Fatalf("\nwant: %q\ngot: %q", want, got) } } func TestRunnerEnvNoModify(t *testing.T) { t.Parallel() env := expand.ListEnviron("one=1", "two=2") file := parse(t, nil, `echo -n "$one $two; "; one=x; unset two`) var b bytes.Buffer r, _ := New(Env(env), StdIO(nil, &b, &b)) ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() for i := 0; i < 3; i++ { r.Reset() err := r.Run(ctx, file) if err != nil { t.Fatal(err) } } want := "1 2; 1 2; 1 2; " if got := b.String(); got != want { t.Fatalf("\nwant: %q\ngot: %q", want, got) } } func TestMalformedPathOnWindows(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("Skipping windows test on non-windows GOOS") } tdir := t.TempDir() t.Parallel() path := filepath.Join(tdir, "test.cmd") script := []byte("@echo foo") if err := os.WriteFile(path, script, 0o777); err != nil { t.Fatal(err) } // set PATH to c:\tmp\dir instead of C:\tmp\dir volume := filepath.VolumeName(tdir) pathList := strings.ToLower(volume) + tdir[len(volume):] file := parse(t, nil, "test.cmd") var cb concBuffer r, _ := New(Env(expand.ListEnviron("PATH="+pathList)), StdIO(nil, &cb, &cb)) ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r.Run(ctx, file); err != nil { t.Fatal(err) } want := "foo\r\n" if got := cb.String(); got != want { t.Fatalf("wrong output:\nwant: %q\ngot: %q", want, got) } } func TestReadShouldNotPanicWithNilStdin(t *testing.T) { t.Parallel() r, err := New() if err != nil { t.Fatal(err) } f := parse(t, nil, "read foobar") ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r.Run(ctx, f); err == nil { t.Fatal("it should have retuned an error") } } func TestRunnerVars(t *testing.T) { t.Parallel() r, err := New() if err != nil { t.Fatal(err) } f := parse(t, nil, "FOO=updated; BAR=new") ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r.Run(ctx, f); err != nil { t.Fatal(err) } if want, got := "updated", r.Vars["FOO"].String(); got != want { t.Fatalf("wrong output:\nwant: %q\ngot: %q", want, got) } } func TestRunnerSubshell(t *testing.T) { t.Parallel() r1, err := New() if err != nil { t.Fatal(err) } r2 := r1.Subshell() f1 := parse(t, nil, "PARENT=foo") f2 := parse(t, nil, "CHILD=bar") ctx, cancel := context.WithTimeout(context.Background(), runnerRunTimeout) defer cancel() if err := r1.Run(ctx, f1); err != nil { t.Fatal(err) } if err := r2.Run(ctx, f2); err != nil { t.Fatal(err) } if want, got := "foo", r1.Vars["PARENT"].String(); got != want { t.Fatalf("wrong output:\nwant: %q\ngot: %q", want, got) } if want, got := "bar", r2.Vars["CHILD"].String(); got != want { t.Fatalf("wrong output:\nwant: %q\ngot: %q", want, got) } r3 := r2.Subshell() f3 := parse(t, nil, "CHILD=modified") if err := r3.Run(ctx, f3); err != nil { t.Fatal(err) } if want, got := "bar", r2.Vars["CHILD"].String(); got != want { t.Fatalf("wrong output:\nwant: %q\ngot: %q", want, got) } if want, got := "modified", r3.Vars["CHILD"].String(); got != want { t.Fatalf("wrong output:\nwant: %q\ngot: %q", want, got) } } sh-3.4.3/interp/os_unix.go000066400000000000000000000020631420425111700154450ustar00rootroot00000000000000// Copyright (c) 2017, Andrey Nering // See LICENSE for licensing information //go:build !windows // +build !windows package interp import ( "os" "os/user" "strconv" "syscall" "golang.org/x/sys/unix" ) func mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } // hasPermissionToDir returns if the OS current user has execute permission // to the given directory func hasPermissionToDir(info os.FileInfo) bool { user, err := user.Current() if err != nil { return true } uid, _ := strconv.Atoi(user.Uid) // super-user if uid == 0 { return true } st, _ := info.Sys().(*syscall.Stat_t) if st == nil { return true } perm := info.Mode().Perm() // user (u) if perm&0o100 != 0 && st.Uid == uint32(uid) { return true } gid, _ := strconv.Atoi(user.Gid) // other users in group (g) if perm&0o010 != 0 && st.Uid != uint32(uid) && st.Gid == uint32(gid) { return true } // remaining users (o) if perm&0o001 != 0 && st.Uid != uint32(uid) && st.Gid != uint32(gid) { return true } return false } sh-3.4.3/interp/os_windows.go000066400000000000000000000005221420425111700161520ustar00rootroot00000000000000// Copyright (c) 2017, Andrey Nering // See LICENSE for licensing information package interp import ( "fmt" "os" ) func mkfifo(path string, mode uint32) error { return fmt.Errorf("unsupported") } // hasPermissionToDir is a no-op on Windows. func hasPermissionToDir(info os.FileInfo) bool { return true } sh-3.4.3/interp/runner.go000066400000000000000000000443741420425111700153050ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package interp import ( "bytes" "context" "fmt" "io" "io/ioutil" "math" "math/rand" "os" "regexp" "runtime" "strings" "sync" "time" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/pattern" "mvdan.cc/sh/v3/syntax" ) func (r *Runner) fillExpandConfig(ctx context.Context) { r.ectx = ctx r.ecfg = &expand.Config{ Env: expandEnv{r}, CmdSubst: func(w io.Writer, cs *syntax.CmdSubst) error { switch len(cs.Stmts) { case 0: // nothing to do return nil case 1: // $( 100 { return "", fmt.Errorf("giving up at creating fifo: %v", err) } } r2 := r.Subshell() stdout := r.origStdout r.wgProcSubsts.Add(1) go func() { defer r.wgProcSubsts.Done() switch ps.Op { case syntax.CmdIn: f, err := os.OpenFile(path, os.O_WRONLY, 0) if err != nil { r.errf("cannot open fifo for stdout: %v", err) return } r2.stdout = f defer func() { if err := f.Close(); err != nil { r.errf("closing stdout fifo: %v", err) } os.Remove(path) }() default: // syntax.CmdOut f, err := os.OpenFile(path, os.O_RDONLY, 0) if err != nil { r.errf("cannot open fifo for stdin: %v", err) return } r2.stdin = f r2.stdout = stdout defer func() { f.Close() os.Remove(path) }() } r2.stmts(ctx, ps.Stmts) }() return path, nil }, } r.updateExpandOpts() } // catShortcutArg checks if a statement is of the form "$(, while , etc) // part of && or || lists // preceded by ! r.exitShell(ctx, r.exit) } else if r.exit != 0 { r.trapCallback(ctx, r.callbackErr, "error") } if !r.keepRedirs { r.stdin, r.stdout, r.stderr = oldIn, oldOut, oldErr } } func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { if r.stop(ctx) { return } switch x := cm.(type) { case *syntax.Block: r.stmts(ctx, x.Stmts) case *syntax.Subshell: r2 := r.Subshell() r2.stmts(ctx, x.Stmts) r.exit = r2.exit r.setErr(r2.err) case *syntax.CallExpr: // Use a new slice, to not modify the slice in the alias map. var args []*syntax.Word left := x.Args for len(left) > 0 && r.opts[optExpandAliases] { als, ok := r.alias[left[0].Lit()] if !ok { break } args = append(args, als.args...) left = left[1:] if !als.blank { break } } args = append(args, left...) fields := r.fields(args...) if len(fields) == 0 { for _, as := range x.Assigns { vr := r.assignVal(as, "") r.setVar(as.Name.Value, as.Index, vr) } break } type restoreVar struct { name string vr expand.Variable } var restores []restoreVar for _, as := range x.Assigns { name := as.Name.Value origVr := r.lookupVar(name) vr := r.assignVal(as, "") // Inline command vars are always exported. vr.Exported = true restores = append(restores, restoreVar{name, origVr}) r.setVarInternal(name, vr) } r.call(ctx, x.Args[0].Pos(), fields) for _, restore := range restores { r.setVarInternal(restore.name, restore.vr) } case *syntax.BinaryCmd: switch x.Op { case syntax.AndStmt, syntax.OrStmt: oldNoErrExit := r.noErrExit r.noErrExit = true r.stmt(ctx, x.X) r.noErrExit = oldNoErrExit if (r.exit == 0) == (x.Op == syntax.AndStmt) { r.stmt(ctx, x.Y) } case syntax.Pipe, syntax.PipeAll: pr, pw, err := os.Pipe() if err != nil { r.setErr(err) return } r2 := r.Subshell() r2.stdout = pw if x.Op == syntax.PipeAll { r2.stderr = pw } else { r2.stderr = r.stderr } r.stdin = pr var wg sync.WaitGroup wg.Add(1) go func() { r2.stmt(ctx, x.X) pw.Close() wg.Done() }() r.stmt(ctx, x.Y) pr.Close() wg.Wait() if r.opts[optPipeFail] && r2.exit != 0 && r.exit == 0 { r.exit = r2.exit } r.setErr(r2.err) } case *syntax.IfClause: oldNoErrExit := r.noErrExit r.noErrExit = true r.stmts(ctx, x.Cond) r.noErrExit = oldNoErrExit if r.exit == 0 { r.stmts(ctx, x.Then) break } r.exit = 0 if x.Else != nil { r.cmd(ctx, x.Else) } case *syntax.WhileClause: for !r.stop(ctx) { oldNoErrExit := r.noErrExit r.noErrExit = true r.stmts(ctx, x.Cond) r.noErrExit = oldNoErrExit stop := (r.exit == 0) == x.Until r.exit = 0 if stop || r.loopStmtsBroken(ctx, x.Do) { break } } case *syntax.ForClause: switch y := x.Loop.(type) { case *syntax.WordIter: name := y.Name.Value items := r.Params // for i; do ... if y.InPos.IsValid() { items = r.fields(y.Items...) // for i in ...; do ... } for _, field := range items { r.setVarString(name, field) if r.loopStmtsBroken(ctx, x.Do) { break } } case *syntax.CStyleLoop: if y.Init != nil { r.arithm(y.Init) } for y.Cond == nil || r.arithm(y.Cond) != 0 { if r.exit != 0 || r.loopStmtsBroken(ctx, x.Do) { break } if y.Post != nil { r.arithm(y.Post) } } } case *syntax.FuncDecl: r.setFunc(x.Name.Value, x.Body) case *syntax.ArithmCmd: r.exit = oneIf(r.arithm(x.X) == 0) case *syntax.LetClause: var val int for _, expr := range x.Exprs { val = r.arithm(expr) } r.exit = oneIf(val == 0) case *syntax.CaseClause: str := r.literal(x.Word) for _, ci := range x.Items { for _, word := range ci.Patterns { pattern := r.pattern(word) if match(pattern, str) { r.stmts(ctx, ci.Stmts) return } } } case *syntax.TestClause: if r.bashTest(ctx, x.X, false) == "" && r.exit == 0 { // to preserve exit status code 2 for regex errors, etc r.exit = 1 } case *syntax.DeclClause: local, global := false, false var modes []string valType := "" switch x.Variant.Value { case "declare": // When used in a function, "declare" acts as "local" // unless the "-g" option is used. local = r.inFunc case "local": if !r.inFunc { r.errf("local: can only be used in a function\n") r.exit = 1 return } local = true case "export": modes = append(modes, "-x") case "readonly": modes = append(modes, "-r") case "nameref": valType = "-n" } for _, as := range x.Args { for _, as := range r.flattenAssign(as) { name := as.Name.Value if strings.HasPrefix(name, "-") { switch name { case "-x", "-r": modes = append(modes, name) case "-a", "-A", "-n": valType = name case "-g": global = true default: r.errf("declare: invalid option %q\n", name) r.exit = 2 return } continue } if !syntax.ValidName(name) { r.errf("declare: invalid name %q\n", name) r.exit = 1 return } var vr expand.Variable if !as.Naked { vr = r.assignVal(as, valType) } if global { vr.Local = false } else if local { vr.Local = true } for _, mode := range modes { switch mode { case "-x": vr.Exported = true case "-r": vr.ReadOnly = true } } if as.Naked { if vr.Exported || vr.Local || vr.ReadOnly { r.setVarInternal(name, vr) } } else { r.setVar(name, as.Index, vr) } } } case *syntax.TimeClause: start := time.Now() if x.Stmt != nil { r.stmt(ctx, x.Stmt) } format := "%s\t%s\n" if x.PosixFormat { format = "%s %s\n" } else { r.outf("\n") } real := time.Since(start) r.outf(format, "real", elapsedString(real, x.PosixFormat)) // TODO: can we do these? r.outf(format, "user", elapsedString(0, x.PosixFormat)) r.outf(format, "sys", elapsedString(0, x.PosixFormat)) default: panic(fmt.Sprintf("unhandled command node: %T", x)) } } func (r *Runner) trapCallback(ctx context.Context, callback, name string) { if callback == "" { return // nothing to do } if r.handlingTrap { return // don't recurse, as that could lead to cycles } r.handlingTrap = true p := syntax.NewParser() // TODO: do this parsing when "trap" is called? file, err := p.Parse(strings.NewReader(callback), name+" trap") if err != nil { r.errf(name+"trap: %v\n", err) // ignore errors in the callback return } r.stmts(ctx, file.Stmts) r.handlingTrap = false } // setExit call this function to exit the shell with status func (r *Runner) exitShell(ctx context.Context, status int) { if status != 0 { r.trapCallback(ctx, r.callbackErr, "error") } r.trapCallback(ctx, r.callbackExit, "exit") r.shellExited = true // Restore the original exit status. We ignore the callbacks. r.exit = status } func (r *Runner) flattenAssign(as *syntax.Assign) []*syntax.Assign { // Convert "declare $x" into "declare value". // Don't use syntax.Parser here, as we only want the basic // splitting by '='. if as.Name != nil { return []*syntax.Assign{as} // nothing to do } var asgns []*syntax.Assign for _, field := range r.fields(as.Value) { as := &syntax.Assign{} parts := strings.SplitN(field, "=", 2) as.Name = &syntax.Lit{Value: parts[0]} if len(parts) == 1 { as.Naked = true } else { as.Value = &syntax.Word{Parts: []syntax.WordPart{ &syntax.Lit{Value: parts[1]}, }} } asgns = append(asgns, as) } return asgns } func match(pat, name string) bool { expr, err := pattern.Regexp(pat, 0) if err != nil { return false } rx := regexp.MustCompile("^" + expr + "$") return rx.MatchString(name) } func elapsedString(d time.Duration, posix bool) string { if posix { return fmt.Sprintf("%.2f", d.Seconds()) } min := int(d.Minutes()) sec := math.Mod(d.Seconds(), 60.0) return fmt.Sprintf("%dm%.3fs", min, sec) } func (r *Runner) stmts(ctx context.Context, stmts []*syntax.Stmt) { for _, stmt := range stmts { r.stmt(ctx, stmt) } } func (r *Runner) hdocReader(rd *syntax.Redirect) io.Reader { if rd.Op != syntax.DashHdoc { hdoc := r.document(rd.Hdoc) return strings.NewReader(hdoc) } var buf bytes.Buffer var cur []syntax.WordPart flushLine := func() { if buf.Len() > 0 { buf.WriteByte('\n') } buf.WriteString(r.document(&syntax.Word{Parts: cur})) cur = cur[:0] } for _, wp := range rd.Hdoc.Parts { lit, ok := wp.(*syntax.Lit) if !ok { cur = append(cur, wp) continue } for i, part := range strings.Split(lit.Value, "\n") { if i > 0 { flushLine() cur = cur[:0] } part = strings.TrimLeft(part, "\t") cur = append(cur, &syntax.Lit{Value: part}) } } flushLine() return &buf } func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, error) { if rd.Hdoc != nil { r.stdin = r.hdocReader(rd) return nil, nil } orig := &r.stdout if rd.N != nil { switch rd.N.Value { case "1": case "2": orig = &r.stderr } } arg := r.literal(rd.Word) switch rd.Op { case syntax.WordHdoc: r.stdin = strings.NewReader(arg + "\n") return nil, nil case syntax.DplOut: switch arg { case "1": *orig = r.stdout case "2": *orig = r.stderr } return nil, nil case syntax.RdrIn, syntax.RdrOut, syntax.AppOut, syntax.RdrAll, syntax.AppAll: // done further below // case syntax.DplIn: default: panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op)) } mode := os.O_RDONLY switch rd.Op { case syntax.AppOut, syntax.AppAll: mode = os.O_WRONLY | os.O_CREATE | os.O_APPEND case syntax.RdrOut, syntax.RdrAll: mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC } f, err := r.open(ctx, arg, mode, 0o644, true) if err != nil { return nil, err } switch rd.Op { case syntax.RdrIn: r.stdin = f case syntax.RdrOut, syntax.AppOut: *orig = f case syntax.RdrAll, syntax.AppAll: r.stdout = f r.stderr = f default: panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op)) } return f, nil } func (r *Runner) loopStmtsBroken(ctx context.Context, stmts []*syntax.Stmt) bool { oldInLoop := r.inLoop r.inLoop = true defer func() { r.inLoop = oldInLoop }() for _, stmt := range stmts { r.stmt(ctx, stmt) if r.contnEnclosing > 0 { r.contnEnclosing-- return r.contnEnclosing > 0 } if r.breakEnclosing > 0 { r.breakEnclosing-- return true } } return false } type returnStatus uint8 func (s returnStatus) Error() string { return fmt.Sprintf("return status %d", s) } func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) { if r.stop(ctx) { return } name := args[0] if body := r.Funcs[name]; body != nil { // stack them to support nested func calls oldParams := r.Params r.Params = args[1:] oldInFunc := r.inFunc r.inFunc = true // Functions run in a nested scope. // Note that Runner.exec below does something similar. origEnv := r.writeEnv r.writeEnv = &overlayEnviron{parent: r.writeEnv, funcScope: true} r.stmt(ctx, body) r.writeEnv = origEnv r.Params = oldParams r.inFunc = oldInFunc if code, ok := r.err.(returnStatus); ok { r.err = nil r.exit = int(code) } return } if isBuiltin(name) { r.exit = r.builtinCode(ctx, pos, name, args[1:]) return } r.exec(ctx, args) } func (r *Runner) exec(ctx context.Context, args []string) { err := r.execHandler(r.handlerCtx(ctx), args) if status, ok := IsExitStatus(err); ok { r.exit = int(status) return } if err != nil { // handler's custom fatal error r.setErr(err) return } r.exit = 0 } func (r *Runner) open(ctx context.Context, path string, flags int, mode os.FileMode, print bool) (io.ReadWriteCloser, error) { f, err := r.openHandler(r.handlerCtx(ctx), path, flags, mode) // TODO: support wrapped PathError returned from openHandler. switch err.(type) { case nil: case *os.PathError: if print { r.errf("%v\n", err) } default: // handler's custom fatal error r.setErr(err) } return f, err } func (r *Runner) stat(name string) (os.FileInfo, error) { return os.Stat(r.absPath(name)) } sh-3.4.3/interp/test.go000066400000000000000000000116031420425111700147400ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package interp import ( "context" "fmt" "os" "os/exec" "regexp" "golang.org/x/term" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) // non-empty string is true, empty string is false func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic bool) string { switch x := expr.(type) { case *syntax.Word: if classic { // In the classic "test" mode, we already expanded and // split the list of words, so don't redo that work. return r.document(x) } return r.literal(x) case *syntax.ParenTest: return r.bashTest(ctx, x.X, classic) case *syntax.BinaryTest: switch x.Op { case syntax.TsMatchShort, syntax.TsMatch, syntax.TsNoMatch: str := r.literal(x.X.(*syntax.Word)) yw := x.Y.(*syntax.Word) if classic { // test, [ lit := r.literal(yw) if (str == lit) == (x.Op != syntax.TsNoMatch) { return "1" } } else { // [[ pattern := r.pattern(yw) if match(pattern, str) == (x.Op != syntax.TsNoMatch) { return "1" } } return "" } if r.binTest(x.Op, r.bashTest(ctx, x.X, classic), r.bashTest(ctx, x.Y, classic)) { return "1" } return "" case *syntax.UnaryTest: if r.unTest(ctx, x.Op, r.bashTest(ctx, x.X, classic)) { return "1" } return "" } return "" } func (r *Runner) binTest(op syntax.BinTestOperator, x, y string) bool { switch op { case syntax.TsReMatch: re, err := regexp.Compile(y) if err != nil { r.exit = 2 return false } return re.MatchString(x) case syntax.TsNewer: info1, err1 := r.stat(x) info2, err2 := r.stat(y) if err1 != nil || err2 != nil { return false } return info1.ModTime().After(info2.ModTime()) case syntax.TsOlder: info1, err1 := r.stat(x) info2, err2 := r.stat(y) if err1 != nil || err2 != nil { return false } return info1.ModTime().Before(info2.ModTime()) case syntax.TsDevIno: info1, err1 := r.stat(x) info2, err2 := r.stat(y) if err1 != nil || err2 != nil { return false } return os.SameFile(info1, info2) case syntax.TsEql: return atoi(x) == atoi(y) case syntax.TsNeq: return atoi(x) != atoi(y) case syntax.TsLeq: return atoi(x) <= atoi(y) case syntax.TsGeq: return atoi(x) >= atoi(y) case syntax.TsLss: return atoi(x) < atoi(y) case syntax.TsGtr: return atoi(x) > atoi(y) case syntax.AndTest: return x != "" && y != "" case syntax.OrTest: return x != "" || y != "" case syntax.TsBefore: return x < y default: // syntax.TsAfter return x > y } } func (r *Runner) statMode(name string, mode os.FileMode) bool { info, err := r.stat(name) return err == nil && info.Mode()&mode != 0 } func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) bool { switch op { case syntax.TsExists: _, err := r.stat(x) return err == nil case syntax.TsRegFile: info, err := r.stat(x) return err == nil && info.Mode().IsRegular() case syntax.TsDirect: return r.statMode(x, os.ModeDir) case syntax.TsCharSp: return r.statMode(x, os.ModeCharDevice) case syntax.TsBlckSp: info, err := r.stat(x) return err == nil && info.Mode()&os.ModeDevice != 0 && info.Mode()&os.ModeCharDevice == 0 case syntax.TsNmPipe: return r.statMode(x, os.ModeNamedPipe) case syntax.TsSocket: return r.statMode(x, os.ModeSocket) case syntax.TsSmbLink: info, err := os.Lstat(r.absPath(x)) return err == nil && info.Mode()&os.ModeSymlink != 0 case syntax.TsSticky: return r.statMode(x, os.ModeSticky) case syntax.TsUIDSet: return r.statMode(x, os.ModeSetuid) case syntax.TsGIDSet: return r.statMode(x, os.ModeSetgid) // case syntax.TsGrpOwn: // case syntax.TsUsrOwn: // case syntax.TsModif: case syntax.TsRead: f, err := r.open(ctx, x, os.O_RDONLY, 0, false) if err == nil { f.Close() } return err == nil case syntax.TsWrite: f, err := r.open(ctx, x, os.O_WRONLY, 0, false) if err == nil { f.Close() } return err == nil case syntax.TsExec: _, err := exec.LookPath(r.absPath(x)) return err == nil case syntax.TsNoEmpty: info, err := r.stat(x) return err == nil && info.Size() > 0 case syntax.TsFdTerm: fd := atoi(x) var f interface{} switch fd { case 0: f = r.stdin case 1: f = r.stdout case 2: f = r.stderr } if f, ok := f.(interface{ Fd() uintptr }); ok { // Support Fd methods such as the one on *os.File. return term.IsTerminal(int(f.Fd())) } // TODO: allow term.IsTerminal here too if running in the // "single process" mode. return false case syntax.TsEmpStr: return x == "" case syntax.TsNempStr: return x != "" case syntax.TsOptSet: if opt := r.optByName(x, false); opt != nil { return *opt } return false case syntax.TsVarSet: return r.lookupVar(x).IsSet() case syntax.TsRefVar: return r.lookupVar(x).Kind == expand.NameRef case syntax.TsNot: return x == "" default: panic(fmt.Sprintf("unhandled unary test op: %v", op)) } } sh-3.4.3/interp/test_classic.go000066400000000000000000000071511420425111700164440ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package interp import ( "fmt" "mvdan.cc/sh/v3/syntax" ) const illegalTok = 0 type testParser struct { eof bool val string rem []string err func(err error) } func (p *testParser) errf(format string, a ...interface{}) { p.err(fmt.Errorf(format, a...)) } func (p *testParser) next() { if p.eof || len(p.rem) == 0 { p.eof = true p.val = "" return } p.val = p.rem[0] p.rem = p.rem[1:] } func (p *testParser) followWord(fval string) *syntax.Word { if p.eof { p.errf("%s must be followed by a word", fval) } w := &syntax.Word{Parts: []syntax.WordPart{ &syntax.Lit{Value: p.val}, }} p.next() return w } func (p *testParser) classicTest(fval string, pastAndOr bool) syntax.TestExpr { var left syntax.TestExpr if pastAndOr { left = p.testExprBase(fval) } else { left = p.classicTest(fval, true) } if left == nil || p.eof { return left } opStr := p.val op := testBinaryOp(p.val) if op == illegalTok { p.errf("not a valid test operator: %s", p.val) } b := &syntax.BinaryTest{ Op: op, X: left, } p.next() switch b.Op { case syntax.AndTest, syntax.OrTest: if b.Y = p.classicTest(opStr, false); b.Y == nil { p.errf("%s must be followed by an expression", opStr) } default: b.Y = p.followWord(opStr) } return b } func (p *testParser) testExprBase(fval string) syntax.TestExpr { if p.eof { return nil } op := testUnaryOp(p.val) switch op { case syntax.TsNot: u := &syntax.UnaryTest{Op: op} p.next() u.X = p.classicTest(op.String(), false) return u case illegalTok: return p.followWord(fval) default: u := &syntax.UnaryTest{Op: op} p.next() if p.eof { // make [ -e ] fall back to [ -n -e ], i.e. use // the operator as an argument return &syntax.Word{Parts: []syntax.WordPart{ &syntax.Lit{Value: op.String()}, }} } u.X = p.followWord(op.String()) return u } } // testUnaryOp is an exact copy of syntax's. func testUnaryOp(val string) syntax.UnTestOperator { switch val { case "!": return syntax.TsNot case "-e", "-a": return syntax.TsExists case "-f": return syntax.TsRegFile case "-d": return syntax.TsDirect case "-c": return syntax.TsCharSp case "-b": return syntax.TsBlckSp case "-p": return syntax.TsNmPipe case "-S": return syntax.TsSocket case "-L", "-h": return syntax.TsSmbLink case "-k": return syntax.TsSticky case "-g": return syntax.TsGIDSet case "-u": return syntax.TsUIDSet case "-G": return syntax.TsGrpOwn case "-O": return syntax.TsUsrOwn case "-N": return syntax.TsModif case "-r": return syntax.TsRead case "-w": return syntax.TsWrite case "-x": return syntax.TsExec case "-s": return syntax.TsNoEmpty case "-t": return syntax.TsFdTerm case "-z": return syntax.TsEmpStr case "-n": return syntax.TsNempStr case "-o": return syntax.TsOptSet case "-v": return syntax.TsVarSet case "-R": return syntax.TsRefVar default: return illegalTok } } // testBinaryOp is like syntax's, but with -a and -o, and without =~. func testBinaryOp(val string) syntax.BinTestOperator { switch val { case "-a": return syntax.AndTest case "-o": return syntax.OrTest case "==", "=": return syntax.TsMatch case "!=": return syntax.TsNoMatch case "-nt": return syntax.TsNewer case "-ot": return syntax.TsOlder case "-ef": return syntax.TsDevIno case "-eq": return syntax.TsEql case "-ne": return syntax.TsNeq case "-le": return syntax.TsLeq case "-ge": return syntax.TsGeq case "-lt": return syntax.TsLss case "-gt": return syntax.TsGtr default: return illegalTok } } sh-3.4.3/interp/unix_test.go000066400000000000000000000070571420425111700160130ustar00rootroot00000000000000// Copyright (c) 2019, Daniel Martí // See LICENSE for licensing information //go:build !windows // +build !windows package interp import ( "bufio" "context" "io" "os" "os/exec" "strings" "testing" "github.com/creack/pty" ) func TestRunnerTerminalStdIO(t *testing.T) { t.Parallel() tests := []struct { name string files func(*testing.T) (secondary io.Writer, primary io.Reader) want string }{ {"Nil", func(t *testing.T) (io.Writer, io.Reader) { return nil, strings.NewReader("\n") }, "\n"}, {"Pipe", func(t *testing.T) (io.Writer, io.Reader) { pr, pw := io.Pipe() return pw, pr }, "end\n"}, {"Pseudo", func(t *testing.T) (io.Writer, io.Reader) { primary, secondary, err := pty.Open() if err != nil { t.Fatal(err) } return secondary, primary }, "012end\r\n"}, } file := parse(t, nil, ` for n in 0 1 2 3; do if [[ -t $n ]]; then echo -n $n; fi; done; echo end `) for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() secondary, primary := test.files(t) // some secondary ends can be used as stdin too secondaryReader, _ := secondary.(io.Reader) r, _ := New(StdIO(secondaryReader, secondary, secondary)) go func() { // To mimic os/exec.Cmd.Start, use a goroutine. if err := r.Run(context.Background(), file); err != nil { t.Error(err) } }() got, err := bufio.NewReader(primary).ReadString('\n') if err != nil { t.Fatal(err) } if got != test.want { t.Fatalf("\nwant: %q\ngot: %q", test.want, got) } if closer, ok := secondary.(io.Closer); ok { if err := closer.Close(); err != nil { t.Fatal(err) } } if closer, ok := primary.(io.Closer); ok { if err := closer.Close(); err != nil { t.Fatal(err) } } }) } } func TestRunnerTerminalExec(t *testing.T) { t.Parallel() tests := []struct { name string start func(*testing.T, *exec.Cmd) io.Reader want string }{ {"Nil", func(t *testing.T, cmd *exec.Cmd) io.Reader { if err := cmd.Start(); err != nil { t.Fatal(err) } return strings.NewReader("\n") }, "\n"}, {"Pipe", func(t *testing.T, cmd *exec.Cmd) io.Reader { out, err := cmd.StdoutPipe() if err != nil { t.Fatal(err) } cmd.Stderr = cmd.Stdout if err := cmd.Start(); err != nil { t.Fatal(err) } return out }, "end\n"}, {"Pseudo", func(t *testing.T, cmd *exec.Cmd) io.Reader { // Note that we avoid pty.Start, // as it closes the secondary terminal via a defer, // possibly before the command has finished. // That can lead to "signal: hangup" flakes. primary, secondary, err := pty.Open() if err != nil { t.Fatal(err) } cmd.Stdin = secondary cmd.Stdout = secondary cmd.Stderr = secondary if err := cmd.Start(); err != nil { t.Fatal(err) } return primary }, "012end\r\n"}, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() cmd := exec.Command(os.Getenv("GOSH_PROG"), "for n in 0 1 2 3; do if [[ -t $n ]]; then echo -n $n; fi; done; echo end") primary := test.start(t, cmd) got, err := bufio.NewReader(primary).ReadString('\n') if err != nil { t.Fatal(err) } if got != test.want { t.Fatalf("\nwant: %q\ngot: %q", test.want, got) } if closer, ok := primary.(io.Closer); ok { if err := closer.Close(); err != nil { t.Fatal(err) } } if err := cmd.Wait(); err != nil { t.Fatal(err) } }) } } func shortPathName(path string) (string, error) { panic("only works on windows") } sh-3.4.3/interp/vars.go000066400000000000000000000215741420425111700147440ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package interp import ( "fmt" "os" "runtime" "strconv" "strings" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) type overlayEnviron struct { parent expand.Environ values map[string]expand.Variable // We need to know if the current scope is a function's scope, because // functions can modify global variables. funcScope bool } func (o *overlayEnviron) Get(name string) expand.Variable { if vr, ok := o.values[name]; ok { return vr } return o.parent.Get(name) } func (o *overlayEnviron) Set(name string, vr expand.Variable) error { // Manipulation of a global var inside a function if o.funcScope && !vr.Local && !o.values[name].Local { // "foo=bar" on a global var in a function updates the global scope if vr.IsSet() { return o.parent.(expand.WriteEnviron).Set(name, vr) } // "foo=bar" followed by "export foo" or "readonly foo" if vr.Exported || vr.ReadOnly { prev := o.Get(name) prev.Exported = prev.Exported || vr.Exported prev.ReadOnly = prev.ReadOnly || vr.ReadOnly vr = prev return o.parent.(expand.WriteEnviron).Set(name, vr) } // "unset" is handled below } prev := o.Get(name) if o.values == nil { o.values = make(map[string]expand.Variable) } if !vr.IsSet() && (vr.Exported || vr.Local || vr.ReadOnly) { // marking as exported/local/readonly prev.Exported = prev.Exported || vr.Exported prev.Local = prev.Local || vr.Local prev.ReadOnly = prev.ReadOnly || vr.ReadOnly vr = prev o.values[name] = vr return nil } if prev.ReadOnly { return fmt.Errorf("readonly variable") } if !vr.IsSet() { // unsetting if prev.Local { vr.Local = true o.values[name] = vr return nil } delete(o.values, name) if writeEnv, _ := o.parent.(expand.WriteEnviron); writeEnv != nil { writeEnv.Set(name, vr) return nil } } else if prev.Exported { // variable is set and was marked as exported vr.Exported = true } // modifying the entire variable vr.Local = prev.Local || vr.Local o.values[name] = vr return nil } func (o *overlayEnviron) Each(f func(name string, vr expand.Variable) bool) { o.parent.Each(f) for name, vr := range o.values { if !f(name, vr) { return } } } func execEnv(env expand.Environ) []string { list := make([]string, 0, 64) env.Each(func(name string, vr expand.Variable) bool { if !vr.IsSet() { // If a variable is set globally but unset in the // runner, we need to ensure it's not part of the final // list. Seems like zeroing the element is enough. // This is a linear search, but this scenario should be // rare, and the number of variables shouldn't be large. for i, kv := range list { if strings.HasPrefix(kv, name+"=") { list[i] = "" } } } if vr.Exported && vr.Kind == expand.String { list = append(list, name+"="+vr.String()) } return true }) return list } func (r *Runner) lookupVar(name string) expand.Variable { if name == "" { panic("variable name must not be empty") } var vr expand.Variable switch name { case "#": vr.Kind, vr.Str = expand.String, strconv.Itoa(len(r.Params)) case "@", "*": vr.Kind = expand.Indexed if r.Params == nil { // r.Params may be nil but positional parameters always exist vr.List = []string{} } else { vr.List = r.Params } case "?": vr.Kind, vr.Str = expand.String, strconv.Itoa(r.lastExit) case "$": vr.Kind, vr.Str = expand.String, strconv.Itoa(os.Getpid()) case "PPID": vr.Kind, vr.Str = expand.String, strconv.Itoa(os.Getppid()) case "DIRSTACK": vr.Kind, vr.List = expand.Indexed, r.dirStack case "0": vr.Kind = expand.String if r.filename != "" { vr.Str = r.filename } else { vr.Str = "gosh" } case "1", "2", "3", "4", "5", "6", "7", "8", "9": vr.Kind = expand.String i := int(name[0] - '1') if i < len(r.Params) { vr.Str = r.Params[i] } else { vr.Str = "" } } if vr.IsSet() { return vr } if vr := r.writeEnv.Get(name); vr.IsSet() { return vr } if runtime.GOOS == "windows" { upper := strings.ToUpper(name) if vr := r.writeEnv.Get(upper); vr.IsSet() { return vr } } return expand.Variable{} } func (r *Runner) envGet(name string) string { return r.lookupVar(name).String() } func (r *Runner) delVar(name string) { if err := r.writeEnv.Set(name, expand.Variable{}); err != nil { r.errf("%s: %v\n", name, err) r.exit = 1 return } } func (r *Runner) setVarString(name, value string) { r.setVar(name, nil, expand.Variable{Kind: expand.String, Str: value}) } func (r *Runner) setVarInternal(name string, vr expand.Variable) { if r.opts[optAllExport] { vr.Exported = true } if err := r.writeEnv.Set(name, vr); err != nil { r.errf("%s: %v\n", name, err) r.exit = 1 return } } func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable) { cur := r.lookupVar(name) if name2, var2 := cur.Resolve(r.writeEnv); name2 != "" { name = name2 cur = var2 } if vr.Kind == expand.String && index == nil { // When assigning a string to an array, fall back to the // zero value for the index. switch cur.Kind { case expand.Indexed: index = &syntax.Word{Parts: []syntax.WordPart{ &syntax.Lit{Value: "0"}, }} case expand.Associative: index = &syntax.Word{Parts: []syntax.WordPart{ &syntax.DblQuoted{}, }} } } if index == nil { r.setVarInternal(name, vr) return } // from the syntax package, we know that value must be a string if index // is non-nil; nested arrays are forbidden. valStr := vr.Str var list []string switch cur.Kind { case expand.String: list = append(list, cur.Str) case expand.Indexed: list = cur.List case expand.Associative: // if the existing variable is already an AssocArray, try our // best to convert the key to a string w, ok := index.(*syntax.Word) if !ok { return } k := r.literal(w) cur.Map[k] = valStr r.setVarInternal(name, cur) return } k := r.arithm(index) for len(list) < k+1 { list = append(list, "") } list[k] = valStr cur.Kind = expand.Indexed cur.List = list r.setVarInternal(name, cur) } func (r *Runner) setFunc(name string, body *syntax.Stmt) { if r.Funcs == nil { r.Funcs = make(map[string]*syntax.Stmt, 4) } r.Funcs[name] = body } func stringIndex(index syntax.ArithmExpr) bool { w, ok := index.(*syntax.Word) if !ok || len(w.Parts) != 1 { return false } switch w.Parts[0].(type) { case *syntax.DblQuoted, *syntax.SglQuoted: return true } return false } // TODO: make assignVal and setVar consistent with the WriteEnviron interface func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { prev := r.lookupVar(as.Name.Value) if as.Value != nil { s := r.literal(as.Value) if !as.Append || !prev.IsSet() { prev.Kind = expand.String if valType == "-n" { prev.Kind = expand.NameRef } prev.Str = s return prev } switch prev.Kind { case expand.String: prev.Str += s case expand.Indexed: if len(prev.List) == 0 { prev.List = append(prev.List, "") } prev.List[0] += s case expand.Associative: // TODO } return prev } if as.Array == nil { // don't return the zero value, as that's an unset variable prev.Kind = expand.String if valType == "-n" { prev.Kind = expand.NameRef } prev.Str = "" return prev } // Array assignment. elems := as.Array.Elems if valType == "" { valType = "-a" // indexed if len(elems) > 0 && stringIndex(elems[0].Index) { valType = "-A" // associative } } if valType == "-A" { amap := make(map[string]string, len(elems)) for _, elem := range elems { k := r.literal(elem.Index.(*syntax.Word)) amap[k] = r.literal(elem.Value) } if !as.Append { prev.Kind = expand.Associative prev.Map = amap return prev } // TODO return prev } // Evaluate values for each array element. elemValues := make([]struct { index int values []string }, len(elems)) var index, maxIndex int for i, elem := range elems { if elem.Index != nil { // Index resets our index with a literal value. index = r.arithm(elem.Index) elemValues[i].values = []string{r.literal(elem.Value)} } else { // Implicit index, advancing for every word. elemValues[i].values = r.fields(elem.Value) } elemValues[i].index = index index += len(elemValues[i].values) if index > maxIndex { maxIndex = index } } // Flatten down the values. strs := make([]string, maxIndex) for _, ev := range elemValues { for i, str := range ev.values { strs[ev.index+i] = str } } if !as.Append { prev.Kind = expand.Indexed prev.List = strs return prev } switch prev.Kind { case expand.Unset: prev.Kind = expand.Indexed prev.List = strs case expand.String: prev.Kind = expand.Indexed prev.List = append([]string{prev.Str}, strs...) case expand.Indexed: prev.List = append(prev.List, strs...) case expand.Associative: // TODO default: panic(fmt.Sprintf("unhandled conversion of kind %d", prev.Kind)) } return prev } sh-3.4.3/interp/windows_test.go000066400000000000000000000012301420425111700165050ustar00rootroot00000000000000// Copyright (c) 2019, Daniel Martí // See LICENSE for licensing information //go:build windows // +build windows package interp import "golang.org/x/sys/windows" // shortPathName is used for testing against DOS short names. // // Only used for testing, so we assume that a short path always fits in // 2*len(path) in UTF-16. func shortPathName(path string) (string, error) { src, err := windows.UTF16FromString(path) if err != nil { return "", err } dst := make([]uint16, len(src)*2) if _, err := windows.GetShortPathName(&src[0], &dst[0], uint32(len(dst))); err != nil { return "", err } return windows.UTF16ToString(dst), nil } sh-3.4.3/pattern/000077500000000000000000000000001420425111700136055ustar00rootroot00000000000000sh-3.4.3/pattern/example_test.go000066400000000000000000000015751420425111700166360ustar00rootroot00000000000000// Copyright (c) 2019, Daniel Martí // See LICENSE for licensing information package pattern_test import ( "fmt" "regexp" "mvdan.cc/sh/v3/pattern" ) func ExampleRegexp() { pat := "foo?bar*" fmt.Println(pat) expr, err := pattern.Regexp(pat, 0) if err != nil { return } fmt.Println(expr) rx := regexp.MustCompile(expr) fmt.Println(rx.MatchString("foo bar baz")) fmt.Println(rx.MatchString("foobarbaz")) // Output: // foo?bar* // foo.bar.* // true // false } func ExampleQuoteMeta() { pat := "foo?bar*" const mode = 0 fmt.Println(pat) quoted := pattern.QuoteMeta(pat, mode) fmt.Println(quoted) expr, err := pattern.Regexp(quoted, mode) if err != nil { return } rx := regexp.MustCompile(expr) fmt.Println(rx.MatchString("foo bar baz")) fmt.Println(rx.MatchString("foo?bar*")) // Output: // foo?bar* // foo\?bar\* // false // true } sh-3.4.3/pattern/pattern.go000066400000000000000000000166401420425111700156200ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information // Package pattern allows working with shell pattern matching notation, also // known as wildcards or globbing. // // For reference, see // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13. package pattern import ( "bytes" "fmt" "regexp" "strconv" "strings" ) // Mode can be used to supply a number of options to the package's functions. // Not all functions change their behavior with all of the options below. type Mode uint const ( Shortest Mode = 1 << iota // prefer the shortest match. Filenames // "*" and "?" don't match slashes; only "**" does Braces // support "{a,b}" and "{1..4}" ) var numRange = regexp.MustCompile(`^([+-]?\d+)\.\.([+-]?\d+)}`) // Regexp turns a shell pattern into a regular expression that can be used with // regexp.Compile. It will return an error if the input pattern was incorrect. // Otherwise, the returned expression can be passed to regexp.MustCompile. // // For example, Regexp(`foo*bar?`, true) returns `foo.*bar.`. // // Note that this function (and QuoteMeta) should not be directly used with file // paths if Windows is supported, as the path separator on that platform is the // same character as the escaping character for shell patterns. func Regexp(pat string, mode Mode) (string, error) { any := false noopLoop: for _, r := range pat { switch r { // including those that need escaping since they are // regular expression metacharacters case '*', '?', '[', '\\', '.', '+', '(', ')', '|', ']', '{', '}', '^', '$': any = true break noopLoop } } if !any { // short-cut without a string copy return pat, nil } closingBraces := []int{} var buf bytes.Buffer writeLoop: for i := 0; i < len(pat); i++ { switch c := pat[i]; c { case '*': if mode&Filenames != 0 { if i++; i < len(pat) && pat[i] == '*' { if i++; i < len(pat) && pat[i] == '/' { buf.WriteString("(.*/|)") } else { buf.WriteString(".*") i-- } } else { buf.WriteString("[^/]*") i-- } } else { buf.WriteString(".*") } if mode&Shortest != 0 { buf.WriteByte('?') } case '?': if mode&Filenames != 0 { buf.WriteString("[^/]") } else { buf.WriteByte('.') } case '\\': if i++; i >= len(pat) { return "", fmt.Errorf(`\ at end of pattern`) } buf.WriteString(regexp.QuoteMeta(string(pat[i]))) case '[': name, err := charClass(pat[i:]) if err != nil { return "", err } if name != "" { buf.WriteString(name) i += len(name) - 1 break } if mode&Filenames != 0 { for _, c := range pat[i:] { if c == ']' { break } else if c == '/' { buf.WriteString("\\[") continue writeLoop } } } buf.WriteByte(c) if i++; i >= len(pat) { return "", fmt.Errorf("[ was not matched with a closing ]") } switch c = pat[i]; c { case '!', '^': buf.WriteByte('^') if i++; i >= len(pat) { return "", fmt.Errorf("[ was not matched with a closing ]") } } if c = pat[i]; c == ']' { buf.WriteByte(']') if i++; i >= len(pat) { return "", fmt.Errorf("[ was not matched with a closing ]") } } rangeStart := byte(0) loopBracket: for ; i < len(pat); i++ { c = pat[i] buf.WriteByte(c) switch c { case '\\': if i++; i < len(pat) { buf.WriteByte(pat[i]) } continue case ']': break loopBracket } if rangeStart != 0 && rangeStart > c { return "", fmt.Errorf("invalid range: %c-%c", rangeStart, c) } if c == '-' { rangeStart = pat[i-1] } else { rangeStart = 0 } } if i >= len(pat) { return "", fmt.Errorf("[ was not matched with a closing ]") } case '{': if mode&Braces == 0 { buf.WriteString(regexp.QuoteMeta(string(c))) break } innerLevel := 1 commas := false peekBrace: for j := i + 1; j < len(pat); j++ { switch c := pat[j]; c { case '{': innerLevel++ case ',': commas = true case '\\': j++ case '}': if innerLevel--; innerLevel > 0 { continue } if !commas { break peekBrace } closingBraces = append(closingBraces, j) buf.WriteString("(?:") continue writeLoop } } if match := numRange.FindStringSubmatch(pat[i+1:]); len(match) == 3 { start, err1 := strconv.Atoi(match[1]) end, err2 := strconv.Atoi(match[2]) if err1 != nil || err2 != nil || start > end { return "", fmt.Errorf("invalid range: %q", match[0]) } // TODO: can we do better here? buf.WriteString("(?:") for n := start; n <= end; n++ { if n > start { buf.WriteByte('|') } fmt.Fprintf(&buf, "%d", n) } buf.WriteByte(')') i += len(match[0]) break } buf.WriteString(regexp.QuoteMeta(string(c))) case ',': if len(closingBraces) == 0 { buf.WriteString(regexp.QuoteMeta(string(c))) } else { buf.WriteByte('|') } case '}': if len(closingBraces) > 0 && closingBraces[len(closingBraces)-1] == i { buf.WriteByte(')') closingBraces = closingBraces[:len(closingBraces)-1] } else { buf.WriteString(regexp.QuoteMeta(string(c))) } default: if c > 128 { buf.WriteByte(c) } else { buf.WriteString(regexp.QuoteMeta(string(c))) } } } return buf.String(), nil } func charClass(s string) (string, error) { if strings.HasPrefix(s, "[[.") || strings.HasPrefix(s, "[[=") { return "", fmt.Errorf("collating features not available") } if !strings.HasPrefix(s, "[[:") { return "", nil } name := s[3:] end := strings.Index(name, ":]]") if end < 0 { return "", fmt.Errorf("[[: was not matched with a closing :]]") } name = name[:end] switch name { case "alnum", "alpha", "ascii", "blank", "cntrl", "digit", "graph", "lower", "print", "punct", "space", "upper", "word", "xdigit": default: return "", fmt.Errorf("invalid character class: %q", name) } return s[:len(name)+6], nil } // HasMeta returns whether a string contains any unescaped pattern // metacharacters: '*', '?', or '['. When the function returns false, the given // pattern can only match at most one string. // // For example, HasMeta(`foo\*bar`) returns false, but HasMeta(`foo*bar`) // returns true. // // This can be useful to avoid extra work, like TranslatePattern. Note that this // function cannot be used to avoid QuotePattern, as backslashes are quoted by // that function but ignored here. func HasMeta(pat string, mode Mode) bool { for i := 0; i < len(pat); i++ { switch pat[i] { case '\\': i++ case '*', '?', '[': return true case '{': if mode&Braces != 0 { return true } } } return false } // QuoteMeta returns a string that quotes all pattern metacharacters in the // given text. The returned string is a pattern that matches the literal text. // // For example, QuoteMeta(`foo*bar?`) returns `foo\*bar\?`. func QuoteMeta(pat string, mode Mode) string { any := false loop: for _, r := range pat { switch r { case '{': if mode&Braces == 0 { continue } fallthrough case '*', '?', '[', '\\': any = true break loop } } if !any { // short-cut without a string copy return pat } var buf bytes.Buffer for _, r := range pat { switch r { case '*', '?', '[', '\\': buf.WriteByte('\\') case '{': if mode&Braces != 0 { buf.WriteByte('\\') } } buf.WriteRune(r) } return buf.String() } sh-3.4.3/pattern/pattern_test.go000066400000000000000000000077501420425111700166610ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package pattern import ( "fmt" "regexp/syntax" "testing" ) var translateTests = []struct { pat string mode Mode want string wantErr bool }{ {pat: ``, want: ``}, {pat: `foo`, want: `foo`}, {pat: `foóà中`, mode: Filenames | Braces, want: `foóà中`}, {pat: `.`, want: `\.`}, {pat: `foo*`, want: `foo.*`}, {pat: `foo*`, mode: Shortest, want: `foo.*?`}, {pat: `foo*`, mode: Shortest | Filenames, want: `foo[^/]*?`}, {pat: `*foo`, mode: Filenames, want: `[^/]*foo`}, {pat: `**`, want: `.*.*`}, {pat: `**`, mode: Filenames, want: `.*`}, {pat: `/**/foo`, want: `/.*.*/foo`}, {pat: `/**/foo`, mode: Filenames, want: `/(.*/|)foo`}, {pat: `/**/à`, mode: Filenames, want: `/(.*/|)à`}, {pat: `/**foo`, mode: Filenames, want: `/.*foo`}, {pat: `\*`, want: `\*`}, {pat: `\`, wantErr: true}, {pat: `?`, want: `.`}, {pat: `?`, mode: Filenames, want: `[^/]`}, {pat: `?à`, want: `.à`}, {pat: `\a`, want: `a`}, {pat: `(`, want: `\(`}, {pat: `a|b`, want: `a\|b`}, {pat: `x{3}`, want: `x\{3\}`}, {pat: `{3,4}`, want: `\{3,4\}`}, {pat: `{3,4}`, mode: Braces, want: `(?:3|4)`}, {pat: `{3,`, want: `\{3,`}, {pat: `{3,`, mode: Braces, want: `\{3,`}, {pat: `{3,{4}`, mode: Braces, want: `\{3,\{4\}`}, {pat: `{3,{4}}`, mode: Braces, want: `(?:3|\{4\})`}, {pat: `{3,{4,[56]}}`, mode: Braces, want: `(?:3|(?:4|[56]))`}, {pat: `{3..5}`, mode: Braces, want: `(?:3|4|5)`}, {pat: `{9..12}`, mode: Braces, want: `(?:9|10|11|12)`}, {pat: `[a]`, want: `[a]`}, {pat: `[abc]`, want: `[abc]`}, {pat: `[^bc]`, want: `[^bc]`}, {pat: `[!bc]`, want: `[^bc]`}, {pat: `[[]`, want: `[[]`}, {pat: `[\]]`, want: `[\]]`}, {pat: `[\]]`, mode: Filenames, want: `[\]]`}, {pat: `[]]`, want: `[]]`}, {pat: `[!]]`, want: `[^]]`}, {pat: `[^]]`, want: `[^]]`}, {pat: `[a/b]`, want: `[a/b]`}, {pat: `[a/b]`, mode: Filenames, want: `\[a/b\]`}, {pat: `[`, wantErr: true}, {pat: `[\`, wantErr: true}, {pat: `[^`, wantErr: true}, {pat: `[!`, wantErr: true}, {pat: `[!bc]`, want: `[^bc]`}, {pat: `[]`, wantErr: true}, {pat: `[^]`, wantErr: true}, {pat: `[!]`, wantErr: true}, {pat: `[ab`, wantErr: true}, {pat: `[a-]`, want: `[a-]`}, {pat: `[z-a]`, wantErr: true}, {pat: `[a-a]`, want: `[a-a]`}, {pat: `[aa]`, want: `[aa]`}, {pat: `[0-4A-Z]`, want: `[0-4A-Z]`}, {pat: `[-a]`, want: `[-a]`}, {pat: `[^-a]`, want: `[^-a]`}, {pat: `[a-]`, want: `[a-]`}, {pat: `[[:digit:]]`, want: `[[:digit:]]`}, {pat: `[[:`, wantErr: true}, {pat: `[[:digit`, wantErr: true}, {pat: `[[:wrong:]]`, wantErr: true}, {pat: `[[=x=]]`, wantErr: true}, {pat: `[[.x.]]`, wantErr: true}, } func TestRegexp(t *testing.T) { t.Parallel() for i, tc := range translateTests { t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { got, gotErr := Regexp(tc.pat, tc.mode) if tc.wantErr && gotErr == nil { t.Fatalf("(%q, %b) did not error", tc.pat, tc.mode) } if !tc.wantErr && gotErr != nil { t.Fatalf("(%q, %b) errored with %q", tc.pat, tc.mode, gotErr) } if got != tc.want { t.Fatalf("(%q, %b) got %q, wanted %q", tc.pat, tc.mode, got, tc.want) } _, rxErr := syntax.Parse(got, syntax.Perl) if gotErr == nil && rxErr != nil { t.Fatalf("regexp/syntax.Parse(%q) failed with %q", got, rxErr) } }) } } var metaTests = []struct { pat string mode Mode wantHas bool wantQuote string }{ {``, 0, false, ``}, {`foo`, 0, false, `foo`}, {`.`, 0, false, `.`}, {`*`, 0, true, `\*`}, {`*`, Shortest | Filenames, true, `\*`}, {`foo?`, 0, true, `foo\?`}, {`\[`, 0, false, `\\\[`}, {`{`, 0, false, `{`}, {`{`, Braces, true, `\{`}, } func TestMeta(t *testing.T) { t.Parallel() for _, tc := range metaTests { if got := HasMeta(tc.pat, tc.mode); got != tc.wantHas { t.Errorf("HasMeta(%q, %b) got %t, wanted %t", tc.pat, tc.mode, got, tc.wantHas) } if got := QuoteMeta(tc.pat, tc.mode); got != tc.wantQuote { t.Errorf("QuoteMeta(%q, %b) got %q, wanted %q", tc.pat, tc.mode, got, tc.wantQuote) } } } sh-3.4.3/shell/000077500000000000000000000000001420425111700132375ustar00rootroot00000000000000sh-3.4.3/shell/doc.go000066400000000000000000000011451420425111700143340ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information // Package shell contains high-level features that use the syntax, expand, and // interp packages under the hood. // // Please note that this package uses POSIX Shell syntax. As such, path names on // Windows need to use double backslashes or be within single quotes when given // to functions like Fields. For example: // // shell.Fields("echo /foo/bar") // on Unix-like // shell.Fields("echo C:\\foo\\bar") // on Windows // shell.Fields("echo 'C:\foo\bar'") // on Windows, with quotes package shell sh-3.4.3/shell/example_test.go000066400000000000000000000021671420425111700162660ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package shell_test import ( "fmt" "mvdan.cc/sh/v3/shell" ) func ExampleExpand() { env := func(name string) string { switch name { case "HOME": return "/home/user" } return "" // leave the rest unset } out, _ := shell.Expand("No place like $HOME", env) fmt.Println(out) out, _ = shell.Expand("Some vars are ${missing:-awesome}", env) fmt.Println(out) out, _ = shell.Expand("Math is fun! $((12 * 34))", nil) fmt.Println(out) // Output: // No place like /home/user // Some vars are awesome // Math is fun! 408 } func ExampleFields() { env := func(name string) string { switch name { case "foo": return "bar baz" } return "" // leave the rest unset } out, _ := shell.Fields(`"many quoted" ' strings '`, env) fmt.Printf("%#v\n", out) out, _ = shell.Fields("unquoted $foo", env) fmt.Printf("%#v\n", out) out, _ = shell.Fields(`quoted "$foo"`, env) fmt.Printf("%#v\n", out) // Output: // []string{"many quoted", " strings "} // []string{"unquoted", "bar", "baz"} // []string{"quoted", "bar baz"} } sh-3.4.3/shell/expand.go000066400000000000000000000036511420425111700150520ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package shell import ( "os" "strings" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) // Expand performs shell expansion on s as if it were within double quotes, // using env to resolve variables. This includes parameter expansion, arithmetic // expansion, and quote removal. // // If env is nil, the current environment variables are used. Empty variables // are treated as unset; to support variables which are set but empty, use the // expand package directly. // // Command subsitutions like $(echo foo) aren't supported to avoid running // arbitrary code. To support those, use an interpreter with the expand package. // // An error will be reported if the input string had invalid syntax. func Expand(s string, env func(string) string) (string, error) { p := syntax.NewParser() word, err := p.Document(strings.NewReader(s)) if err != nil { return "", err } if env == nil { env = os.Getenv } cfg := &expand.Config{Env: expand.FuncEnviron(env)} return expand.Document(cfg, word) } // Fields performs shell expansion on s as if it were a command's arguments, // using env to resolve variables. It is similar to Expand, but includes brace // expansion, tilde expansion, and globbing. // // If env is nil, the current environment variables are used. Empty variables // are treated as unset; to support variables which are set but empty, use the // expand package directly. // // An error will be reported if the input string had invalid syntax. func Fields(s string, env func(string) string) ([]string, error) { p := syntax.NewParser() var words []*syntax.Word err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool { words = append(words, w) return true }) if err != nil { return nil, err } if env == nil { env = os.Getenv } cfg := &expand.Config{Env: expand.FuncEnviron(env)} return expand.Fields(cfg, words...) } sh-3.4.3/shell/expand_test.go000066400000000000000000000053311420425111700161060ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package shell import ( "fmt" "os" "reflect" "runtime" "strings" "testing" ) func strEnviron(pairs ...string) func(string) string { return func(name string) string { prefix := name + "=" for _, pair := range pairs { if val := strings.TrimPrefix(pair, prefix); val != pair { return val } } return "" } } var expandTests = []struct { in string env func(name string) string want string }{ {"foo", nil, "foo"}, {"\nfoo\n", nil, "\nfoo\n"}, {"a-$b-c", nil, "a--c"}, {"${INTERP_GLOBAL:+hasOsEnv}", nil, "hasOsEnv"}, {"a-$b-c", strEnviron(), "a--c"}, {"a-$b-c", strEnviron("b=b_val"), "a-b_val-c"}, {"${x//o/a}", strEnviron("x=foo"), "faa"}, {"*.go", nil, "*.go"}, {"~", nil, "~"}, } func TestExpand(t *testing.T) { os.Setenv("INTERP_GLOBAL", "value") for _, tc := range expandTests { tc := tc t.Run("", func(t *testing.T) { t.Parallel() got, err := Expand(tc.in, tc.env) if err != nil { t.Fatal(err) } if got != tc.want { t.Fatalf("\nwant: %q\ngot: %q", tc.want, got) } }) } } func TestUnexpectedCmdSubst(t *testing.T) { t.Parallel() want := "unexpected command substitution at 1:6" for _, fn := range []func() error{ func() error { _, err := Expand("echo $(uname -a)", nil) return err }, func() error { _, err := Fields("echo $(uname -a)", nil) return err }, } { got := fmt.Sprint(fn()) if !strings.Contains(got, want) { t.Fatalf("wanted error %q, got: %s", want, got) } } } var fieldsTests = []struct { in string env func(name string) string want []string }{ {"foo", nil, []string{"foo"}}, {"\nfoo\n", nil, []string{"foo"}}, {"foo bar", nil, []string{"foo", "bar"}}, {"foo 'bar baz'", nil, []string{"foo", "bar baz"}}, {"$x", strEnviron("x=foo bar"), []string{"foo", "bar"}}, {`"$x"`, strEnviron("x=foo bar"), []string{"foo bar"}}, {"~", strEnviron("HOME=/my/home"), []string{"/my/home"}}, {"~/foo/bar", strEnviron("HOME=/my/home"), []string{"/my/home/foo/bar"}}, {"~foo/file", strEnviron("HOME foo=/bar"), []string{"/bar/file"}}, {"*.go", nil, []string{"*.go"}}, {"~", func(name string) string { switch runtime.GOOS { case "windows": if name == "USERPROFILE" { return "/my/home" } default: if name == "HOME" { return "/my/home" } } return "" }, []string{"/my/home"}}, } func TestFields(t *testing.T) { os.Setenv("INTERP_GLOBAL", "value") for _, tc := range fieldsTests { tc := tc t.Run("", func(t *testing.T) { t.Parallel() got, err := Fields(tc.in, tc.env) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, tc.want) { t.Fatalf("\nwant: %q\ngot: %q", tc.want, got) } }) } } sh-3.4.3/syntax/000077500000000000000000000000001420425111700134565ustar00rootroot00000000000000sh-3.4.3/syntax/braces.go000066400000000000000000000074131420425111700152510ustar00rootroot00000000000000// Copyright (c) 2018, Daniel Martí // See LICENSE for licensing information package syntax import "strconv" var ( litLeftBrace = &Lit{Value: "{"} litComma = &Lit{Value: ","} litDots = &Lit{Value: ".."} litRightBrace = &Lit{Value: "}"} ) // SplitBraces parses brace expansions within a word's literal parts. If any // valid brace expansions are found, they are replaced with BraceExp nodes, and // the function returns true. Otherwise, the word is left untouched and the // function returns false. // // For example, a literal word "foo{bar,baz}" will result in a word containing // the literal "foo", and a brace expansion with the elements "bar" and "baz". // // It does not return an error; malformed brace expansions are simply skipped. // For example, the literal word "a{b" is left unchanged. func SplitBraces(word *Word) bool { any := false top := &Word{} acc := top var cur *BraceExp open := []*BraceExp{} pop := func() *BraceExp { old := cur open = open[:len(open)-1] if len(open) == 0 { cur = nil acc = top } else { cur = open[len(open)-1] acc = cur.Elems[len(cur.Elems)-1] } return old } addLit := func(lit *Lit) { acc.Parts = append(acc.Parts, lit) } for _, wp := range word.Parts { lit, ok := wp.(*Lit) if !ok { acc.Parts = append(acc.Parts, wp) continue } last := 0 for j := 0; j < len(lit.Value); j++ { addlitidx := func() { if last == j { return // empty lit } l2 := *lit l2.Value = l2.Value[last:j] addLit(&l2) } switch lit.Value[j] { case '{': addlitidx() acc = &Word{} cur = &BraceExp{Elems: []*Word{acc}} open = append(open, cur) case ',': if cur == nil { continue } addlitidx() acc = &Word{} cur.Elems = append(cur.Elems, acc) case '.': if cur == nil { continue } if j+1 >= len(lit.Value) || lit.Value[j+1] != '.' { continue } addlitidx() cur.Sequence = true acc = &Word{} cur.Elems = append(cur.Elems, acc) j++ case '}': if cur == nil { continue } any = true addlitidx() br := pop() if len(br.Elems) == 1 { // return {x} to a non-brace addLit(litLeftBrace) acc.Parts = append(acc.Parts, br.Elems[0].Parts...) addLit(litRightBrace) break } if !br.Sequence { acc.Parts = append(acc.Parts, br) break } var chars [2]bool broken := false for i, elem := range br.Elems[:2] { val := elem.Lit() if _, err := strconv.Atoi(val); err == nil { } else if len(val) == 1 && 'a' <= val[0] && val[0] <= 'z' { chars[i] = true } else { broken = true } } if len(br.Elems) == 3 { // increment must be a number val := br.Elems[2].Lit() if _, err := strconv.Atoi(val); err != nil { broken = true } } // are start and end both chars or // non-chars? if chars[0] != chars[1] { broken = true } if !broken { acc.Parts = append(acc.Parts, br) break } // return broken {x..y[..incr]} to a non-brace addLit(litLeftBrace) for i, elem := range br.Elems { if i > 0 { addLit(litDots) } acc.Parts = append(acc.Parts, elem.Parts...) } addLit(litRightBrace) default: continue } last = j + 1 } if last == 0 { addLit(lit) } else { left := *lit left.Value = left.Value[last:] addLit(&left) } } if !any { return false } // open braces that were never closed fall back to non-braces for acc != top { br := pop() addLit(litLeftBrace) for i, elem := range br.Elems { if i > 0 { if br.Sequence { addLit(litDots) } else { addLit(litComma) } } acc.Parts = append(acc.Parts, elem.Parts...) } } *word = *top return true } sh-3.4.3/syntax/canonical.sh000066400000000000000000000004751420425111700157470ustar00rootroot00000000000000#!/bin/bash # separate comment ! foo bar >a & foo() { bar; } { var1="some long value" # var1 comment var2=short # var2 comment } if foo; then bar; fi for foo in a b c; do bar done case $foo in a) A ;; b) B ;; esac foo | bar foo && $(bar) && (more) foo 2>&1 foo <<-EOF bar EOF $((3 + 4)) sh-3.4.3/syntax/doc.go000066400000000000000000000003421420425111700145510ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information // Package syntax implements parsing and formatting of shell programs. // It supports POSIX Shell, Bash, and mksh. package syntax sh-3.4.3/syntax/example_test.go000066400000000000000000000115301420425111700164770ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax_test import ( "fmt" "os" "strings" "mvdan.cc/sh/v3/syntax" ) func Example() { r := strings.NewReader("{ foo; bar; }") f, err := syntax.NewParser().Parse(r, "") if err != nil { return } syntax.NewPrinter().Print(os.Stdout, f) // Output: // { // foo // bar // } } func ExampleWord() { r := strings.NewReader("echo foo${bar}'baz'") f, err := syntax.NewParser().Parse(r, "") if err != nil { return } printer := syntax.NewPrinter() args := f.Stmts[0].Cmd.(*syntax.CallExpr).Args for i, word := range args { fmt.Printf("Word number %d:\n", i) for _, part := range word.Parts { fmt.Printf("%-20T - ", part) printer.Print(os.Stdout, part) fmt.Println() } fmt.Println() } // Output: // Word number 0: // *syntax.Lit - echo // // Word number 1: // *syntax.Lit - foo // *syntax.ParamExp - ${bar} // *syntax.SglQuoted - 'baz' } func ExampleCommand() { r := strings.NewReader("echo foo; if x; then y; fi; foo | bar") f, err := syntax.NewParser().Parse(r, "") if err != nil { return } printer := syntax.NewPrinter() for i, stmt := range f.Stmts { fmt.Printf("Cmd %d: %-20T - ", i, stmt.Cmd) printer.Print(os.Stdout, stmt.Cmd) fmt.Println() } // Output: // Cmd 0: *syntax.CallExpr - echo foo // Cmd 1: *syntax.IfClause - if x; then y; fi // Cmd 2: *syntax.BinaryCmd - foo | bar } func ExampleNewParser_options() { src := "for ((i = 0; i < 5; i++)); do echo $i >f; done" // LangBash is the default r := strings.NewReader(src) f, err := syntax.NewParser().Parse(r, "") fmt.Println(err) // Parser errors with LangPOSIX r = strings.NewReader(src) _, err = syntax.NewParser(syntax.Variant(syntax.LangPOSIX)).Parse(r, "") fmt.Println(err) syntax.NewPrinter().Print(os.Stdout, f) syntax.NewPrinter(syntax.SpaceRedirects(true)).Print(os.Stdout, f) // Output: // // 1:5: c-style fors are a bash feature // for ((i = 0; i < 5; i++)); do echo $i >f; done // for ((i = 0; i < 5; i++)); do echo $i > f; done } // Keep in sync with FuzzQuote. func ExampleQuote() { for _, s := range []string{ "foo", "bar $baz", `"won't"`, "~/home", "#1304", "name=value", "for", "glob-*", "invalid-\xe2'", "nonprint-\x0b\x1b", } { // We assume Bash syntax here. // For general shell syntax quoting, use syntax.LangPOSIX. quoted, err := syntax.Quote(s, syntax.LangBash) if err != nil { fmt.Printf("%q cannot be quoted: %v\n", s, err) } else { fmt.Printf("Quote(%17q): %s\n", s, quoted) } } // Output: // Quote( "foo"): foo // Quote( "bar $baz"): 'bar $baz' // Quote( "\"won't\""): "\"won't\"" // Quote( "~/home"): '~/home' // Quote( "#1304"): '#1304' // Quote( "name=value"): 'name=value' // Quote( "for"): 'for' // Quote( "glob-*"): 'glob-*' // Quote( "invalid-\xe2'"): $'invalid-\xe2\'' // Quote("nonprint-\v\x1b"): $'nonprint-\v\x1b' } func ExampleWalk() { in := strings.NewReader(`echo $foo "and $bar"`) f, err := syntax.NewParser().Parse(in, "") if err != nil { return } syntax.Walk(f, func(node syntax.Node) bool { switch x := node.(type) { case *syntax.ParamExp: x.Param.Value = strings.ToUpper(x.Param.Value) } return true }) syntax.NewPrinter().Print(os.Stdout, f) // Output: echo $FOO "and $BAR" } func ExampleDebugPrint() { in := strings.NewReader(`echo 'foo'`) f, err := syntax.NewParser().Parse(in, "") if err != nil { return } syntax.DebugPrint(os.Stdout, f) // Output: // *syntax.File { // . Name: "" // . Stmts: []*syntax.Stmt (len = 1) { // . . 0: *syntax.Stmt { // . . . Comments: []syntax.Comment (len = 0) {} // . . . Cmd: *syntax.CallExpr { // . . . . Assigns: []*syntax.Assign (len = 0) {} // . . . . Args: []*syntax.Word (len = 2) { // . . . . . 0: *syntax.Word { // . . . . . . Parts: []syntax.WordPart (len = 1) { // . . . . . . . 0: *syntax.Lit { // . . . . . . . . ValuePos: 1:1 // . . . . . . . . ValueEnd: 1:5 // . . . . . . . . Value: "echo" // . . . . . . . } // . . . . . . } // . . . . . } // . . . . . 1: *syntax.Word { // . . . . . . Parts: []syntax.WordPart (len = 1) { // . . . . . . . 0: *syntax.SglQuoted { // . . . . . . . . Left: 1:6 // . . . . . . . . Right: 1:10 // . . . . . . . . Dollar: false // . . . . . . . . Value: "foo" // . . . . . . . } // . . . . . . } // . . . . . } // . . . . } // . . . } // . . . Position: 1:1 // . . . Semicolon: 0:0 // . . . Negated: false // . . . Background: false // . . . Coprocess: false // . . . Redirs: []*syntax.Redirect (len = 0) {} // . . } // . } // . Last: []syntax.Comment (len = 0) {} // } } sh-3.4.3/syntax/filetests_test.go000066400000000000000000002701751420425111700170620ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "reflect" "strings" "testing" ) func prepareTest(c *testCase) { c.common = fullProg(c.common) c.bash = fullProg(c.bash) c.posix = fullProg(c.posix) c.mksh = fullProg(c.mksh) c.bsmk = fullProg(c.bsmk) // bash AND mksh c.bats = fullProg(c.bats) if f, ok := c.common.(*File); ok && f != nil { c.All = append(c.All, f) c.Bash = f c.Posix = f c.MirBSDKorn = f c.Bats = f } if f, ok := c.bash.(*File); ok && f != nil { c.All = append(c.All, f) c.Bash = f c.Bats = f } if f, ok := c.posix.(*File); ok && f != nil { c.All = append(c.All, f) c.Posix = f } if f, ok := c.mksh.(*File); ok && f != nil { c.All = append(c.All, f) c.MirBSDKorn = f } if f, ok := c.bsmk.(*File); ok && f != nil { c.All = append(c.All, f) c.Bash = f c.MirBSDKorn = f } if f, ok := c.bats.(*File); ok && f != nil { c.All = append(c.All, f) c.Bats = f } } func init() { for i := range fileTests { prepareTest(&fileTests[i]) } for i := range fileTestsNoPrint { prepareTest(&fileTestsNoPrint[i]) } } func lit(s string) *Lit { return &Lit{Value: s} } func word(ps ...WordPart) *Word { return &Word{Parts: ps} } func litWord(s string) *Word { return word(lit(s)) } func litWords(strs ...string) []*Word { l := make([]*Word, 0, len(strs)) for _, s := range strs { l = append(l, litWord(s)) } return l } func call(words ...*Word) *CallExpr { return &CallExpr{Args: words} } func litCall(strs ...string) *CallExpr { return call(litWords(strs...)...) } func stmt(cmd Command) *Stmt { return &Stmt{Cmd: cmd} } func stmts(cmds ...Command) []*Stmt { l := make([]*Stmt, len(cmds)) for i, cmd := range cmds { l[i] = stmt(cmd) } return l } func litStmt(strs ...string) *Stmt { return stmt(litCall(strs...)) } func litStmts(strs ...string) []*Stmt { l := make([]*Stmt, len(strs)) for i, s := range strs { l[i] = litStmt(s) } return l } func sglQuoted(s string) *SglQuoted { return &SglQuoted{Value: s} } func sglDQuoted(s string) *SglQuoted { return &SglQuoted{Dollar: true, Value: s} } func dblQuoted(ps ...WordPart) *DblQuoted { return &DblQuoted{Parts: ps} } func dblDQuoted(ps ...WordPart) *DblQuoted { return &DblQuoted{Dollar: true, Parts: ps} } func block(sts ...*Stmt) *Block { return &Block{Stmts: sts} } func subshell(sts ...*Stmt) *Subshell { return &Subshell{Stmts: sts} } func arithmExp(e ArithmExpr) *ArithmExp { return &ArithmExp{X: e} } func arithmExpBr(e ArithmExpr) *ArithmExp { return &ArithmExp{Bracket: true, X: e} } func arithmCmd(e ArithmExpr) *ArithmCmd { return &ArithmCmd{X: e} } func parenArit(e ArithmExpr) *ParenArithm { return &ParenArithm{X: e} } func parenTest(e TestExpr) *ParenTest { return &ParenTest{X: e} } func cmdSubst(sts ...*Stmt) *CmdSubst { return &CmdSubst{Stmts: sts} } func litParamExp(s string) *ParamExp { return &ParamExp{Short: true, Param: lit(s)} } func letClause(exps ...ArithmExpr) *LetClause { return &LetClause{Exprs: exps} } func arrValues(words ...*Word) *ArrayExpr { ae := &ArrayExpr{} for _, w := range words { ae.Elems = append(ae.Elems, &ArrayElem{Value: w}) } return ae } type testCase struct { Strs []string common interface{} bash, posix interface{} bsmk, mksh interface{} bats interface{} All []*File Bash, Posix *File MirBSDKorn *File Bats *File } var fileTests = []testCase{ { Strs: []string{"", " ", "\t", "\n \n", "\r \r\n"}, common: &File{}, }, { Strs: []string{"", "# foo", "# foo ( bar", "# foo'bar"}, common: &File{}, }, { Strs: []string{"foo", "foo ", " foo", "foo # bar"}, common: litWord("foo"), }, { Strs: []string{`\`}, common: litWord(`\`), }, { Strs: []string{`foo\`, "f\\\noo\\"}, common: litWord(`foo\`), }, { Strs: []string{`foo\a`, "f\\\noo\\a"}, common: litWord(`foo\a`), }, { Strs: []string{ "foo\nbar", "foo; bar;", "foo;bar;", "\nfoo\nbar\n", "foo\r\nbar\r\n", }, common: litStmts("foo", "bar"), }, { Strs: []string{"foo a b", " foo a b ", "foo \\\n a b"}, common: litCall("foo", "a", "b"), }, { Strs: []string{"foobar", "foo\\\nbar", "foo\\\nba\\\nr"}, common: litWord("foobar"), }, { Strs: []string{"foo", "foo \\\n"}, common: litWord("foo"), }, { Strs: []string{"foo'bar'"}, common: word(lit("foo"), sglQuoted("bar")), }, { Strs: []string{"(foo)", "(foo;)", "(\nfoo\n)"}, common: subshell(litStmt("foo")), }, { Strs: []string{"(\n\tfoo\n\tbar\n)", "(foo; bar)"}, common: subshell(litStmt("foo"), litStmt("bar")), }, { Strs: []string{"{ foo; }", "{\nfoo\n}"}, common: block(litStmt("foo")), }, { Strs: []string{ "{ if a; then b; fi; }", "{ if a; then b; fi }", }, common: block(stmt(&IfClause{ Cond: litStmts("a"), Then: litStmts("b"), })), }, { Strs: []string{ "if a; then b; fi", "if a\nthen\nb\nfi", "if a;\nthen\nb\nfi", "if a \nthen\nb\nfi", "if\x00 a; th\x00en b; \x00fi", }, common: &IfClause{ Cond: litStmts("a"), Then: litStmts("b"), }, }, { Strs: []string{ "if a; then b; else c; fi", "if a\nthen b\nelse\nc\nfi", }, common: &IfClause{ Cond: litStmts("a"), Then: litStmts("b"), Else: &IfClause{ Then: litStmts("c"), }, }, }, { Strs: []string{ "if a; then a; elif b; then b; else c; fi", }, common: &IfClause{ Cond: litStmts("a"), Then: litStmts("a"), Else: &IfClause{ Cond: litStmts("b"), Then: litStmts("b"), Else: &IfClause{ Then: litStmts("c"), }, }, }, }, { Strs: []string{ "if a; then a; elif b; then b; elif c; then c; else d; fi", "if a\nthen a\nelif b\nthen b\nelif c\nthen c\nelse\nd\nfi", }, common: &IfClause{ Cond: litStmts("a"), Then: litStmts("a"), Else: &IfClause{ Cond: litStmts("b"), Then: litStmts("b"), Else: &IfClause{ Cond: litStmts("c"), Then: litStmts("c"), Else: &IfClause{ Then: litStmts("d"), }, }, }, }, }, { Strs: []string{ "if\n\ta1\n\ta2 foo\n\ta3 bar\nthen b; fi", "if a1; a2 foo; a3 bar; then b; fi", }, common: &IfClause{ Cond: []*Stmt{ litStmt("a1"), litStmt("a2", "foo"), litStmt("a3", "bar"), }, Then: litStmts("b"), }, }, { Strs: []string{`((a == 2))`}, bsmk: arithmCmd(&BinaryArithm{ Op: Eql, X: litWord("a"), Y: litWord("2"), }), posix: subshell(stmt(subshell(litStmt("a", "==", "2")))), }, { Strs: []string{"if (($# > 2)); then b; fi"}, bsmk: &IfClause{ Cond: stmts(arithmCmd(&BinaryArithm{ Op: Gtr, X: word(litParamExp("#")), Y: litWord("2"), })), Then: litStmts("b"), }, }, { Strs: []string{ "(($(date -u) > DATE))", "((`date -u` > DATE))", }, bsmk: arithmCmd(&BinaryArithm{ Op: Gtr, X: word(cmdSubst(litStmt("date", "-u"))), Y: litWord("DATE"), }), }, { Strs: []string{": $((0x$foo == 10))"}, common: call( litWord(":"), word(arithmExp(&BinaryArithm{ Op: Eql, X: word(lit("0x"), litParamExp("foo")), Y: litWord("10"), })), ), }, { Strs: []string{"((# 1 + 2))", "(( # 1 + 2 ))"}, mksh: &ArithmCmd{ X: &BinaryArithm{ Op: Add, X: litWord("1"), Y: litWord("2"), }, Unsigned: true, }, }, { Strs: []string{"$((# 1 + 2))", "$(( # 1 + 2 ))"}, mksh: &ArithmExp{ X: &BinaryArithm{ Op: Add, X: litWord("1"), Y: litWord("2"), }, Unsigned: true, }, }, { Strs: []string{"((3#20))"}, bsmk: arithmCmd(litWord("3#20")), }, { Strs: []string{ "while a; do b; done", "wh\\\nile a; do b; done", "while a\ndo\nb\ndone", "while a;\ndo\nb\ndone", }, common: &WhileClause{ Cond: litStmts("a"), Do: litStmts("b"), }, }, { Strs: []string{"while { a; }; do b; done", "while { a; } do b; done"}, common: &WhileClause{ Cond: stmts(block(litStmt("a"))), Do: litStmts("b"), }, }, { Strs: []string{"while (a); do b; done", "while (a) do b; done"}, common: &WhileClause{ Cond: stmts(subshell(litStmt("a"))), Do: litStmts("b"), }, }, { Strs: []string{"while ((1 > 2)); do b; done"}, bsmk: &WhileClause{ Cond: stmts(arithmCmd(&BinaryArithm{ Op: Gtr, X: litWord("1"), Y: litWord("2"), })), Do: litStmts("b"), }, }, { Strs: []string{"until a; do b; done", "until a\ndo\nb\ndone"}, common: &WhileClause{ Until: true, Cond: litStmts("a"), Do: litStmts("b"), }, }, { Strs: []string{ "for i; do foo; done", "for i do foo; done", "for i\ndo foo\ndone", "for i;\ndo foo\ndone", "for i in; do foo; done", }, common: &ForClause{ Loop: &WordIter{Name: lit("i")}, Do: litStmts("foo"), }, }, { Strs: []string{ "for i in 1 2 3; do echo $i; done", "for i in 1 2 3\ndo echo $i\ndone", "for i in 1 2 3;\ndo echo $i\ndone", "for i in 1 2 3 #foo\ndo echo $i\ndone", }, common: &ForClause{ Loop: &WordIter{ Name: lit("i"), Items: litWords("1", "2", "3"), }, Do: stmts(call( litWord("echo"), word(litParamExp("i")), )), }, }, { Strs: []string{ "for i in \\\n\t1 2 3; do #foo\n\techo $i\ndone", "for i #foo\n\tin 1 2 3; do\n\techo $i\ndone", }, common: &ForClause{ Loop: &WordIter{ Name: lit("i"), Items: litWords("1", "2", "3"), }, Do: stmts(call( litWord("echo"), word(litParamExp("i")), )), }, }, { Strs: []string{ "for i; do foo; done", "for i; { foo; }", }, bsmk: &ForClause{ Loop: &WordIter{Name: lit("i")}, Do: litStmts("foo"), }, }, { Strs: []string{ "for i in 1 2 3; do echo $i; done", "for i in 1 2 3; { echo $i; }", }, bsmk: &ForClause{ Loop: &WordIter{ Name: lit("i"), Items: litWords("1", "2", "3"), }, Do: stmts(call( litWord("echo"), word(litParamExp("i")), )), }, }, { Strs: []string{ "for ((i = 0; i < 10; i++)); do echo $i; done", "for ((i=0;i<10;i++)) do echo $i; done", "for (( i = 0 ; i < 10 ; i++ ))\ndo echo $i\ndone", "for (( i = 0 ; i < 10 ; i++ ));\ndo echo $i\ndone", }, bash: &ForClause{ Loop: &CStyleLoop{ Init: &BinaryArithm{ Op: Assgn, X: litWord("i"), Y: litWord("0"), }, Cond: &BinaryArithm{ Op: Lss, X: litWord("i"), Y: litWord("10"), }, Post: &UnaryArithm{ Op: Inc, Post: true, X: litWord("i"), }, }, Do: stmts(call( litWord("echo"), word(litParamExp("i")), )), }, }, { Strs: []string{ "for (( ; ; )); do foo; done", "for ((;;)); do foo; done", }, bash: &ForClause{ Loop: &CStyleLoop{}, Do: litStmts("foo"), }, }, { Strs: []string{ "for ((i = 0; ; )); do foo; done", "for ((i = 0;;)); do foo; done", }, bash: &ForClause{ Loop: &CStyleLoop{ Init: &BinaryArithm{ Op: Assgn, X: litWord("i"), Y: litWord("0"), }, }, Do: litStmts("foo"), }, }, { Strs: []string{ "select i; do foo; done", // TODO: bash won't allow this - bug? //"select i in; do foo; done", }, bsmk: &ForClause{ Select: true, Loop: &WordIter{Name: lit("i")}, Do: litStmts("foo"), }, }, { Strs: []string{ "select i in 1 2 3; do echo $i; done", "select i in 1 2 3\ndo echo $i\ndone", "select i in 1 2 3 #foo\ndo echo $i\ndone", }, bsmk: &ForClause{ Select: true, Loop: &WordIter{ Name: lit("i"), Items: litWords("1", "2", "3"), }, Do: stmts(call( litWord("echo"), word(litParamExp("i")), )), }, }, { Strs: []string{"select foo bar"}, posix: litStmt("select", "foo", "bar"), }, { Strs: []string{`' ' "foo bar"`}, common: call( word(sglQuoted(" ")), word(dblQuoted(lit("foo bar"))), ), }, { Strs: []string{`"foo \" bar"`}, common: word(dblQuoted(lit(`foo \" bar`))), }, { Strs: []string{"\">foo\" \"\nbar\""}, common: call( word(dblQuoted(lit(">foo"))), word(dblQuoted(lit("\nbar"))), ), }, { Strs: []string{`foo \" bar`}, common: litCall(`foo`, `\"`, `bar`), }, { Strs: []string{`'"'`}, common: sglQuoted(`"`), }, { Strs: []string{"'`'"}, common: sglQuoted("`"), }, { Strs: []string{`"'"`}, common: dblQuoted(lit("'")), }, { Strs: []string{`""`}, common: dblQuoted(), }, { Strs: []string{"=a s{s s=s"}, common: litCall("=a", "s{s", "s=s"), }, { Strs: []string{"foo && bar", "foo&&bar", "foo &&\nbar"}, common: &BinaryCmd{ Op: AndStmt, X: litStmt("foo"), Y: litStmt("bar"), }, }, { Strs: []string{"foo &&\n\tbar"}, common: &BinaryCmd{ Op: AndStmt, X: litStmt("foo"), Y: litStmt("bar"), }, }, { Strs: []string{"foo || bar", "foo||bar", "foo ||\nbar"}, common: &BinaryCmd{ Op: OrStmt, X: litStmt("foo"), Y: litStmt("bar"), }, }, { Strs: []string{"if a; then b; fi || while a; do b; done"}, common: &BinaryCmd{ Op: OrStmt, X: stmt(&IfClause{ Cond: litStmts("a"), Then: litStmts("b"), }), Y: stmt(&WhileClause{ Cond: litStmts("a"), Do: litStmts("b"), }), }, }, { Strs: []string{"foo && bar1 || bar2"}, common: &BinaryCmd{ Op: OrStmt, X: stmt(&BinaryCmd{ Op: AndStmt, X: litStmt("foo"), Y: litStmt("bar1"), }), Y: litStmt("bar2"), }, }, { Strs: []string{"a || b || c || d"}, common: &BinaryCmd{ Op: OrStmt, X: stmt(&BinaryCmd{ Op: OrStmt, X: stmt(&BinaryCmd{ Op: OrStmt, X: litStmt("a"), Y: litStmt("b"), }), Y: litStmt("c"), }), Y: litStmt("d"), }, }, { Strs: []string{"foo | bar", "foo|bar", "foo |\n#etc\nbar"}, common: &BinaryCmd{ Op: Pipe, X: litStmt("foo"), Y: litStmt("bar"), }, }, { Strs: []string{"foo | bar | extra"}, common: &BinaryCmd{ Op: Pipe, X: stmt(&BinaryCmd{ Op: Pipe, X: litStmt("foo"), Y: litStmt("bar"), }), Y: litStmt("extra"), }, }, { Strs: []string{"foo | a=b bar"}, common: &BinaryCmd{ Op: Pipe, X: litStmt("foo"), Y: stmt(&CallExpr{ Assigns: []*Assign{{ Name: lit("a"), Value: litWord("b"), }}, Args: litWords("bar"), }), }, }, { Strs: []string{"foo |&"}, mksh: &Stmt{Cmd: litCall("foo"), Coprocess: true}, }, { Strs: []string{"foo \\\n\t|&"}, mksh: &Stmt{Cmd: litCall("foo"), Coprocess: true}, }, { Strs: []string{"foo |& bar", "foo|&bar"}, bash: &BinaryCmd{ Op: PipeAll, X: litStmt("foo"), Y: litStmt("bar"), }, mksh: []*Stmt{ {Cmd: litCall("foo"), Coprocess: true}, litStmt("bar"), }, }, { Strs: []string{ "foo() {\n\ta\n\tb\n}", "foo() { a; b; }", "foo ( ) {\na\nb\n}", "foo()\n{\na\nb\n}", }, common: &FuncDecl{ Parens: true, Name: lit("foo"), Body: stmt(block(litStmt("a"), litStmt("b"))), }, }, { Strs: []string{"foo() { a; }\nbar", "foo() {\na\n}; bar"}, common: []Command{ &FuncDecl{ Parens: true, Name: lit("foo"), Body: stmt(block(litStmt("a"))), }, litCall("bar"), }, }, { Strs: []string{"foO_123() { a; }"}, common: &FuncDecl{ Parens: true, Name: lit("foO_123"), Body: stmt(block(litStmt("a"))), }, }, { Strs: []string{"-foo_.,+-bar() { a; }"}, bsmk: &FuncDecl{ Parens: true, Name: lit("-foo_.,+-bar"), Body: stmt(block(litStmt("a"))), }, }, { Strs: []string{ "function foo() {\n\ta\n\tb\n}", "function foo() { a; b; }", }, bsmk: &FuncDecl{ RsrvWord: true, Parens: true, Name: lit("foo"), Body: stmt(block(litStmt("a"), litStmt("b"))), }, }, { Strs: []string{ "function foo {\n\ta\n\tb\n}", "function foo { a; b; }", }, bsmk: &FuncDecl{ RsrvWord: true, Name: lit("foo"), Body: stmt(block(litStmt("a"), litStmt("b"))), }, }, { Strs: []string{"function foo() (a)"}, bash: &FuncDecl{ RsrvWord: true, Parens: true, Name: lit("foo"), Body: stmt(subshell(litStmt("a"))), }, }, { Strs: []string{"a=b foo=$bar foo=start$bar"}, common: &CallExpr{ Assigns: []*Assign{ {Name: lit("a"), Value: litWord("b")}, {Name: lit("foo"), Value: word(litParamExp("bar"))}, {Name: lit("foo"), Value: word( lit("start"), litParamExp("bar"), )}, }, }, }, { Strs: []string{"a=\"\nbar\""}, common: &CallExpr{ Assigns: []*Assign{{ Name: lit("a"), Value: word(dblQuoted(lit("\nbar"))), }}, }, }, { Strs: []string{"A_3a= foo"}, common: &CallExpr{ Assigns: []*Assign{{Name: lit("A_3a")}}, Args: litWords("foo"), }, }, { Strs: []string{"a=b=c"}, common: &CallExpr{ Assigns: []*Assign{{Name: lit("a"), Value: litWord("b=c")}}, }, }, { Strs: []string{"à=b foo"}, common: litStmt("à=b", "foo"), }, { Strs: []string{ "foo >a >>b a >> b < c", ">a >>b a", "foo >a bar", }, common: &Stmt{ Cmd: litCall("foo", "bar"), Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("a")}, }, }, }, { Strs: []string{`>a >\b`}, common: &Stmt{ Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("a")}, {Op: RdrOut, Word: litWord(`\b`)}, }, }, }, { Strs: []string{">a\n>b", ">a; >b"}, common: []*Stmt{ {Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("a")}, }}, {Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("b")}, }}, }, }, { Strs: []string{"foo1\nfoo2 >r2", "foo1; >r2 foo2"}, common: []*Stmt{ litStmt("foo1"), { Cmd: litCall("foo2"), Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("r2")}, }, }, }, }, { Strs: []string{"foo >bar$(etc)", "foo >b\\\nar`etc`"}, common: &Stmt{ Cmd: litCall("foo"), Redirs: []*Redirect{ {Op: RdrOut, Word: word( lit("bar"), cmdSubst(litStmt("etc")), )}, }, }, }, { Strs: []string{ "a=b c=d foo >x x x a=b c=d x x c=d foo f\nbar\nEOF"}, common: &Stmt{ Cmd: litCall("foo"), Redirs: []*Redirect{ { Op: Hdoc, Word: litWord("EOF"), Hdoc: litWord("bar\n"), }, {Op: RdrOut, Word: litWord("f")}, }, }, }, { Strs: []string{"foo <&2 <&0 2>file 345>file <>f2"}, common: &Stmt{ Cmd: litCall("foo"), Redirs: []*Redirect{ {Op: DplOut, Word: litWord("2")}, {Op: DplIn, Word: litWord("0")}, {Op: RdrOut, N: lit("2"), Word: litWord("file")}, {Op: RdrOut, N: lit("345"), Word: litWord("file")}, {Op: RdrInOut, Word: litWord("f2")}, }, }, }, { Strs: []string{ "foo bar >file", "foo bar>file", }, common: &Stmt{ Cmd: litCall("foo", "bar"), Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("file")}, }, }, }, { Strs: []string{"foo &>a &>>b"}, bsmk: &Stmt{ Cmd: litCall("foo"), Redirs: []*Redirect{ {Op: RdrAll, Word: litWord("a")}, {Op: AppAll, Word: litWord("b")}, }, }, posix: []*Stmt{ {Cmd: litCall("foo"), Background: true}, {Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("a")}, }, Background: true}, {Redirs: []*Redirect{ {Op: AppOut, Word: litWord("b")}, }}, }, }, { Strs: []string{"foo 2>file bar", "2>file foo bar"}, common: &Stmt{ Cmd: litCall("foo", "bar"), Redirs: []*Redirect{ {Op: RdrOut, N: lit("2"), Word: litWord("file")}, }, }, }, { Strs: []string{"a >f1\nb >f2", "a >f1; b >f2"}, common: []*Stmt{ { Cmd: litCall("a"), Redirs: []*Redirect{{Op: RdrOut, Word: litWord("f1")}}, }, { Cmd: litCall("b"), Redirs: []*Redirect{{Op: RdrOut, Word: litWord("f2")}}, }, }, }, { Strs: []string{"foo >|bar"}, common: &Stmt{ Cmd: litCall("foo"), Redirs: []*Redirect{ {Op: ClbOut, Word: litWord("bar")}, }, }, }, { Strs: []string{ "foo <<(foo)"}, bash: call( litWord("foo"), word(&ProcSubst{ Op: CmdOut, Stmts: litStmts("foo"), }), ), }, { Strs: []string{"foo < <(foo)"}, bash: &Stmt{ Cmd: litCall("foo"), Redirs: []*Redirect{{ Op: RdrIn, Word: word(&ProcSubst{ Op: CmdIn, Stmts: litStmts("foo"), }), }}, }, }, { Strs: []string{"a<(b) c>(d)"}, bash: call( word(lit("a"), &ProcSubst{ Op: CmdIn, Stmts: litStmts("b"), }), word(lit("c"), &ProcSubst{ Op: CmdOut, Stmts: litStmts("d"), }), ), }, { Strs: []string{"foo {fd}/dev/null &", "! if foo; then bar; fi>/dev/null&", }, common: &Stmt{ Negated: true, Cmd: &IfClause{ Cond: litStmts("foo"), Then: litStmts("bar"), }, Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("/dev/null")}, }, Background: true, }, }, { Strs: []string{"! foo && bar"}, common: &BinaryCmd{ Op: AndStmt, X: &Stmt{ Cmd: litCall("foo"), Negated: true, }, Y: litStmt("bar"), }, }, { Strs: []string{"! foo | bar"}, common: &Stmt{ Cmd: &BinaryCmd{ Op: Pipe, X: litStmt("foo"), Y: litStmt("bar"), }, Negated: true, }, }, { Strs: []string{ "a && b &\nc", "a && b & c", }, common: []*Stmt{ { Cmd: &BinaryCmd{ Op: AndStmt, X: litStmt("a"), Y: litStmt("b"), }, Background: true, }, litStmt("c"), }, }, { Strs: []string{"a | b &"}, common: &Stmt{ Cmd: &BinaryCmd{ Op: Pipe, X: litStmt("a"), Y: litStmt("b"), }, Background: true, }, }, { Strs: []string{"foo#bar"}, common: litWord("foo#bar"), }, { Strs: []string{"{ echo } }; }"}, common: block(litStmt("echo", "}", "}")), }, { Strs: []string{"$({ echo; })"}, common: cmdSubst(stmt( block(litStmt("echo")), )), }, { Strs: []string{ "$( (echo foo bar))", "$( (echo foo bar) )", "`(echo foo bar)`", }, common: cmdSubst(stmt( subshell(litStmt("echo", "foo", "bar")), )), }, { Strs: []string{"$()"}, common: cmdSubst(), }, { Strs: []string{"()"}, mksh: subshell(), // not common, as dash/bash wrongly error }, { Strs: []string{ "$(\n\t(a)\n\tb\n)", "$( (a); b)", "`(a); b`", }, common: cmdSubst( stmt(subshell(litStmt("a"))), litStmt("b"), ), }, { Strs: []string{ `$(echo \')`, "`" + `echo \\'` + "`", }, common: cmdSubst(litStmt("echo", `\'`)), }, { Strs: []string{ `$(echo \\)`, "`" + `echo \\\\` + "`", }, common: cmdSubst(litStmt("echo", `\\`)), }, { Strs: []string{ `$(echo '\' 'a\b' "\\" "a\a")`, "`" + `echo '\' 'a\b' "\\\\" "a\a"` + "`", }, common: cmdSubst(stmt(call( litWord("echo"), word(sglQuoted(`\`)), word(sglQuoted(`a\b`)), word(dblQuoted(lit(`\\`))), word(dblQuoted(lit(`a\a`))), ))), }, { Strs: []string{ "$(echo $(x))", "`echo \\`x\\``", }, common: cmdSubst(stmt(call( litWord("echo"), word(cmdSubst(litStmt("x"))), ))), }, { Strs: []string{ "$($(foo bar))", "`\\`foo bar\\``", }, common: cmdSubst(stmt(call( word(cmdSubst(litStmt("foo", "bar"))), ))), }, { Strs: []string{"$( (a) | b)"}, common: cmdSubst( stmt(&BinaryCmd{ Op: Pipe, X: stmt(subshell(litStmt("a"))), Y: litStmt("b"), }), ), }, { Strs: []string{`"$( (foo))"`}, common: dblQuoted(cmdSubst(stmt( subshell(litStmt("foo")), ))), }, { Strs: []string{"\"foo\\\nbar\""}, common: dblQuoted(lit("foo"), lit("bar")), }, { Strs: []string{"'foo\\\nbar'"}, common: sglQuoted("foo\\\nbar"), }, { Strs: []string{"$({ echo; })", "`{ echo; }`"}, common: cmdSubst(stmt( block(litStmt("echo")), )), }, { Strs: []string{`{foo}`}, common: litWord(`{foo}`), }, { Strs: []string{`{"foo"`}, common: word(lit("{"), dblQuoted(lit("foo"))), }, { Strs: []string{`foo"bar"`, "fo\\\no\"bar\""}, common: word(lit("foo"), dblQuoted(lit("bar"))), }, { Strs: []string{`!foo`}, common: litWord(`!foo`), }, { Strs: []string{"$(foo bar)", "`foo bar`"}, common: cmdSubst(litStmt("foo", "bar")), }, { Strs: []string{"$(foo | bar)", "`foo | bar`"}, common: cmdSubst( stmt(&BinaryCmd{ Op: Pipe, X: litStmt("foo"), Y: litStmt("bar"), }), ), }, { Strs: []string{"$(foo | >f)", "`foo | >f`"}, common: cmdSubst( stmt(&BinaryCmd{ Op: Pipe, X: litStmt("foo"), Y: &Stmt{Redirs: []*Redirect{{ Op: RdrOut, Word: litWord("f"), }}}, }), ), }, { Strs: []string{"$(foo $(b1 b2))"}, common: cmdSubst(stmt(call( litWord("foo"), word(cmdSubst(litStmt("b1", "b2"))), ))), }, { Strs: []string{`"$(foo "bar")"`}, common: dblQuoted(cmdSubst(stmt(call( litWord("foo"), word(dblQuoted(lit("bar"))), )))), }, { Strs: []string{"$(foo)", "`fo\\\no`"}, common: cmdSubst(litStmt("foo")), }, { Strs: []string{"foo $(bar)", "foo `bar`"}, common: call( litWord("foo"), word(cmdSubst(litStmt("bar"))), ), }, { Strs: []string{"$(foo 'bar')", "`foo 'bar'`"}, common: cmdSubst(stmt(call( litWord("foo"), word(sglQuoted("bar")), ))), }, { Strs: []string{`$(foo "bar")`, "`foo \"bar\"`"}, common: cmdSubst(stmt(call( litWord("foo"), word(dblQuoted(lit("bar"))), ))), }, { Strs: []string{`"$(foo "bar")"`, "\"`foo \"bar\"`\""}, common: dblQuoted(cmdSubst(stmt(call( litWord("foo"), word(dblQuoted(lit("bar"))), )))), }, { Strs: []string{"${ foo;}", "${\n\tfoo; }", "${\tfoo;}"}, mksh: &CmdSubst{ Stmts: litStmts("foo"), TempFile: true, }, }, { Strs: []string{"${\n\tfoo\n\tbar\n}", "${ foo; bar;}"}, mksh: &CmdSubst{ Stmts: litStmts("foo", "bar"), TempFile: true, }, }, { Strs: []string{"${|foo;}", "${| foo; }"}, mksh: &CmdSubst{ Stmts: litStmts("foo"), ReplyVar: true, }, }, { Strs: []string{"${|\n\tfoo\n\tbar\n}", "${|foo; bar;}"}, mksh: &CmdSubst{ Stmts: litStmts("foo", "bar"), ReplyVar: true, }, }, { Strs: []string{`"$foo"`}, common: dblQuoted(litParamExp("foo")), }, { Strs: []string{`"#foo"`}, common: dblQuoted(lit("#foo")), }, { Strs: []string{`$@a $*a $#a $$a $?a $!a $-a $0a $30a $_a`}, common: call( word(litParamExp("@"), lit("a")), word(litParamExp("*"), lit("a")), word(litParamExp("#"), lit("a")), word(litParamExp("$"), lit("a")), word(litParamExp("?"), lit("a")), word(litParamExp("!"), lit("a")), word(litParamExp("-"), lit("a")), word(litParamExp("0"), lit("a")), word(litParamExp("3"), lit("0a")), word(litParamExp("_a")), ), }, { Strs: []string{`$`, `$ #`}, common: litWord("$"), }, { Strs: []string{`${@} ${*} ${#} ${$} ${?} ${!} ${0} ${29} ${-}`}, common: call( word(&ParamExp{Param: lit("@")}), word(&ParamExp{Param: lit("*")}), word(&ParamExp{Param: lit("#")}), word(&ParamExp{Param: lit("$")}), word(&ParamExp{Param: lit("?")}), word(&ParamExp{Param: lit("!")}), word(&ParamExp{Param: lit("0")}), word(&ParamExp{Param: lit("29")}), word(&ParamExp{Param: lit("-")}), ), }, { Strs: []string{`${#$} ${#@} ${#*} ${##}`}, common: call( word(&ParamExp{Length: true, Param: lit("$")}), word(&ParamExp{Length: true, Param: lit("@")}), word(&ParamExp{Length: true, Param: lit("*")}), word(&ParamExp{Length: true, Param: lit("#")}), ), }, { Strs: []string{`${foo}`}, common: &ParamExp{Param: lit("foo")}, }, { Strs: []string{`${foo}"bar"`}, common: word( &ParamExp{Param: lit("foo")}, dblQuoted(lit("bar")), ), }, { Strs: []string{`$a/b $a-b $a:b $a}b $a]b $a.b $a,b $a*b $a_b $a2b`}, common: call( word(litParamExp("a"), lit("/b")), word(litParamExp("a"), lit("-b")), word(litParamExp("a"), lit(":b")), word(litParamExp("a"), lit("}b")), word(litParamExp("a"), lit("]b")), word(litParamExp("a"), lit(".b")), word(litParamExp("a"), lit(",b")), word(litParamExp("a"), lit("*b")), word(litParamExp("a_b")), word(litParamExp("a2b")), ), }, { Strs: []string{`$aàb $àb $,b`}, common: call( word(litParamExp("a"), lit("àb")), word(lit("$"), lit("àb")), word(lit("$"), lit(",b")), ), }, { Strs: []string{"$à", "$\\\nà"}, common: word(lit("$"), lit("à")), }, { Strs: []string{"$foobar", "$foo\\\nbar"}, common: call( word(litParamExp("foobar")), ), }, { Strs: []string{"$foo\\bar"}, common: call( word(litParamExp("foo"), lit("\\bar")), ), }, { Strs: []string{`echo -e "$foo\nbar"`}, common: call( litWord("echo"), litWord("-e"), word(dblQuoted( litParamExp("foo"), lit(`\nbar`), )), ), }, { Strs: []string{`${foo-bar}`}, common: &ParamExp{ Param: lit("foo"), Exp: &Expansion{ Op: DefaultUnset, Word: litWord("bar"), }, }, }, { Strs: []string{`${foo+}"bar"`}, common: word( &ParamExp{ Param: lit("foo"), Exp: &Expansion{Op: AlternateUnset}, }, dblQuoted(lit("bar")), ), }, { Strs: []string{`${foo:=<"bar"}`}, common: &ParamExp{ Param: lit("foo"), Exp: &Expansion{ Op: AssignUnsetOrNull, Word: word(lit("<"), dblQuoted(lit("bar"))), }, }, }, { Strs: []string{ "${foo:=b${c}$(d)}", "${foo:=b${c}`d`}", }, common: &ParamExp{ Param: lit("foo"), Exp: &Expansion{ Op: AssignUnsetOrNull, Word: word( lit("b"), &ParamExp{Param: lit("c")}, cmdSubst(litStmt("d")), ), }, }, }, { Strs: []string{`${foo?"${bar}"}`}, common: &ParamExp{ Param: lit("foo"), Exp: &Expansion{ Op: ErrorUnset, Word: word(dblQuoted( &ParamExp{Param: lit("bar")}, )), }, }, }, { Strs: []string{`${foo:?bar1 bar2}`}, common: &ParamExp{ Param: lit("foo"), Exp: &Expansion{ Op: ErrorUnsetOrNull, Word: litWord("bar1 bar2"), }, }, }, { Strs: []string{`${a:+b}${a:-b}${a=b}`}, common: word( &ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: AlternateUnsetOrNull, Word: litWord("b"), }, }, &ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: DefaultUnsetOrNull, Word: litWord("b"), }, }, &ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: AssignUnset, Word: litWord("b"), }, }, ), }, { Strs: []string{`${3:-'$x'}`}, common: &ParamExp{ Param: lit("3"), Exp: &Expansion{ Op: DefaultUnsetOrNull, Word: word(sglQuoted("$x")), }, }, }, { Strs: []string{`${@:-$x}`}, common: &ParamExp{ Param: lit("@"), Exp: &Expansion{ Op: DefaultUnsetOrNull, Word: word(litParamExp("x")), }, }, }, { Strs: []string{`${var#*'="'}`}, common: &ParamExp{ Param: lit("var"), Exp: &Expansion{ Op: RemSmallPrefix, Word: word(lit("*"), sglQuoted(`="`)), }, }, }, { Strs: []string{`${var/'a'/b'c'd}`}, bsmk: &ParamExp{ Param: lit("var"), Repl: &Replace{ Orig: word(sglQuoted("a")), With: word(lit("b"), sglQuoted("c"), lit("d")), }, }, }, { Strs: []string{`${foo%bar}${foo%%bar*}`}, common: word( &ParamExp{ Param: lit("foo"), Exp: &Expansion{ Op: RemSmallSuffix, Word: litWord("bar"), }, }, &ParamExp{ Param: lit("foo"), Exp: &Expansion{ Op: RemLargeSuffix, Word: litWord("bar*"), }, }, ), }, { Strs: []string{`${3#bar}${-##bar*}`}, common: word( &ParamExp{ Param: lit("3"), Exp: &Expansion{ Op: RemSmallPrefix, Word: litWord("bar"), }, }, &ParamExp{ Param: lit("-"), Exp: &Expansion{ Op: RemLargePrefix, Word: litWord("bar*"), }, }, ), }, { Strs: []string{`${foo%?}`}, common: &ParamExp{ Param: lit("foo"), Exp: &Expansion{ Op: RemSmallSuffix, Word: litWord("?"), }, }, }, { Strs: []string{ `${foo[1]}`, `${foo[ 1 ]}`, }, bsmk: &ParamExp{ Param: lit("foo"), Index: litWord("1"), }, }, { Strs: []string{`${foo[-1]}`}, bsmk: &ParamExp{ Param: lit("foo"), Index: &UnaryArithm{ Op: Minus, X: litWord("1"), }, }, }, { Strs: []string{`${foo[@]}`}, bsmk: &ParamExp{ Param: lit("foo"), Index: litWord("@"), }, }, { Strs: []string{`${foo[*]-etc}`}, bsmk: &ParamExp{ Param: lit("foo"), Index: litWord("*"), Exp: &Expansion{ Op: DefaultUnset, Word: litWord("etc"), }, }, }, { Strs: []string{`${foo[bar]}`}, bsmk: &ParamExp{ Param: lit("foo"), Index: litWord("bar"), }, }, { Strs: []string{`${foo[$bar]}`}, bsmk: &ParamExp{ Param: lit("foo"), Index: word(litParamExp("bar")), }, }, { Strs: []string{`${foo[${bar}]}`}, bsmk: &ParamExp{ Param: lit("foo"), Index: word(&ParamExp{Param: lit("bar")}), }, }, { Strs: []string{`${foo:1}`, `${foo: 1 }`}, bsmk: &ParamExp{ Param: lit("foo"), Slice: &Slice{Offset: litWord("1")}, }, }, { Strs: []string{`${foo:1:2}`, `${foo: 1 : 2 }`}, bsmk: &ParamExp{ Param: lit("foo"), Slice: &Slice{ Offset: litWord("1"), Length: litWord("2"), }, }, }, { Strs: []string{`${foo:a:b}`}, bsmk: &ParamExp{ Param: lit("foo"), Slice: &Slice{ Offset: litWord("a"), Length: litWord("b"), }, }, }, { Strs: []string{`${foo:1:-2}`}, bsmk: &ParamExp{ Param: lit("foo"), Slice: &Slice{ Offset: litWord("1"), Length: &UnaryArithm{Op: Minus, X: litWord("2")}, }, }, }, { Strs: []string{`${foo::+3}`}, bsmk: &ParamExp{ Param: lit("foo"), Slice: &Slice{ Length: &UnaryArithm{Op: Plus, X: litWord("3")}, }, }, }, { Strs: []string{`${foo: -1}`}, bsmk: &ParamExp{ Param: lit("foo"), Slice: &Slice{ Offset: &UnaryArithm{Op: Minus, X: litWord("1")}, }, }, }, { Strs: []string{`${foo: +2+3}`}, bsmk: &ParamExp{ Param: lit("foo"), Slice: &Slice{ Offset: &BinaryArithm{ Op: Add, X: &UnaryArithm{Op: Plus, X: litWord("2")}, Y: litWord("3"), }, }, }, }, { Strs: []string{`${foo:a?1:2:3}`}, bsmk: &ParamExp{ Param: lit("foo"), Slice: &Slice{ Offset: &BinaryArithm{ Op: TernQuest, X: litWord("a"), Y: &BinaryArithm{ Op: TernColon, X: litWord("1"), Y: litWord("2"), }, }, Length: litWord("3"), }, }, }, { Strs: []string{`${foo/a/b}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{Orig: litWord("a"), With: litWord("b")}, }, }, { Strs: []string{"${foo/ /\t}"}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{Orig: litWord(" "), With: litWord("\t")}, }, }, { Strs: []string{`${foo/[/]-}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{Orig: litWord("["), With: litWord("]-")}, }, }, { Strs: []string{`${foo/bar/b/a/r}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{ Orig: litWord("bar"), With: litWord("b/a/r"), }, }, }, { Strs: []string{`${foo/$a/$'\''}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{ Orig: word(litParamExp("a")), With: word(sglDQuoted(`\'`)), }, }, }, { Strs: []string{`${foo//b1/b2}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{ All: true, Orig: litWord("b1"), With: litWord("b2"), }, }, }, { Strs: []string{`${foo///}`, `${foo//}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{All: true}, }, }, { Strs: []string{`${foo/-//}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{Orig: litWord("-"), With: litWord("/")}, }, }, { Strs: []string{`${foo//#/}`, `${foo//#}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{All: true, Orig: litWord("#")}, }, }, { Strs: []string{`${foo//[42]/}`}, bsmk: &ParamExp{ Param: lit("foo"), Repl: &Replace{All: true, Orig: litWord("[42]")}, }, }, { Strs: []string{`${a^b} ${a^^b} ${a,b} ${a,,b}`}, bash: call( word(&ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: UpperFirst, Word: litWord("b"), }, }), word(&ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: UpperAll, Word: litWord("b"), }, }), word(&ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: LowerFirst, Word: litWord("b"), }, }), word(&ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: LowerAll, Word: litWord("b"), }, }), ), }, { Strs: []string{`${a@E} ${b@a} ${@@Q} ${!ref@P}`}, bash: call( word(&ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: OtherParamOps, Word: litWord("E"), }, }), word(&ParamExp{ Param: lit("b"), Exp: &Expansion{ Op: OtherParamOps, Word: litWord("a"), }, }), word(&ParamExp{ Param: lit("@"), Exp: &Expansion{ Op: OtherParamOps, Word: litWord("Q"), }, }), word(&ParamExp{ Excl: true, Param: lit("ref"), Exp: &Expansion{ Op: OtherParamOps, Word: litWord("P"), }, }), ), }, { Strs: []string{`${a@Q} ${b@#}`}, mksh: call( word(&ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: OtherParamOps, Word: litWord("Q"), }, }), word(&ParamExp{ Param: lit("b"), Exp: &Expansion{ Op: OtherParamOps, Word: litWord("#"), }, }), ), }, { Strs: []string{`${#foo}`}, common: &ParamExp{ Length: true, Param: lit("foo"), }, }, { Strs: []string{`${%foo}`}, mksh: &ParamExp{ Width: true, Param: lit("foo"), }, }, { Strs: []string{`${!foo} ${!bar[@]}`}, bsmk: call( word(&ParamExp{ Excl: true, Param: lit("foo"), }), word(&ParamExp{ Excl: true, Param: lit("bar"), Index: litWord("@"), }), ), }, { Strs: []string{`${!foo*} ${!bar@}`}, bsmk: call( word(&ParamExp{ Excl: true, Param: lit("foo"), Names: NamesPrefix, }), word(&ParamExp{ Excl: true, Param: lit("bar"), Names: NamesPrefixWords, }), ), }, { Strs: []string{`${#?}`}, common: call( word(&ParamExp{Length: true, Param: lit("?")}), ), }, { Strs: []string{`${#-foo} ${#?bar}`}, common: call( word(&ParamExp{ Param: lit("#"), Exp: &Expansion{ Op: DefaultUnset, Word: litWord("foo"), }, }), word(&ParamExp{ Param: lit("#"), Exp: &Expansion{ Op: ErrorUnset, Word: litWord("bar"), }, }), ), }, { Strs: []string{`"${foo}"`}, common: dblQuoted(&ParamExp{Param: lit("foo")}), }, { Strs: []string{`"(foo)"`}, common: dblQuoted(lit("(foo)")), }, { Strs: []string{`"${foo}>"`}, common: dblQuoted( &ParamExp{Param: lit("foo")}, lit(">"), ), }, { Strs: []string{`"$(foo)"`, "\"`foo`\""}, common: dblQuoted(cmdSubst(litStmt("foo"))), }, { Strs: []string{ `"$(foo bar)"`, `"$(foo bar)"`, "\"`foo bar`\"", "\"`foo bar`\"", }, common: dblQuoted(cmdSubst(litStmt("foo", "bar"))), }, { Strs: []string{`'${foo}'`}, common: sglQuoted("${foo}"), }, { Strs: []string{"$((1))"}, common: arithmExp(litWord("1")), }, { Strs: []string{"$((1 + 3))", "$((1+3))"}, common: arithmExp(&BinaryArithm{ Op: Add, X: litWord("1"), Y: litWord("3"), }), }, { Strs: []string{`"$((foo))"`}, common: dblQuoted(arithmExp( litWord("foo"), )), }, { Strs: []string{`$((a)) b`}, common: call( word(arithmExp(litWord("a"))), litWord("b"), ), }, { Strs: []string{`$((arr[0]++))`}, bsmk: arithmExp(&UnaryArithm{ Op: Inc, Post: true, X: word(&ParamExp{ Short: true, Param: lit("arr"), Index: litWord("0"), }), }), }, { Strs: []string{`$((++arr[0]))`}, bsmk: arithmExp(&UnaryArithm{ Op: Inc, X: word(&ParamExp{ Short: true, Param: lit("arr"), Index: litWord("0"), }), }), }, { Strs: []string{`$((${a:-1}))`}, bsmk: arithmExp(word(&ParamExp{ Param: lit("a"), Exp: &Expansion{ Op: DefaultUnsetOrNull, Word: litWord("1"), }, })), }, { Strs: []string{"$((5 * 2 - 1))", "$((5*2-1))"}, common: arithmExp(&BinaryArithm{ Op: Sub, X: &BinaryArithm{ Op: Mul, X: litWord("5"), Y: litWord("2"), }, Y: litWord("1"), }), }, { Strs: []string{"$((i | 13))"}, common: arithmExp(&BinaryArithm{ Op: Or, X: litWord("i"), Y: litWord("13"), }), }, { Strs: []string{ "$(((a) + ((b))))", "$((\n(a) + \n(\n(b)\n)\n))", }, common: arithmExp(&BinaryArithm{ Op: Add, X: parenArit(litWord("a")), Y: parenArit(parenArit(litWord("b"))), }), }, { Strs: []string{ "$((3 % 7))", "$((3\n% 7))", "$((3\\\n % 7))", }, common: arithmExp(&BinaryArithm{ Op: Rem, X: litWord("3"), Y: litWord("7"), }), }, { Strs: []string{`"$((1 / 3))"`}, common: dblQuoted(arithmExp(&BinaryArithm{ Op: Quo, X: litWord("1"), Y: litWord("3"), })), }, { Strs: []string{"$((2 ** 10))"}, common: arithmExp(&BinaryArithm{ Op: Pow, X: litWord("2"), Y: litWord("10"), }), }, { Strs: []string{`$(((1) ^ 3))`}, common: arithmExp(&BinaryArithm{ Op: Xor, X: parenArit(litWord("1")), Y: litWord("3"), }), }, { Strs: []string{`$((1 >> (3 << 2)))`}, common: arithmExp(&BinaryArithm{ Op: Shr, X: litWord("1"), Y: parenArit(&BinaryArithm{ Op: Shl, X: litWord("3"), Y: litWord("2"), }), }), }, { Strs: []string{`$((-(1)))`}, common: arithmExp(&UnaryArithm{ Op: Minus, X: parenArit(litWord("1")), }), }, { Strs: []string{`$((i++))`}, common: arithmExp(&UnaryArithm{ Op: Inc, Post: true, X: litWord("i"), }), }, { Strs: []string{`$((--i))`}, common: arithmExp(&UnaryArithm{Op: Dec, X: litWord("i")}), }, { Strs: []string{`$((!i))`}, common: arithmExp(&UnaryArithm{Op: Not, X: litWord("i")}), }, { Strs: []string{`$((~i))`}, common: arithmExp(&UnaryArithm{Op: BitNegation, X: litWord("i")}), }, { Strs: []string{`$((-!+i))`}, common: arithmExp(&UnaryArithm{ Op: Minus, X: &UnaryArithm{ Op: Not, X: &UnaryArithm{Op: Plus, X: litWord("i")}, }, }), }, { Strs: []string{`$((!!i))`}, common: arithmExp(&UnaryArithm{ Op: Not, X: &UnaryArithm{Op: Not, X: litWord("i")}, }), }, { Strs: []string{`$((~~i))`}, common: arithmExp(&UnaryArithm{ Op: BitNegation, X: &UnaryArithm{Op: BitNegation, X: litWord("i")}, }), }, { Strs: []string{`$((1 < 3))`}, common: arithmExp(&BinaryArithm{ Op: Lss, X: litWord("1"), Y: litWord("3"), }), }, { Strs: []string{`$((i = 2))`, `$((i=2))`}, common: arithmExp(&BinaryArithm{ Op: Assgn, X: litWord("i"), Y: litWord("2"), }), }, { Strs: []string{`((a[i] = 4))`, `((a[i]=4))`}, bsmk: arithmCmd(&BinaryArithm{ Op: Assgn, X: word(&ParamExp{ Short: true, Param: lit("a"), Index: litWord("i"), }), Y: litWord("4"), }), }, { Strs: []string{"$((a += 2, b -= 3))"}, common: arithmExp(&BinaryArithm{ Op: Comma, X: &BinaryArithm{ Op: AddAssgn, X: litWord("a"), Y: litWord("2"), }, Y: &BinaryArithm{ Op: SubAssgn, X: litWord("b"), Y: litWord("3"), }, }), }, { Strs: []string{"$((a >>= 2, b <<= 3))"}, common: arithmExp(&BinaryArithm{ Op: Comma, X: &BinaryArithm{ Op: ShrAssgn, X: litWord("a"), Y: litWord("2"), }, Y: &BinaryArithm{ Op: ShlAssgn, X: litWord("b"), Y: litWord("3"), }, }), }, { Strs: []string{"$((a == b && c > d))"}, common: arithmExp(&BinaryArithm{ Op: AndArit, X: &BinaryArithm{ Op: Eql, X: litWord("a"), Y: litWord("b"), }, Y: &BinaryArithm{ Op: Gtr, X: litWord("c"), Y: litWord("d"), }, }), }, { Strs: []string{"$((a != b))"}, common: arithmExp(&BinaryArithm{ Op: Neq, X: litWord("a"), Y: litWord("b"), }), }, { Strs: []string{"$((a &= b))"}, common: arithmExp(&BinaryArithm{ Op: AndAssgn, X: litWord("a"), Y: litWord("b"), }), }, { Strs: []string{"$((a |= b))"}, common: arithmExp(&BinaryArithm{ Op: OrAssgn, X: litWord("a"), Y: litWord("b"), }), }, { Strs: []string{"$((a %= b))"}, common: arithmExp(&BinaryArithm{ Op: RemAssgn, X: litWord("a"), Y: litWord("b"), }), }, { Strs: []string{"$((a /= b))", "$((a/=b))"}, common: arithmExp(&BinaryArithm{ Op: QuoAssgn, X: litWord("a"), Y: litWord("b"), }), }, { Strs: []string{"$((a ^= b))"}, common: arithmExp(&BinaryArithm{ Op: XorAssgn, X: litWord("a"), Y: litWord("b"), }), }, { Strs: []string{"$((i *= 3))"}, common: arithmExp(&BinaryArithm{ Op: MulAssgn, X: litWord("i"), Y: litWord("3"), }), }, { Strs: []string{"$((2 >= 10))"}, common: arithmExp(&BinaryArithm{ Op: Geq, X: litWord("2"), Y: litWord("10"), }), }, { Strs: []string{"$((foo ? b1 : b2))"}, common: arithmExp(&BinaryArithm{ Op: TernQuest, X: litWord("foo"), Y: &BinaryArithm{ Op: TernColon, X: litWord("b1"), Y: litWord("b2"), }, }), }, { Strs: []string{`$((a <= (1 || 2)))`}, common: arithmExp(&BinaryArithm{ Op: Leq, X: litWord("a"), Y: parenArit(&BinaryArithm{ Op: OrArit, X: litWord("1"), Y: litWord("2"), }), }), }, { Strs: []string{"foo$", "foo$\n"}, common: word(lit("foo"), lit("$")), }, { Strs: []string{"foo$", "foo$\\\n"}, common: word(lit("foo"), lit("$")), }, { Strs: []string{`$''`}, bsmk: sglDQuoted(""), posix: word(lit("$"), sglQuoted("")), }, { Strs: []string{`$""`}, bsmk: dblDQuoted(), posix: word(lit("$"), dblQuoted()), }, { Strs: []string{`$'foo'`}, bsmk: sglDQuoted("foo"), posix: word(lit("$"), sglQuoted("foo")), }, { Strs: []string{`$'f+oo${'`}, bsmk: sglDQuoted("f+oo${"), }, { Strs: []string{"$'foo bar`'"}, bsmk: sglDQuoted("foo bar`"), }, { Strs: []string{"$'a ${b} c'"}, bsmk: sglDQuoted("a ${b} c"), }, { Strs: []string{`$"a ${b} c"`}, bsmk: dblDQuoted( lit("a "), &ParamExp{Param: lit("b")}, lit(" c"), ), }, { Strs: []string{`"a $b c"`}, common: dblQuoted(lit("a "), litParamExp("b"), lit(" c")), }, { Strs: []string{`$"a $b c"`}, bsmk: dblDQuoted( lit("a "), litParamExp("b"), lit(" c"), ), }, { Strs: []string{"$'f\\'oo\n'"}, bsmk: sglDQuoted("f\\'oo\n"), }, { Strs: []string{`$"foo"`}, bsmk: dblDQuoted(lit("foo")), posix: word(lit("$"), dblQuoted(lit("foo"))), }, { Strs: []string{`$"foo$"`}, bsmk: dblDQuoted(lit("foo"), lit("$")), }, { Strs: []string{`$"foo bar"`}, bsmk: dblDQuoted(lit("foo bar")), }, { Strs: []string{`$'f\'oo'`}, bsmk: sglDQuoted(`f\'oo`), }, { Strs: []string{`$"f\"oo"`}, bsmk: dblDQuoted(lit(`f\"oo`)), }, { Strs: []string{`"foo$"`}, common: dblQuoted(lit("foo"), lit("$")), }, { Strs: []string{`"foo$$"`}, common: dblQuoted(lit("foo"), litParamExp("$")), }, { Strs: []string{`"a $\"b\" c"`}, common: dblQuoted(lit(`a `), lit(`$`), lit(`\"b\" c`)), }, { Strs: []string{"$(foo$)", "`foo$`"}, common: cmdSubst( stmt(call(word(lit("foo"), lit("$")))), ), }, { Strs: []string{"foo$bar"}, common: word(lit("foo"), litParamExp("bar")), }, { Strs: []string{"foo$(bar)"}, common: word(lit("foo"), cmdSubst(litStmt("bar"))), }, { Strs: []string{"foo${bar}"}, common: word(lit("foo"), &ParamExp{Param: lit("bar")}), }, { Strs: []string{"'foo${bar'"}, common: sglQuoted("foo${bar"), }, { Strs: []string{"(foo)\nbar", "(foo); bar"}, common: []Command{ subshell(litStmt("foo")), litCall("bar"), }, }, { Strs: []string{"foo\n(bar)", "foo; (bar)"}, common: []Command{ litCall("foo"), subshell(litStmt("bar")), }, }, { Strs: []string{"foo\n(bar)", "foo; (bar)"}, common: []Command{ litCall("foo"), subshell(litStmt("bar")), }, }, { Strs: []string{ "case $i in 1) foo ;; 2 | 3*) bar ;; esac", "case $i in 1) foo;; 2 | 3*) bar; esac", "case $i in (1) foo;; 2 | 3*) bar;; esac", "case $i\nin\n#etc\n1)\nfoo\n;;\n2 | 3*)\nbar\n;;\nesac", }, common: &CaseClause{ Word: word(litParamExp("i")), Items: []*CaseItem{ { Op: Break, Patterns: litWords("1"), Stmts: litStmts("foo"), }, { Op: Break, Patterns: litWords("2", "3*"), Stmts: litStmts("bar"), }, }, }, }, { Strs: []string{"case i in 1) a ;& 2) ;; esac"}, bsmk: &CaseClause{ Word: litWord("i"), Items: []*CaseItem{ { Op: Fallthrough, Patterns: litWords("1"), Stmts: litStmts("a"), }, {Op: Break, Patterns: litWords("2")}, }, }, }, { Strs: []string{ "case i in 1) a ;; esac", "case i { 1) a ;; }", "case i {\n1) a ;;\n}", }, mksh: &CaseClause{ Word: litWord("i"), Items: []*CaseItem{{ Op: Break, Patterns: litWords("1"), Stmts: litStmts("a"), }}, }, }, { Strs: []string{"case i in 1) a ;;& 2) b ;; esac"}, bash: &CaseClause{ Word: litWord("i"), Items: []*CaseItem{ { Op: Resume, Patterns: litWords("1"), Stmts: litStmts("a"), }, { Op: Break, Patterns: litWords("2"), Stmts: litStmts("b"), }, }, }, }, { Strs: []string{"case i in 1) a ;| 2) b ;; esac"}, mksh: &CaseClause{ Word: litWord("i"), Items: []*CaseItem{ { Op: ResumeKorn, Patterns: litWords("1"), Stmts: litStmts("a"), }, { Op: Break, Patterns: litWords("2"), Stmts: litStmts("b"), }, }, }, }, { Strs: []string{"case $i in 1) cat <f; fi", "if a; then >f\nfi"}, common: &IfClause{ Cond: litStmts("a"), Then: []*Stmt{{ Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("f")}, }, }}, }, }, { Strs: []string{"if a; then (a); fi", "if a; then (a) fi"}, common: &IfClause{ Cond: litStmts("a"), Then: stmts(subshell(litStmt("a"))), }, }, { Strs: []string{"a=b\nc=d", "a=b; c=d"}, common: []Command{ &CallExpr{Assigns: []*Assign{ {Name: lit("a"), Value: litWord("b")}, }}, &CallExpr{Assigns: []*Assign{ {Name: lit("c"), Value: litWord("d")}, }}, }, }, { Strs: []string{"foo && write | read"}, common: &BinaryCmd{ Op: AndStmt, X: litStmt("foo"), Y: stmt(&BinaryCmd{ Op: Pipe, X: litStmt("write"), Y: litStmt("read"), }), }, }, { Strs: []string{"write | read && bar"}, common: &BinaryCmd{ Op: AndStmt, X: stmt(&BinaryCmd{ Op: Pipe, X: litStmt("write"), Y: litStmt("read"), }), Y: litStmt("bar"), }, }, { Strs: []string{"foo >f | bar"}, common: &BinaryCmd{ Op: Pipe, X: &Stmt{ Cmd: litCall("foo"), Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("f")}, }, }, Y: litStmt("bar"), }, }, { Strs: []string{"(foo) >f | bar"}, common: &BinaryCmd{ Op: Pipe, X: &Stmt{ Cmd: subshell(litStmt("foo")), Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("f")}, }, }, Y: litStmt("bar"), }, }, { Strs: []string{"foo | >f"}, common: &BinaryCmd{ Op: Pipe, X: litStmt("foo"), Y: &Stmt{Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("f")}, }}, }, }, { Strs: []string{"[[ a ]]"}, bsmk: &TestClause{X: litWord("a")}, posix: litStmt("[[", "a", "]]"), }, { Strs: []string{"[[ a ]]\nb"}, bsmk: stmts( &TestClause{X: litWord("a")}, litCall("b"), ), }, { Strs: []string{"[[ a > b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: TsAfter, X: litWord("a"), Y: litWord("b"), }}, }, { Strs: []string{"[[ 1 -nt 2 ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: TsNewer, X: litWord("1"), Y: litWord("2"), }}, }, { Strs: []string{"[[ 1 -eq 2 ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: TsEql, X: litWord("1"), Y: litWord("2"), }}, }, { Strs: []string{ "[[ -R a ]]", "[[\n-R a\n]]", }, bash: &TestClause{X: &UnaryTest{ Op: TsRefVar, X: litWord("a"), }}, }, { Strs: []string{"[[ a =~ b ]]", "[[ a =~ b ]];"}, bash: &TestClause{X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: litWord("b"), }}, }, { Strs: []string{`[[ a =~ " foo "$bar ]]`}, bash: &TestClause{X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: word( dblQuoted(lit(" foo ")), litParamExp("bar"), ), }}, }, { Strs: []string{`[[ a =~ foo"bar" ]]`}, bash: &TestClause{X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: word( lit("foo"), dblQuoted(lit("bar")), ), }}, }, { Strs: []string{`[[ a =~ [ab](c |d) ]]`}, bash: &TestClause{X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: litWord("[ab](c |d)"), }}, }, { Strs: []string{`[[ a =~ ( ]]<>;&) ]]`}, bash: &TestClause{X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: litWord("( ]]<>;&)"), }}, }, { Strs: []string{`[[ a =~ ($foo) ]]`}, bash: &TestClause{X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: word(lit("("), litParamExp("foo"), lit(")")), }}, }, { Strs: []string{`[[ a =~ b\ c|d ]]`}, bash: &TestClause{X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: litWord(`b\ c|d`), }}, }, { Strs: []string{`[[ a == -n ]]`}, bsmk: &TestClause{X: &BinaryTest{ Op: TsMatch, X: litWord("a"), Y: litWord("-n"), }}, }, { Strs: []string{`[[ a =~ -n ]]`}, bash: &TestClause{X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: litWord("-n"), }}, }, { Strs: []string{"[[ a =~ b$ || c =~ d$ ]]"}, bash: &TestClause{X: &BinaryTest{ Op: OrTest, X: &BinaryTest{ Op: TsReMatch, X: litWord("a"), Y: word(lit("b"), lit("$")), }, Y: &BinaryTest{ Op: TsReMatch, X: litWord("c"), Y: word(lit("d"), lit("$")), }, }}, }, { Strs: []string{"[[ -n $a ]]"}, bsmk: &TestClause{ X: &UnaryTest{Op: TsNempStr, X: word(litParamExp("a"))}, }, }, { Strs: []string{"[[ ! $a < 'b' ]]"}, bsmk: &TestClause{X: &UnaryTest{ Op: TsNot, X: &BinaryTest{ Op: TsBefore, X: word(litParamExp("a")), Y: word(sglQuoted("b")), }, }}, }, { Strs: []string{ "[[ ! -e $a ]]", "[[ ! -a $a ]]", "[[\n!\n-a $a\n]]", }, bsmk: &TestClause{X: &UnaryTest{ Op: TsNot, X: &UnaryTest{Op: TsExists, X: word(litParamExp("a"))}, }}, }, { Strs: []string{ "[[ a && b ]]", "[[\na &&\nb ]]", "[[\n\na &&\n\nb ]]", }, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: litWord("a"), Y: litWord("b"), }}, }, { Strs: []string{"[[ (a && b) ]]"}, bsmk: &TestClause{X: parenTest(&BinaryTest{ Op: AndTest, X: litWord("a"), Y: litWord("b"), })}, }, { Strs: []string{ "[[ a && (b) ]]", "[[ a &&\n(\nb) ]]", }, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: litWord("a"), Y: parenTest(litWord("b")), }}, }, { Strs: []string{"[[ (a && b) || -f c ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: OrTest, X: parenTest(&BinaryTest{ Op: AndTest, X: litWord("a"), Y: litWord("b"), }), Y: &UnaryTest{Op: TsRegFile, X: litWord("c")}, }}, }, { Strs: []string{ "[[ -S a && -L b ]]", "[[ -S a && -h b ]]", }, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsSocket, X: litWord("a")}, Y: &UnaryTest{Op: TsSmbLink, X: litWord("b")}, }}, }, { Strs: []string{"[[ -k a && -N b ]]"}, bash: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsSticky, X: litWord("a")}, Y: &UnaryTest{Op: TsModif, X: litWord("b")}, }}, }, { Strs: []string{"[[ -G a && -O b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsGrpOwn, X: litWord("a")}, Y: &UnaryTest{Op: TsUsrOwn, X: litWord("b")}, }}, }, { Strs: []string{"[[ -d a && -c b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsDirect, X: litWord("a")}, Y: &UnaryTest{Op: TsCharSp, X: litWord("b")}, }}, }, { Strs: []string{"[[ -b a && -p b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsBlckSp, X: litWord("a")}, Y: &UnaryTest{Op: TsNmPipe, X: litWord("b")}, }}, }, { Strs: []string{"[[ -g a && -u b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsGIDSet, X: litWord("a")}, Y: &UnaryTest{Op: TsUIDSet, X: litWord("b")}, }}, }, { Strs: []string{"[[ -r a && -w b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsRead, X: litWord("a")}, Y: &UnaryTest{Op: TsWrite, X: litWord("b")}, }}, }, { Strs: []string{"[[ -x a && -s b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsExec, X: litWord("a")}, Y: &UnaryTest{Op: TsNoEmpty, X: litWord("b")}, }}, }, { Strs: []string{"[[ -t a && -z b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsFdTerm, X: litWord("a")}, Y: &UnaryTest{Op: TsEmpStr, X: litWord("b")}, }}, }, { Strs: []string{"[[ -o a && -v b ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &UnaryTest{Op: TsOptSet, X: litWord("a")}, Y: &UnaryTest{Op: TsVarSet, X: litWord("b")}, }}, }, { Strs: []string{"[[ a -ot b && c -ef d ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &BinaryTest{ Op: TsOlder, X: litWord("a"), Y: litWord("b"), }, Y: &BinaryTest{ Op: TsDevIno, X: litWord("c"), Y: litWord("d"), }, }}, }, { Strs: []string{"[[ a = b && c != d ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &BinaryTest{ Op: TsMatchShort, X: litWord("a"), Y: litWord("b"), }, Y: &BinaryTest{ Op: TsNoMatch, X: litWord("c"), Y: litWord("d"), }, }}, }, { Strs: []string{"[[ a -ne b && c -le d ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &BinaryTest{ Op: TsNeq, X: litWord("a"), Y: litWord("b"), }, Y: &BinaryTest{ Op: TsLeq, X: litWord("c"), Y: litWord("d"), }, }}, }, { Strs: []string{"[[ c -ge d ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: TsGeq, X: litWord("c"), Y: litWord("d"), }}, }, { Strs: []string{"[[ a -lt b && c -gt d ]]"}, bsmk: &TestClause{X: &BinaryTest{ Op: AndTest, X: &BinaryTest{ Op: TsLss, X: litWord("a"), Y: litWord("b"), }, Y: &BinaryTest{ Op: TsGtr, X: litWord("c"), Y: litWord("d"), }, }}, }, { Strs: []string{"declare -f func"}, common: litStmt("declare", "-f", "func"), bash: &DeclClause{ Variant: lit("declare"), Args: []*Assign{ {Naked: true, Value: litWord("-f")}, {Naked: true, Name: lit("func")}, }, }, }, { Strs: []string{"(local bar)"}, bsmk: subshell(stmt(&DeclClause{ Variant: lit("local"), Args: []*Assign{{ Naked: true, Name: lit("bar"), }}, })), posix: subshell(litStmt("local", "bar")), }, { Strs: []string{"typeset"}, bsmk: &DeclClause{Variant: lit("typeset")}, posix: litStmt("typeset"), }, { Strs: []string{"export bar"}, bsmk: &DeclClause{ Variant: lit("export"), Args: []*Assign{{ Naked: true, Name: lit("bar"), }}, }, posix: litStmt("export", "bar"), }, { Strs: []string{"readonly -n"}, bsmk: &DeclClause{ Variant: lit("readonly"), Args: []*Assign{{Naked: true, Value: litWord("-n")}}, }, posix: litStmt("readonly", "-n"), }, { Strs: []string{"nameref bar="}, bsmk: &DeclClause{ Variant: lit("nameref"), Args: []*Assign{{ Name: lit("bar"), }}, }, posix: litStmt("nameref", "bar="), }, { Strs: []string{"declare -a +n -b$o foo=bar"}, bash: &DeclClause{ Variant: lit("declare"), Args: []*Assign{ {Naked: true, Value: litWord("-a")}, {Naked: true, Value: litWord("+n")}, {Naked: true, Value: word(lit("-b"), litParamExp("o"))}, {Name: lit("foo"), Value: litWord("bar")}, }, }, }, { Strs: []string{ "declare -a foo=(b1 $(b2))", "declare -a foo=(b1 `b2`)", }, bash: &DeclClause{ Variant: lit("declare"), Args: []*Assign{ {Naked: true, Value: litWord("-a")}, { Name: lit("foo"), Array: arrValues( litWord("b1"), word(cmdSubst(litStmt("b2"))), ), }, }, }, }, { Strs: []string{"local -a foo=(b1)"}, bash: &DeclClause{ Variant: lit("local"), Args: []*Assign{ {Naked: true, Value: litWord("-a")}, { Name: lit("foo"), Array: arrValues(litWord("b1")), }, }, }, }, { Strs: []string{"declare -A foo=([a]=b)"}, bash: &DeclClause{ Variant: lit("declare"), Args: []*Assign{ {Naked: true, Value: litWord("-A")}, { Name: lit("foo"), Array: &ArrayExpr{Elems: []*ArrayElem{{ Index: litWord("a"), Value: litWord("b"), }}}, }, }, }, }, { Strs: []string{"declare foo[a]="}, bash: &DeclClause{ Variant: lit("declare"), Args: []*Assign{{ Name: lit("foo"), Index: litWord("a"), }}, }, }, { Strs: []string{"declare foo[*]"}, bash: &DeclClause{ Variant: lit("declare"), Args: []*Assign{{ Name: lit("foo"), Index: litWord("*"), Naked: true, }}, }, }, { Strs: []string{`declare foo["x y"]`}, bash: &DeclClause{ Variant: lit("declare"), Args: []*Assign{{ Name: lit("foo"), Index: word(dblQuoted(lit("x y"))), Naked: true, }}, }, }, { Strs: []string{`declare foo['x y']`}, bash: &DeclClause{ Variant: lit("declare"), Args: []*Assign{{ Name: lit("foo"), Index: word(sglQuoted("x y")), Naked: true, }}, }, }, { Strs: []string{"foo=([)"}, mksh: &CallExpr{Assigns: []*Assign{{ Name: lit("foo"), Array: arrValues(litWord("[")), }}}, }, { Strs: []string{ "a && b=(c)\nd", "a && b=(c); d", }, bsmk: stmts( &BinaryCmd{ Op: AndStmt, X: litStmt("a"), Y: stmt(&CallExpr{Assigns: []*Assign{{ Name: lit("b"), Array: arrValues(litWord("c")), }}}), }, litCall("d"), ), }, { Strs: []string{"declare -f $func >/dev/null"}, bash: &Stmt{ Cmd: &DeclClause{ Variant: lit("declare"), Args: []*Assign{ {Naked: true, Value: litWord("-f")}, { Naked: true, Value: word(litParamExp("func")), }, }, }, Redirs: []*Redirect{ {Op: RdrOut, Word: litWord("/dev/null")}, }, }, }, { Strs: []string{"declare a\n{ x; }"}, bash: stmts( &DeclClause{ Variant: lit("declare"), Args: []*Assign{{ Naked: true, Name: lit("a"), }}, }, block(litStmt("x")), ), }, { Strs: []string{"eval a=b foo"}, common: litStmt("eval", "a=b", "foo"), }, { Strs: []string{"time", "time\n"}, posix: litStmt("time"), bsmk: &TimeClause{}, }, { Strs: []string{"time -p"}, posix: litStmt("time", "-p"), bsmk: &TimeClause{PosixFormat: true}, }, { Strs: []string{"time -a"}, posix: litStmt("time", "-a"), bsmk: &TimeClause{Stmt: litStmt("-a")}, }, { Strs: []string{"time --"}, posix: litStmt("time", "--"), bsmk: &TimeClause{Stmt: litStmt("--")}, }, { Strs: []string{"time foo"}, bsmk: &TimeClause{Stmt: litStmt("foo")}, }, { Strs: []string{"time { foo; }"}, bsmk: &TimeClause{Stmt: stmt(block(litStmt("foo")))}, }, { Strs: []string{"time\nfoo"}, bsmk: []*Stmt{ stmt(&TimeClause{}), litStmt("foo"), }, }, { Strs: []string{"coproc foo bar"}, common: litStmt("coproc", "foo", "bar"), bash: &CoprocClause{Stmt: litStmt("foo", "bar")}, }, { Strs: []string{"coproc name { foo; }"}, bash: &CoprocClause{ Name: litWord("name"), Stmt: stmt(block(litStmt("foo"))), }, }, { Strs: []string{"coproc $namevar { foo; }"}, bash: &CoprocClause{ Name: word(litParamExp("namevar")), Stmt: stmt(block(litStmt("foo"))), }, }, { Strs: []string{"coproc foo", "coproc foo;"}, bash: &CoprocClause{Stmt: litStmt("foo")}, }, { Strs: []string{"coproc { foo; }"}, bash: &CoprocClause{ Stmt: stmt(block(litStmt("foo"))), }, }, { Strs: []string{"coproc (foo)"}, bash: &CoprocClause{ Stmt: stmt(subshell(litStmt("foo"))), }, }, { Strs: []string{"coproc name foo | bar"}, bash: &CoprocClause{ Name: litWord("name"), Stmt: stmt(&BinaryCmd{ Op: Pipe, X: litStmt("foo"), Y: litStmt("bar"), }), }, }, { Strs: []string{"coproc $()", "coproc ``"}, bash: &CoprocClause{Stmt: stmt(call( word(cmdSubst()), ))}, }, { Strs: []string{`let i++`}, bsmk: letClause( &UnaryArithm{Op: Inc, Post: true, X: litWord("i")}, ), posix: litStmt("let", "i++"), }, { Strs: []string{`let a++ b++ c +d`}, bsmk: letClause( &UnaryArithm{Op: Inc, Post: true, X: litWord("a")}, &UnaryArithm{Op: Inc, Post: true, X: litWord("b")}, litWord("c"), &UnaryArithm{Op: Plus, X: litWord("d")}, ), }, { Strs: []string{`let ++i >/dev/null`}, bsmk: &Stmt{ Cmd: letClause(&UnaryArithm{Op: Inc, X: litWord("i")}), Redirs: []*Redirect{{Op: RdrOut, Word: litWord("/dev/null")}}, }, }, { Strs: []string{ `let a=(1 + 2) b=3+4`, `let a=(1+2) b=3+4`, }, bash: letClause( &BinaryArithm{ Op: Assgn, X: litWord("a"), Y: parenArit(&BinaryArithm{ Op: Add, X: litWord("1"), Y: litWord("2"), }), }, &BinaryArithm{ Op: Assgn, X: litWord("b"), Y: &BinaryArithm{ Op: Add, X: litWord("3"), Y: litWord("4"), }, }, ), }, { Strs: []string{ `let a=$(echo 3)`, "let a=`echo 3`", }, bash: letClause( &BinaryArithm{ Op: Assgn, X: litWord("a"), Y: word(cmdSubst(litStmt("echo", "3"))), }, ), }, { Strs: []string{"(foo-bar)"}, common: subshell(litStmt("foo-bar")), }, { Strs: []string{ "let i++\nbar", "let i++ \nbar", "let i++; bar", }, bsmk: stmts( letClause(&UnaryArithm{ Op: Inc, Post: true, X: litWord("i"), }), litCall("bar"), ), }, { Strs: []string{ "let i++\nfoo=(bar)", "let i++; foo=(bar)", "let i++; foo=(bar)\n", }, bsmk: stmts( letClause(&UnaryArithm{ Op: Inc, Post: true, X: litWord("i"), }), &CallExpr{Assigns: []*Assign{{ Name: lit("foo"), Array: arrValues(litWord("bar")), }}}, ), }, { Strs: []string{ "case a in b) let i++ ;; esac", "case a in b) let i++;; esac", }, bsmk: &CaseClause{ Word: word(lit("a")), Items: []*CaseItem{{ Op: Break, Patterns: litWords("b"), Stmts: stmts(letClause(&UnaryArithm{ Op: Inc, Post: true, X: litWord("i"), })), }}, }, }, { Strs: []string{"a+=1"}, bsmk: &CallExpr{ Assigns: []*Assign{{ Append: true, Name: lit("a"), Value: litWord("1"), }}, }, posix: litStmt("a+=1"), }, { Strs: []string{"b+=(2 3)"}, bsmk: &CallExpr{Assigns: []*Assign{{ Append: true, Name: lit("b"), Array: arrValues(litWords("2", "3")...), }}}, }, { Strs: []string{"a[2]=b c[-3]= d[x]+=e"}, posix: litStmt("a[2]=b", "c[-3]=", "d[x]+=e"), bsmk: &CallExpr{Assigns: []*Assign{ { Name: lit("a"), Index: litWord("2"), Value: litWord("b"), }, { Name: lit("c"), Index: &UnaryArithm{ Op: Minus, X: litWord("3"), }, }, { Name: lit("d"), Index: litWord("x"), Append: true, Value: litWord("e"), }, }}, }, { Strs: []string{"*[i]=x"}, posix: lit("*[i]=x"), common: word(lit("*"), lit("[i]=x")), }, { Strs: []string{ "b[i]+=2", "b[ i ]+=2", }, bsmk: &CallExpr{Assigns: []*Assign{{ Append: true, Name: lit("b"), Index: litWord("i"), Value: litWord("2"), }}}, }, { Strs: []string{`$((a + "b + $c"))`}, common: arithmExp(&BinaryArithm{ Op: Add, X: litWord("a"), Y: word(dblQuoted( lit("b + "), litParamExp("c"), )), }), }, { Strs: []string{`let 'i++'`}, bsmk: letClause(word(sglQuoted("i++"))), }, { Strs: []string{`echo ${a["x y"]}`}, bash: call(litWord("echo"), word(&ParamExp{ Param: lit("a"), Index: word(dblQuoted(lit("x y"))), })), }, { Strs: []string{ `a[$"x y"]=b`, `a[ $"x y" ]=b`, }, bash: &CallExpr{Assigns: []*Assign{{ Name: lit("a"), Index: word(&DblQuoted{Dollar: true, Parts: []WordPart{ lit("x y"), }}), Value: litWord("b"), }}}, }, { Strs: []string{`((a["x y"] = b))`, `((a["x y"]=b))`}, bsmk: arithmCmd(&BinaryArithm{ Op: Assgn, X: word(&ParamExp{ Short: true, Param: lit("a"), Index: word(dblQuoted(lit("x y"))), }), Y: litWord("b"), }), }, { Strs: []string{ `a=(["x y"]=b)`, `a=( [ "x y" ]=b)`, }, bash: &CallExpr{Assigns: []*Assign{{ Name: lit("a"), Array: &ArrayExpr{Elems: []*ArrayElem{{ Index: word(dblQuoted(lit("x y"))), Value: litWord("b"), }}}, }}}, }, { Strs: []string{ "a=([x]= [y]=)", "a=(\n[x]=\n[y]=\n)", }, bash: &CallExpr{Assigns: []*Assign{{ Name: lit("a"), Array: &ArrayExpr{Elems: []*ArrayElem{ {Index: litWord("x")}, {Index: litWord("y")}, }}, }}}, }, { Strs: []string{"a]b"}, common: litStmt("a]b"), }, { Strs: []string{"echo a[b c[de]f"}, posix: litStmt("echo", "a[b", "c[de]f"), bsmk: call(litWord("echo"), word(lit("a"), lit("[b")), word(lit("c"), lit("[de]f")), ), }, { Strs: []string{"< uint(len(src)) { tb.Fatalf("Pos %d in %T is out of bounds in %q", pos, v, src) return } if strs == nil { return } if strings.Contains(src, "<<-") { // since the tab indentation in <<- heredoc bodies // aren't part of the final literals return } var gotErr string for i, want := range strs { got := src[offs:] if i == 0 { gotErr = got } if !strings.Contains(want, "\\\n") { // Hack to let "foobar" match the input "foo\\\nbar". got = strings.ReplaceAll(got, "\\\n", "") } got = strings.ReplaceAll(got, "\x00", "") if strings.HasPrefix(got, want) { return } } tb.Fatalf("Expected one of %q at %d in %q, found %q", strs, pos, src, gotErr) } setPos := func(p *Pos, strs ...string) { checkSrc(*p, strs...) if *p == zeroPos { tb.Fatalf("Pos in %T is already %v", v, zeroPos) } *p = zeroPos } checkPos := func(n Node) { if n == nil { return } if n.Pos() != zeroPos { tb.Fatalf("Found unexpected Pos() in %T: want %d, got %d", n, zeroPos, n.Pos()) } if n.Pos().After(n.End()) { tb.Fatalf("Found End() before Pos() in %T", n) } } recurse := func(v interface{}) { clearPosRecurse(tb, src, v) if n, ok := v.(Node); ok { checkPos(n) } } switch x := v.(type) { case *File: recurse(x.Stmts) recurse(x.Last) checkPos(x) case []*Stmt: for _, s := range x { recurse(s) } case []Comment: for i := range x { recurse(&x[i]) } case *Comment: setPos(&x.Hash, "#"+x.Text) case *Stmt: endOff := int(x.End().Offset()) if endOff < len(src) { end := src[endOff] switch { case end == ' ', end == '\n', end == '\t', end == '\r': // ended by whitespace case regOps(rune(end)): // ended by end character case endOff > 0 && src[endOff-1] == ';': // ended by semicolon case endOff > 0 && src[endOff-1] == '&': // ended by & or |& default: tb.Fatalf("Unexpected Stmt.End() %d %q in %q", endOff, end, src) } } recurse(x.Comments) if src[x.Position.Offset()] == '#' { tb.Fatalf("Stmt.Pos() should not be a comment") } setPos(&x.Position) if x.Semicolon.IsValid() { setPos(&x.Semicolon, ";", "&", "|&") } if x.Cmd != nil { recurse(x.Cmd) } for _, r := range x.Redirs { setPos(&r.OpPos, r.Op.String()) if r.N != nil { recurse(r.N) } recurse(r.Word) if r.Hdoc != nil { recurse(r.Hdoc) } } case []*Assign: for _, a := range x { if a.Name != nil { recurse(a.Name) } if a.Index != nil { recurse(a.Index) } if a.Value != nil { recurse(a.Value) } if a.Array != nil { recurse(a.Array) } checkPos(a) } case *CallExpr: recurse(x.Assigns) recurse(x.Args) case []*Word: for _, w := range x { recurse(w) } case *Word: recurse(x.Parts) case []WordPart: for _, wp := range x { recurse(wp) } case *Lit: pos, end := int(x.Pos().Offset()), int(x.End().Offset()) want := pos + len(x.Value) val := x.Value posLine := x.Pos().Line() endLine := x.End().Line() switch { case src == "": case strings.Contains(src, "\\\n"): case !strings.Contains(x.Value, "\n") && posLine != endLine: tb.Fatalf("Lit without newlines has Pos/End lines %d and %d", posLine, endLine) case strings.Contains(src, "`") && strings.Contains(src, "\\"): // removed quotes inside backquote cmd substs val = "" case end < len(src) && src[end] == '\n': // heredoc literals that end with the // stop word and a newline case end == len(src): // same as above, but with word and EOF case end != want: tb.Fatalf("Unexpected Lit %q End() %d (wanted %d) in %q", val, end, want, src) } setPos(&x.ValuePos, val) setPos(&x.ValueEnd) case *Subshell: setPos(&x.Lparen, "(") setPos(&x.Rparen, ")") recurse(x.Stmts) recurse(x.Last) case *Block: setPos(&x.Lbrace, "{") setPos(&x.Rbrace, "}") recurse(x.Stmts) recurse(x.Last) case *IfClause: if x.ThenPos.IsValid() { setPos(&x.Position, "if", "elif") setPos(&x.ThenPos, "then") } else { setPos(&x.Position, "else") } setPos(&x.FiPos, "fi") recurse(x.Cond) recurse(x.CondLast) recurse(x.Then) recurse(x.ThenLast) if x.Else != nil { recurse(x.Else) } case *WhileClause: rsrv := "while" if x.Until { rsrv = "until" } setPos(&x.WhilePos, rsrv) setPos(&x.DoPos, "do") setPos(&x.DonePos, "done") recurse(x.Cond) recurse(x.CondLast) recurse(x.Do) recurse(x.DoLast) case *ForClause: if x.Select { setPos(&x.ForPos, "select") } else { setPos(&x.ForPos, "for") } if x.Braces { setPos(&x.DoPos, "{") setPos(&x.DonePos, "}") // Zero out Braces, to not duplicate all the test cases. // The printer ignores the field anyway. x.Braces = false } else { setPos(&x.DoPos, "do") setPos(&x.DonePos, "done") } recurse(x.Loop) recurse(x.Do) recurse(x.DoLast) case *WordIter: recurse(x.Name) if x.InPos.IsValid() { setPos(&x.InPos, "in") } recurse(x.Items) case *CStyleLoop: setPos(&x.Lparen, "((") setPos(&x.Rparen, "))") if x.Init != nil { recurse(x.Init) } if x.Cond != nil { recurse(x.Cond) } if x.Post != nil { recurse(x.Post) } case *SglQuoted: checkSrc(posAddCol(x.End(), -1), "'") valuePos := posAddCol(x.Left, 1) if x.Dollar { valuePos = posAddCol(valuePos, 1) } checkSrc(valuePos, x.Value) if x.Dollar { setPos(&x.Left, "$'") } else { setPos(&x.Left, "'") } setPos(&x.Right, "'") case *DblQuoted: checkSrc(posAddCol(x.End(), -1), `"`) if x.Dollar { setPos(&x.Left, `$"`) } else { setPos(&x.Left, `"`) } setPos(&x.Right, `"`) recurse(x.Parts) case *UnaryArithm: setPos(&x.OpPos, x.Op.String()) recurse(x.X) case *UnaryTest: strs := []string{x.Op.String()} switch x.Op { case TsExists: strs = append(strs, "-a") case TsSmbLink: strs = append(strs, "-h") } setPos(&x.OpPos, strs...) recurse(x.X) case *BinaryCmd: setPos(&x.OpPos, x.Op.String()) recurse(x.X) recurse(x.Y) case *BinaryArithm: setPos(&x.OpPos, x.Op.String()) recurse(x.X) recurse(x.Y) case *BinaryTest: strs := []string{x.Op.String()} switch x.Op { case TsMatch: strs = append(strs, "=") } setPos(&x.OpPos, strs...) recurse(x.X) recurse(x.Y) case *ParenArithm: setPos(&x.Lparen, "(") setPos(&x.Rparen, ")") recurse(x.X) case *ParenTest: setPos(&x.Lparen, "(") setPos(&x.Rparen, ")") recurse(x.X) case *FuncDecl: if x.RsrvWord { setPos(&x.Position, "function") } else { setPos(&x.Position) } recurse(x.Name) recurse(x.Body) case *ParamExp: doll := "$" if x.nakedIndex() { doll = "" } setPos(&x.Dollar, doll) if !x.Short { setPos(&x.Rbrace, "}") } else if x.nakedIndex() { checkSrc(posAddCol(x.End(), -1), "]") } recurse(x.Param) if x.Index != nil { recurse(x.Index) } if x.Slice != nil { if x.Slice.Offset != nil { recurse(x.Slice.Offset) } if x.Slice.Length != nil { recurse(x.Slice.Length) } } if x.Repl != nil { if x.Repl.Orig != nil { recurse(x.Repl.Orig) } if x.Repl.With != nil { recurse(x.Repl.With) } } if x.Exp != nil && x.Exp.Word != nil { recurse(x.Exp.Word) } case *ArithmExp: if x.Bracket { // deprecated $(( form setPos(&x.Left, "$[") setPos(&x.Right, "]") } else { setPos(&x.Left, "$((") setPos(&x.Right, "))") } recurse(x.X) case *ArithmCmd: setPos(&x.Left, "((") setPos(&x.Right, "))") recurse(x.X) case *CmdSubst: switch { case x.TempFile: setPos(&x.Left, "${ ", "${\t", "${\n") setPos(&x.Right, "}") case x.ReplyVar: setPos(&x.Left, "${|") setPos(&x.Right, "}") case x.Backquotes: setPos(&x.Left, "`", "\\`") setPos(&x.Right, "`", "\\`") // Zero out Backquotes, to not duplicate all the test // cases. The printer ignores the field anyway. x.Backquotes = false default: setPos(&x.Left, "$(") setPos(&x.Right, ")") } recurse(x.Stmts) recurse(x.Last) case *CaseClause: setPos(&x.Case, "case") if x.Braces { setPos(&x.In, "{") setPos(&x.Esac, "}") // Zero out Braces, to not duplicate all the test cases. // The printer ignores the field anyway. x.Braces = false } else { setPos(&x.In, "in") setPos(&x.Esac, "esac") } recurse(x.Word) for _, ci := range x.Items { recurse(ci) } case *CaseItem: if x.OpPos.IsValid() { setPos(&x.OpPos, x.Op.String(), "esac") } recurse(x.Patterns) recurse(x.Stmts) recurse(x.Last) case *TestClause: setPos(&x.Left, "[[") setPos(&x.Right, "]]") recurse(x.X) case *DeclClause: recurse(x.Variant) recurse(x.Args) case *TimeClause: setPos(&x.Time, "time") if x.Stmt != nil { recurse(x.Stmt) } case *CoprocClause: setPos(&x.Coproc, "coproc") if x.Name != nil { recurse(x.Name) } recurse(x.Stmt) case *LetClause: setPos(&x.Let, "let") for _, expr := range x.Exprs { recurse(expr) } case *TestDecl: setPos(&x.Position, "@test") recurse(x.Description) recurse(x.Body) case *ArrayExpr: setPos(&x.Lparen, "(") setPos(&x.Rparen, ")") for _, elem := range x.Elems { recurse(elem) } case *ArrayElem: if x.Index != nil { recurse(x.Index) } if x.Value != nil { recurse(x.Value) } case *ExtGlob: setPos(&x.OpPos, x.Op.String()) checkSrc(posAddCol(x.End(), -1), ")") recurse(x.Pattern) case *ProcSubst: setPos(&x.OpPos, x.Op.String()) setPos(&x.Rparen, ")") recurse(x.Stmts) recurse(x.Last) default: panic(reflect.TypeOf(v)) } } sh-3.4.3/syntax/fuzz_test.go000066400000000000000000000105221420425111700160420ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information //go:build go1.18 // +build go1.18 package syntax import ( "fmt" "io" "os/exec" "strings" "testing" qt "github.com/frankban/quicktest" ) func FuzzQuote(f *testing.F) { if _, err := exec.LookPath("bash"); err != nil { f.Skipf("requires bash to verify quoted strings") } // Keep in sync with ExampleQuote. f.Add("foo", uint8(LangBash)) f.Add("bar $baz", uint8(LangBash)) f.Add(`"won't"`, uint8(LangBash)) f.Add(`~/home`, uint8(LangBash)) f.Add("#1304", uint8(LangBash)) f.Add("name=value", uint8(LangBash)) f.Add(`glob-*`, uint8(LangBash)) f.Add("invalid-\xe2'", uint8(LangBash)) f.Add("nonprint-\x0b\x1b", uint8(LangBash)) f.Fuzz(func(t *testing.T, s string, langVariant uint8) { if langVariant > 3 { t.Skip() // lang variants are 0-3 } lang := LangVariant(langVariant) quoted, err := Quote(s, lang) if err != nil { // Cannot be quoted; not interesting. t.Skip() } var shellProgram string switch lang { case LangBash: hasBash51(t) shellProgram = "bash" case LangPOSIX: hasDash059(t) shellProgram = "dash" case LangMirBSDKorn: hasMksh59(t) shellProgram = "mksh" case LangBats: t.Skip() // bats has no shell and its syntax is just bash default: panic(fmt.Sprintf("unknown lang variant: %d", lang)) } f, err := NewParser(Variant(lang)).Parse(strings.NewReader(quoted), "") if err != nil { t.Fatalf("parse error on %q quoted as %s: %v", s, quoted, err) } qt.Assert(t, len(f.Stmts), qt.Equals, 1, qt.Commentf("in: %q, quoted: %s", s, quoted)) call, ok := f.Stmts[0].Cmd.(*CallExpr) qt.Assert(t, ok, qt.IsTrue, qt.Commentf("in: %q, quoted: %s", s, quoted)) qt.Assert(t, len(call.Args), qt.Equals, 1, qt.Commentf("in: %q, quoted: %s", s, quoted)) // Beware that this might run arbitrary code // if Quote is too naive and allows ';' or '$'. // // Also note that this fuzzing would not catch '=', // as we don't use the quoted string as a first argument // to avoid running random commands. // // We could consider ways to fully sandbox the bash process, // but for now that feels overkill. out, err := exec.Command(shellProgram, "-c", "printf %s "+quoted).CombinedOutput() if err != nil { t.Fatalf("%s error on %q quoted as %s: %v: %s", shellProgram, s, quoted, err, out) } want, got := s, string(out) if want != got { t.Fatalf("%s output mismatch on %q quoted as %s: got %q (len=%d)", shellProgram, want, quoted, got, len(got)) } }) } func FuzzParsePrint(f *testing.F) { add := func(src string, variant LangVariant) { // For now, default to just KeepComments. f.Add(src, uint8(variant), true, false, uint8(0), false, false, false, false, false, false, false) } for _, test := range shellTests { add(test.in, LangBash) } for _, test := range printTests { add(test.in, LangBash) } for _, test := range fileTests { for _, in := range test.Strs { if test.Bash != nil { add(in, LangBash) } if test.Posix != nil { add(in, LangPOSIX) } if test.MirBSDKorn != nil { add(in, LangMirBSDKorn) } if test.Bats != nil { add(in, LangBats) } } } f.Fuzz(func(t *testing.T, src string, // parser options // TODO: also fuzz StopAt langVariant uint8, // 0-3 keepComments bool, simplify bool, // printer options indent uint8, // 0-255 binaryNextLine bool, switchCaseIndent bool, spaceRedirects bool, keepPadding bool, minify bool, singleLine bool, functionNextLine bool, ) { if langVariant > 3 { t.Skip() // lang variants are 0-3 } if indent > 16 { t.Skip() // more indentation won't really be interesting } parser := NewParser() Variant(LangVariant(langVariant))(parser) KeepComments(keepComments)(parser) prog, err := parser.Parse(strings.NewReader(src), "") if err != nil { t.Skip() // not valid shell syntax } if simplify { Simplify(prog) } printer := NewPrinter() Indent(uint(indent))(printer) BinaryNextLine(binaryNextLine)(printer) SwitchCaseIndent(switchCaseIndent)(printer) SpaceRedirects(spaceRedirects)(printer) KeepPadding(keepPadding)(printer) Minify(minify)(printer) SingleLine(singleLine)(printer) FunctionNextLine(functionNextLine)(printer) if err := printer.Print(io.Discard, prog); err != nil { t.Skip() // e.g. invalid option } }) } sh-3.4.3/syntax/lexer.go000066400000000000000000000475141420425111700151370ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "bytes" "io" "unicode/utf8" ) // bytes that form or start a token func regOps(r rune) bool { switch r { case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`': return true } return false } // tokenize these inside parameter expansions func paramOps(r rune) bool { switch r { case '}', '#', '!', ':', '-', '+', '=', '?', '%', '[', ']', '/', '^', ',', '@', '*': return true } return false } // these start a parameter expansion name func paramNameOp(r rune) bool { switch r { case '}', ':', '+', '=', '%', '[', ']', '/', '^', ',': return false } return true } // tokenize these inside arithmetic expansions func arithmOps(r rune) bool { switch r { case '+', '-', '!', '~', '*', '/', '%', '(', ')', '^', '<', '>', ':', '=', ',', '?', '|', '&', '[', ']', '#': return true } return false } func bquoteEscaped(b byte) bool { switch b { case '$', '`', '\\': return true } return false } const escNewl rune = utf8.RuneSelf + 1 func (p *Parser) rune() rune { if p.r == '\n' || p.r == escNewl { // p.r instead of b so that newline // character positions don't have col 0. if p.line++; p.line > lineMax { p.lineOverflow = true } p.col = 0 p.colOverflow = false } if p.col += p.w; p.col > colMax { p.colOverflow = true } bquotes := 0 retry: if p.bsp < len(p.bs) { if b := p.bs[p.bsp]; b < utf8.RuneSelf { p.bsp++ if b == '\x00' { // Ignore null bytes while parsing, like bash. goto retry } if b == '\\' { if p.r != '\\' && p.peekByte('\n') { p.bsp++ p.w, p.r = 1, escNewl return escNewl } if p.openBquotes > 0 && bquotes < p.openBquotes && p.bsp < len(p.bs) && bquoteEscaped(p.bs[p.bsp]) { bquotes++ goto retry } } if b == '`' { p.lastBquoteEsc = bquotes } if p.litBs != nil { p.litBs = append(p.litBs, b) } p.w, p.r = 1, rune(b) return p.r } if !utf8.FullRune(p.bs[p.bsp:]) { // we need more bytes to read a full non-ascii rune p.fill() } var w int p.r, w = utf8.DecodeRune(p.bs[p.bsp:]) if p.litBs != nil { p.litBs = append(p.litBs, p.bs[p.bsp:p.bsp+w]...) } p.bsp += w if p.r == utf8.RuneError && w == 1 { p.posErr(p.nextPos(), "invalid UTF-8 encoding") } p.w = w } else { if p.r == utf8.RuneSelf { } else if p.fill(); p.bs == nil { p.bsp++ p.r = utf8.RuneSelf p.w = 1 } else { goto retry } } return p.r } // fill reads more bytes from the input src into readBuf. Any bytes that // had not yet been used at the end of the buffer are slid into the // beginning of the buffer. func (p *Parser) fill() { p.offs += p.bsp left := len(p.bs) - p.bsp copy(p.readBuf[:left], p.readBuf[p.bsp:]) readAgain: n, err := 0, p.readErr if err == nil { n, err = p.src.Read(p.readBuf[left:]) p.readErr = err } if n == 0 { if err == nil { goto readAgain } // don't use p.errPass as we don't want to overwrite p.tok if err != io.EOF { p.err = err } if left > 0 { p.bs = p.readBuf[:left] } else { p.bs = nil } } else { p.bs = p.readBuf[:left+n] } p.bsp = 0 } func (p *Parser) nextKeepSpaces() { r := p.r if p.quote != hdocBody && p.quote != hdocBodyTabs { // Heredocs handle escaped newlines in a special way, but others // do not. for r == escNewl { r = p.rune() } } p.pos = p.nextPos() switch p.quote { case paramExpRepl: switch r { case '}', '/': p.tok = p.paramToken(r) case '`', '"', '$', '\'': p.tok = p.regToken(r) default: p.advanceLitOther(r) } case dblQuotes: switch r { case '`', '"', '$': p.tok = p.dqToken(r) default: p.advanceLitDquote(r) } case hdocBody, hdocBodyTabs: switch r { case '`', '$': p.tok = p.dqToken(r) default: p.advanceLitHdoc(r) } default: // paramExpExp: switch r { case '}': p.tok = p.paramToken(r) case '`', '"', '$', '\'': p.tok = p.regToken(r) default: p.advanceLitOther(r) } } if p.err != nil && p.tok != _EOF { p.tok = _EOF } } func (p *Parser) next() { if p.r == utf8.RuneSelf { p.tok = _EOF return } p.spaced = false if p.quote&allKeepSpaces != 0 { p.nextKeepSpaces() return } r := p.r for r == escNewl { r = p.rune() } skipSpace: for { switch r { case utf8.RuneSelf: p.tok = _EOF return case escNewl: r = p.rune() case ' ', '\t', '\r': p.spaced = true r = p.rune() case '\n': if p.tok == _Newl { // merge consecutive newline tokens r = p.rune() continue } p.spaced = true p.tok = _Newl if p.quote != hdocWord && len(p.heredocs) > p.buriedHdocs { p.doHeredocs() } return default: break skipSpace } } if p.stopAt != nil && (p.spaced || p.tok == illegalTok || p.stopToken()) { w := utf8.RuneLen(r) if bytes.HasPrefix(p.bs[p.bsp-w:], p.stopAt) { p.r = utf8.RuneSelf p.w = 1 p.tok = _EOF return } } p.pos = p.nextPos() switch { case p.quote&allRegTokens != 0: switch r { case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`': p.tok = p.regToken(r) case '#': r = p.rune() p.newLit(r) for r != '\n' && r != utf8.RuneSelf { if r == escNewl { p.litBs = append(p.litBs, '\\', '\n') break } r = p.rune() } if p.keepComments { *p.curComs = append(*p.curComs, Comment{ Hash: p.pos, Text: p.endLit(), }) } else { p.litBs = nil } p.next() case '[', '=': if p.quote == arrayElems { p.tok = p.paramToken(r) } else { p.advanceLitNone(r) } case '?', '*', '+', '@', '!': if p.peekByte('(') { switch r { case '?': p.tok = globQuest case '*': p.tok = globStar case '+': p.tok = globPlus case '@': p.tok = globAt default: // '!' p.tok = globExcl } p.rune() p.rune() } else { p.advanceLitNone(r) } default: p.advanceLitNone(r) } case p.quote&allArithmExpr != 0 && arithmOps(r): p.tok = p.arithmToken(r) case p.quote&allParamExp != 0 && paramOps(r): p.tok = p.paramToken(r) case p.quote == testExprRegexp: if !p.rxFirstPart && p.spaced { p.quote = noState goto skipSpace } p.rxFirstPart = false switch r { case ';', '"', '\'', '$', '&', '>', '<', '`': p.tok = p.regToken(r) case ')': if p.rxOpenParens > 0 { // continuation of open paren p.advanceLitRe(r) } else { p.tok = rightParen p.quote = noState p.rune() // we are tokenizing manually } default: // including '(', '|' p.advanceLitRe(r) } case regOps(r): p.tok = p.regToken(r) default: p.advanceLitOther(r) } if p.err != nil && p.tok != _EOF { p.tok = _EOF } } func (p *Parser) peekByte(b byte) bool { if p.bsp == len(p.bs) { p.fill() } return p.bsp < len(p.bs) && p.bs[p.bsp] == b } func (p *Parser) regToken(r rune) token { switch r { case '\'': if p.openBquotes > 0 { // bury openBquotes p.buriedBquotes = p.openBquotes p.openBquotes = 0 } p.rune() return sglQuote case '"': p.rune() return dblQuote case '`': // Don't call p.rune, as we need to work out p.openBquotes to // properly handle backslashes in the lexer. return bckQuote case '&': switch p.rune() { case '&': p.rune() return andAnd case '>': if p.lang == LangPOSIX { break } if p.rune() == '>' { p.rune() return appAll } return rdrAll } return and case '|': switch p.rune() { case '|': p.rune() return orOr case '&': if p.lang == LangPOSIX { break } p.rune() return orAnd } return or case '$': switch p.rune() { case '\'': if p.lang == LangPOSIX { break } p.rune() return dollSglQuote case '"': if p.lang == LangPOSIX { break } p.rune() return dollDblQuote case '{': p.rune() return dollBrace case '[': if !p.lang.isBash() || p.quote == paramExpName { // latter to not tokenise ${$[@]} as $[ break } p.rune() return dollBrack case '(': if p.rune() == '(' { p.rune() return dollDblParen } return dollParen } return dollar case '(': if p.rune() == '(' && p.lang != LangPOSIX && p.quote != testExpr { p.rune() return dblLeftParen } return leftParen case ')': p.rune() return rightParen case ';': switch p.rune() { case ';': if p.rune() == '&' && p.lang.isBash() { p.rune() return dblSemiAnd } return dblSemicolon case '&': if p.lang == LangPOSIX { break } p.rune() return semiAnd case '|': if p.lang != LangMirBSDKorn { break } p.rune() return semiOr } return semicolon case '<': switch p.rune() { case '<': if r = p.rune(); r == '-' { p.rune() return dashHdoc } else if r == '<' && p.lang != LangPOSIX { p.rune() return wordHdoc } return hdoc case '>': p.rune() return rdrInOut case '&': p.rune() return dplIn case '(': if !p.lang.isBash() { break } p.rune() return cmdIn } return rdrIn default: // '>' switch p.rune() { case '>': p.rune() return appOut case '&': p.rune() return dplOut case '|': p.rune() return clbOut case '(': if !p.lang.isBash() { break } p.rune() return cmdOut } return rdrOut } } func (p *Parser) dqToken(r rune) token { switch r { case '"': p.rune() return dblQuote case '`': // Don't call p.rune, as we need to work out p.openBquotes to // properly handle backslashes in the lexer. return bckQuote default: // '$' switch p.rune() { case '{': p.rune() return dollBrace case '[': if !p.lang.isBash() { break } p.rune() return dollBrack case '(': if p.rune() == '(' { p.rune() return dollDblParen } return dollParen } return dollar } } func (p *Parser) paramToken(r rune) token { switch r { case '}': p.rune() return rightBrace case ':': switch p.rune() { case '+': p.rune() return colPlus case '-': p.rune() return colMinus case '?': p.rune() return colQuest case '=': p.rune() return colAssgn } return colon case '+': p.rune() return plus case '-': p.rune() return minus case '?': p.rune() return quest case '=': p.rune() return assgn case '%': if p.rune() == '%' { p.rune() return dblPerc } return perc case '#': if p.rune() == '#' { p.rune() return dblHash } return hash case '!': p.rune() return exclMark case '[': p.rune() return leftBrack case ']': p.rune() return rightBrack case '/': if p.rune() == '/' && p.quote != paramExpRepl { p.rune() return dblSlash } return slash case '^': if p.rune() == '^' { p.rune() return dblCaret } return caret case ',': if p.rune() == ',' { p.rune() return dblComma } return comma case '@': p.rune() return at default: // '*' p.rune() return star } } func (p *Parser) arithmToken(r rune) token { switch r { case '!': if p.rune() == '=' { p.rune() return nequal } return exclMark case '=': if p.rune() == '=' { p.rune() return equal } return assgn case '~': p.rune() return tilde case '(': p.rune() return leftParen case ')': p.rune() return rightParen case '&': switch p.rune() { case '&': p.rune() return andAnd case '=': p.rune() return andAssgn } return and case '|': switch p.rune() { case '|': p.rune() return orOr case '=': p.rune() return orAssgn } return or case '<': switch p.rune() { case '<': if p.rune() == '=' { p.rune() return shlAssgn } return hdoc case '=': p.rune() return lequal } return rdrIn case '>': switch p.rune() { case '>': if p.rune() == '=' { p.rune() return shrAssgn } return appOut case '=': p.rune() return gequal } return rdrOut case '+': switch p.rune() { case '+': p.rune() return addAdd case '=': p.rune() return addAssgn } return plus case '-': switch p.rune() { case '-': p.rune() return subSub case '=': p.rune() return subAssgn } return minus case '%': if p.rune() == '=' { p.rune() return remAssgn } return perc case '*': switch p.rune() { case '*': p.rune() return power case '=': p.rune() return mulAssgn } return star case '/': if p.rune() == '=' { p.rune() return quoAssgn } return slash case '^': if p.rune() == '=' { p.rune() return xorAssgn } return caret case '[': p.rune() return leftBrack case ']': p.rune() return rightBrack case ',': p.rune() return comma case '?': p.rune() return quest case ':': p.rune() return colon default: // '#' p.rune() return hash } } func (p *Parser) newLit(r rune) { switch { case r < utf8.RuneSelf: p.litBs = p.litBuf[:1] p.litBs[0] = byte(r) case r > escNewl: w := utf8.RuneLen(r) p.litBs = append(p.litBuf[:0], p.bs[p.bsp-w:p.bsp]...) default: // don't let r == utf8.RuneSelf go to the second case as RuneLen // would return -1 p.litBs = p.litBuf[:0] } } func (p *Parser) endLit() (s string) { if p.r == utf8.RuneSelf || p.r == escNewl { s = string(p.litBs) } else { s = string(p.litBs[:len(p.litBs)-int(p.w)]) } p.litBs = nil return } func (p *Parser) isLitRedir() bool { lit := p.litBs[:len(p.litBs)-1] if lit[0] == '{' && lit[len(lit)-1] == '}' { return ValidName(string(lit[1 : len(lit)-1])) } for _, b := range lit { switch b { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': default: return false } } return true } func (p *Parser) advanceNameCont(r rune) { // we know that r is a letter or underscore loop: for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { switch { case 'a' <= r && r <= 'z': case 'A' <= r && r <= 'Z': case r == '_': case '0' <= r && r <= '9': case r == escNewl: default: break loop } } p.tok, p.val = _LitWord, p.endLit() } func (p *Parser) advanceLitOther(r rune) { tok := _LitWord loop: for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { switch r { case '\\': // escaped byte follows p.rune() case '\'', '"', '`', '$': tok = _Lit break loop case '}': if p.quote&allParamExp != 0 { break loop } case '/': if p.quote != paramExpExp { break loop } case ':', '=', '%', '^', ',', '?', '!', '~', '*': if p.quote&allArithmExpr != 0 || p.quote == paramExpName { break loop } case '[', ']': if p.lang != LangPOSIX && p.quote&allArithmExpr != 0 { break loop } fallthrough case '#', '@': if p.quote&allParamReg != 0 { break loop } case '+', '-', ' ', '\t', ';', '&', '>', '<', '|', '(', ')', '\n', '\r': if p.quote&allKeepSpaces == 0 { break loop } } } p.tok, p.val = tok, p.endLit() } func (p *Parser) advanceLitNone(r rune) { p.eqlOffs = -1 tok := _LitWord loop: for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { switch r { case ' ', '\t', '\n', '\r', '&', '|', ';', '(', ')': break loop case '\\': // escaped byte follows p.rune() case '>', '<': if p.peekByte('(') { tok = _Lit } else if p.isLitRedir() { tok = _LitRedir } break loop case '`': if p.quote != subCmdBckquo { tok = _Lit } break loop case '"', '\'', '$': tok = _Lit break loop case '?', '*', '+', '@', '!': if p.peekByte('(') { tok = _Lit break loop } case '=': if p.eqlOffs < 0 { p.eqlOffs = len(p.litBs) - 1 } case '[': if p.lang != LangPOSIX && len(p.litBs) > 1 && p.litBs[0] != '[' { tok = _Lit break loop } } } p.tok, p.val = tok, p.endLit() } func (p *Parser) advanceLitDquote(r rune) { tok := _LitWord loop: for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { switch r { case '"': break loop case '\\': // escaped byte follows p.rune() case escNewl, '`', '$': tok = _Lit break loop } } p.tok, p.val = tok, p.endLit() } func (p *Parser) advanceLitHdoc(r rune) { // Unlike the rest of nextKeepSpaces quote states, we handle escaped // newlines here. If lastTok==_Lit, then we know we're following an // escaped newline, so the first line can't end the heredoc. lastTok := p.tok for r == escNewl { r = p.rune() lastTok = _Lit } p.pos = p.nextPos() p.tok = _Lit p.newLit(r) if p.quote == hdocBodyTabs { for r == '\t' { r = p.rune() } } lStart := len(p.litBs) - 1 stop := p.hdocStops[len(p.hdocStops)-1] for ; ; r = p.rune() { switch r { case escNewl, '`', '$': p.val = p.endLit() return case '\\': // escaped byte follows p.rune() case '\n', utf8.RuneSelf: if p.parsingDoc { if r == utf8.RuneSelf { p.tok = _LitWord p.val = p.endLit() return } } else if lStart == 0 && lastTok == _Lit { // This line starts right after an escaped // newline, so it should never end the heredoc. } else if lStart >= 0 { // Compare the current line with the stop word. line := p.litBs[lStart:] if r == '\n' && len(line) > 0 { line = line[:len(line)-1] // minus \n } if bytes.Equal(line, stop) { p.tok = _LitWord p.val = p.endLit()[:lStart] if p.val == "" { p.tok = _Newl } p.hdocStops[len(p.hdocStops)-1] = nil return } } if r == utf8.RuneSelf { return } if p.quote == hdocBodyTabs { for p.peekByte('\t') { p.rune() } } lStart = len(p.litBs) } } } func (p *Parser) quotedHdocWord() *Word { r := p.r p.newLit(r) pos := p.nextPos() stop := p.hdocStops[len(p.hdocStops)-1] for ; ; r = p.rune() { if r == utf8.RuneSelf { return nil } if p.quote == hdocBodyTabs { for r == '\t' { r = p.rune() } } lStart := len(p.litBs) - 1 for r != utf8.RuneSelf && r != '\n' { if r == escNewl { p.litBs = append(p.litBs, '\\', '\n') break } r = p.rune() } if lStart < 0 { continue } // Compare the current line with the stop word. line := p.litBs[lStart:] if r == '\n' && len(line) > 0 { line = line[:len(line)-1] // minus \n } if bytes.Equal(line, stop) { p.hdocStops[len(p.hdocStops)-1] = nil val := p.endLit()[:lStart] if val == "" { return nil } return p.word(p.wps(p.lit(pos, val))) } } } func (p *Parser) advanceLitRe(r rune) { for p.newLit(r); ; r = p.rune() { switch r { case '\\': p.rune() case '(': p.rxOpenParens++ case ')': if p.rxOpenParens--; p.rxOpenParens < 0 { p.tok, p.val = _LitWord, p.endLit() p.quote = noState return } case ' ', '\t', '\r', '\n', ';', '&', '>', '<': if p.rxOpenParens <= 0 { p.tok, p.val = _LitWord, p.endLit() p.quote = noState return } case '"', '\'', '$', '`': p.tok, p.val = _Lit, p.endLit() return case utf8.RuneSelf: p.tok, p.val = _LitWord, p.endLit() p.quote = noState return } } } func testUnaryOp(val string) UnTestOperator { switch val { case "!": return TsNot case "-e", "-a": return TsExists case "-f": return TsRegFile case "-d": return TsDirect case "-c": return TsCharSp case "-b": return TsBlckSp case "-p": return TsNmPipe case "-S": return TsSocket case "-L", "-h": return TsSmbLink case "-k": return TsSticky case "-g": return TsGIDSet case "-u": return TsUIDSet case "-G": return TsGrpOwn case "-O": return TsUsrOwn case "-N": return TsModif case "-r": return TsRead case "-w": return TsWrite case "-x": return TsExec case "-s": return TsNoEmpty case "-t": return TsFdTerm case "-z": return TsEmpStr case "-n": return TsNempStr case "-o": return TsOptSet case "-v": return TsVarSet case "-R": return TsRefVar default: return 0 } } func testBinaryOp(val string) BinTestOperator { switch val { case "=": return TsMatchShort case "==": return TsMatch case "!=": return TsNoMatch case "=~": return TsReMatch case "-nt": return TsNewer case "-ot": return TsOlder case "-ef": return TsDevIno case "-eq": return TsEql case "-ne": return TsNeq case "-le": return TsLeq case "-ge": return TsGeq case "-lt": return TsLss case "-gt": return TsGtr default: return 0 } } sh-3.4.3/syntax/nodes.go000066400000000000000000000561621420425111700151270ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "strconv" "strings" ) // Node represents a syntax tree node. type Node interface { // Pos returns the position of the first character of the node. Comments // are ignored, except if the node is a *File. Pos() Pos // End returns the position of the character immediately after the node. // If the character is a newline, the line number won't cross into the // next line. Comments are ignored, except if the node is a *File. End() Pos } // File represents a shell source file. type File struct { Name string Stmts []*Stmt Last []Comment } func (f *File) Pos() Pos { return stmtsPos(f.Stmts, f.Last) } func (f *File) End() Pos { return stmtsEnd(f.Stmts, f.Last) } func stmtsPos(stmts []*Stmt, last []Comment) Pos { if len(stmts) > 0 { s := stmts[0] sPos := s.Pos() if len(s.Comments) > 0 { if cPos := s.Comments[0].Pos(); sPos.After(cPos) { return cPos } } return sPos } if len(last) > 0 { return last[0].Pos() } return Pos{} } func stmtsEnd(stmts []*Stmt, last []Comment) Pos { if len(last) > 0 { return last[len(last)-1].End() } if len(stmts) > 0 { s := stmts[len(stmts)-1] sEnd := s.End() if len(s.Comments) > 0 { if cEnd := s.Comments[0].End(); cEnd.After(sEnd) { return cEnd } } return sEnd } return Pos{} } // Pos is a position within a shell source file. type Pos struct { offs, lineCol uint32 } // We used to split line and column numbers evenly in 16 bits, but line numbers // are significantly more important in practice. Use more bits for them. const ( lineBitSize = 18 lineMax = (1 << lineBitSize) - 1 colBitSize = 32 - lineBitSize colMax = (1 << colBitSize) - 1 colBitMask = colMax ) // Offset returns the byte offset of the position in the original source file. // Byte offsets start at 0. func (p Pos) Offset() uint { return uint(p.offs) } // Line returns the line number of the position, starting at 1. // // Line is protected against overflows; if an input has too many lines, extra // lines will have a line number of 0, rendered as "?". func (p Pos) Line() uint { return uint(p.lineCol >> colBitSize) } // Col returns the column number of the position, starting at 1. It counts in // bytes. // // Col is protected against overflows; if an input line has too many columns, // extra columns will have a column number of 0, rendered as "?". func (p Pos) Col() uint { return uint(p.lineCol & colBitMask) } func (p Pos) String() string { var b strings.Builder if line := p.Line(); line > 0 { b.WriteString(strconv.FormatUint(uint64(line), 10)) } else { b.WriteByte('?') } b.WriteByte(':') if col := p.Col(); col > 0 { b.WriteString(strconv.FormatUint(uint64(col), 10)) } else { b.WriteByte('?') } return b.String() } // IsValid reports whether the position is valid. All positions in nodes // returned by Parse are valid. func (p Pos) IsValid() bool { return p != Pos{} } // After reports whether the position p is after p2. It is a more expressive // version of p.Offset() > p2.Offset(). func (p Pos) After(p2 Pos) bool { return p.offs > p2.offs } func posAddCol(p Pos, n int) Pos { // TODO: guard against overflows p.lineCol += uint32(n) p.offs += uint32(n) return p } func posMax(p1, p2 Pos) Pos { if p2.After(p1) { return p2 } return p1 } // Comment represents a single comment on a single line. type Comment struct { Hash Pos Text string } func (c *Comment) Pos() Pos { return c.Hash } func (c *Comment) End() Pos { return posAddCol(c.Hash, 1+len(c.Text)) } // Stmt represents a statement, also known as a "complete command". It is // compromised of a command and other components that may come before or after // it. type Stmt struct { Comments []Comment Cmd Command Position Pos Semicolon Pos // position of ';', '&', or '|&', if any Negated bool // ! stmt Background bool // stmt & Coprocess bool // mksh's |& Redirs []*Redirect // stmt >a 0 { end = posMax(end, s.Redirs[len(s.Redirs)-1].End()) } return end } // Command represents all nodes that are simple or compound commands, including // function declarations. // // These are *CallExpr, *IfClause, *WhileClause, *ForClause, *CaseClause, // *Block, *Subshell, *BinaryCmd, *FuncDecl, *ArithmCmd, *TestClause, // *DeclClause, *LetClause, *TimeClause, and *CoprocClause. type Command interface { Node commandNode() } func (*CallExpr) commandNode() {} func (*IfClause) commandNode() {} func (*WhileClause) commandNode() {} func (*ForClause) commandNode() {} func (*CaseClause) commandNode() {} func (*Block) commandNode() {} func (*Subshell) commandNode() {} func (*BinaryCmd) commandNode() {} func (*FuncDecl) commandNode() {} func (*ArithmCmd) commandNode() {} func (*TestClause) commandNode() {} func (*DeclClause) commandNode() {} func (*LetClause) commandNode() {} func (*TimeClause) commandNode() {} func (*CoprocClause) commandNode() {} func (*TestDecl) commandNode() {} // Assign represents an assignment to a variable. // // Here and elsewhere, Index can mean either an index expression into an indexed // array, or a string key into an associative array. // // If Index is non-nil, the value will be a word and not an array as nested // arrays are not allowed. // // If Naked is true and Name is nil, the assignment is part of a DeclClause and // the argument (in the Value field) will be evaluated at run-time. This // includes parameter expansions, which may expand to assignments or options. type Assign struct { Append bool // += Naked bool // without '=' Name *Lit // must be a valid name Index ArithmExpr // [i], ["k"] Value *Word // =val Array *ArrayExpr // =(arr) } func (a *Assign) Pos() Pos { if a.Name == nil { return a.Value.Pos() } return a.Name.Pos() } func (a *Assign) End() Pos { if a.Value != nil { return a.Value.End() } if a.Array != nil { return a.Array.End() } if a.Index != nil { return posAddCol(a.Index.End(), 2) } if a.Naked { return a.Name.End() } return posAddCol(a.Name.End(), 1) } // Redirect represents an input/output redirection. type Redirect struct { OpPos Pos Op RedirOperator N *Lit // fd>, or {varname}> in Bash Word *Word // >word Hdoc *Word // here-document body } func (r *Redirect) Pos() Pos { if r.N != nil { return r.N.Pos() } return r.OpPos } func (r *Redirect) End() Pos { if r.Hdoc != nil { return r.Hdoc.End() } return r.Word.End() } // CallExpr represents a command execution or function call, otherwise known as // a "simple command". // // If Args is empty, Assigns apply to the shell environment. Otherwise, they are // variables that cannot be arrays and which only apply to the call. type CallExpr struct { Assigns []*Assign // a=x b=y args Args []*Word } func (c *CallExpr) Pos() Pos { if len(c.Assigns) > 0 { return c.Assigns[0].Pos() } return c.Args[0].Pos() } func (c *CallExpr) End() Pos { if len(c.Args) == 0 { return c.Assigns[len(c.Assigns)-1].End() } return c.Args[len(c.Args)-1].End() } // Subshell represents a series of commands that should be executed in a nested // shell environment. type Subshell struct { Lparen, Rparen Pos Stmts []*Stmt Last []Comment } func (s *Subshell) Pos() Pos { return s.Lparen } func (s *Subshell) End() Pos { return posAddCol(s.Rparen, 1) } // Block represents a series of commands that should be executed in a nested // scope. It is essentially a list of statements within curly braces. type Block struct { Lbrace, Rbrace Pos Stmts []*Stmt Last []Comment } func (b *Block) Pos() Pos { return b.Lbrace } func (b *Block) End() Pos { return posAddCol(b.Rbrace, 1) } // IfClause represents an if statement. type IfClause struct { Position Pos // position of the starting "if", "elif", or "else" token ThenPos Pos // position of "then", empty if this is an "else" FiPos Pos // position of "fi", shared with .Else if non-nil Cond []*Stmt CondLast []Comment Then []*Stmt ThenLast []Comment Else *IfClause // if non-nil, an "elif" or an "else" Last []Comment // comments on the first "elif", "else", or "fi" } func (c *IfClause) Pos() Pos { return c.Position } func (c *IfClause) End() Pos { return posAddCol(c.FiPos, 2) } // WhileClause represents a while or an until clause. type WhileClause struct { WhilePos, DoPos, DonePos Pos Until bool Cond []*Stmt CondLast []Comment Do []*Stmt DoLast []Comment } func (w *WhileClause) Pos() Pos { return w.WhilePos } func (w *WhileClause) End() Pos { return posAddCol(w.DonePos, 4) } // ForClause represents a for or a select clause. The latter is only present in // Bash. type ForClause struct { ForPos, DoPos, DonePos Pos Select bool Braces bool // deprecated form with { } instead of do/done Loop Loop Do []*Stmt DoLast []Comment } func (f *ForClause) Pos() Pos { return f.ForPos } func (f *ForClause) End() Pos { return posAddCol(f.DonePos, 4) } // Loop holds either *WordIter or *CStyleLoop. type Loop interface { Node loopNode() } func (*WordIter) loopNode() {} func (*CStyleLoop) loopNode() {} // WordIter represents the iteration of a variable over a series of words in a // for clause. If InPos is an invalid position, the "in" token was missing, so // the iteration is over the shell's positional parameters. type WordIter struct { Name *Lit InPos Pos // position of "in" Items []*Word } func (w *WordIter) Pos() Pos { return w.Name.Pos() } func (w *WordIter) End() Pos { if len(w.Items) > 0 { return wordLastEnd(w.Items) } return posMax(w.Name.End(), posAddCol(w.InPos, 2)) } // CStyleLoop represents the behaviour of a for clause similar to the C // language. // // This node will only appear with LangBash. type CStyleLoop struct { Lparen, Rparen Pos // Init, Cond, Post can each be nil, if the for loop construct omits it. Init, Cond, Post ArithmExpr } func (c *CStyleLoop) Pos() Pos { return c.Lparen } func (c *CStyleLoop) End() Pos { return posAddCol(c.Rparen, 2) } // BinaryCmd represents a binary expression between two statements. type BinaryCmd struct { OpPos Pos Op BinCmdOperator X, Y *Stmt } func (b *BinaryCmd) Pos() Pos { return b.X.Pos() } func (b *BinaryCmd) End() Pos { return b.Y.End() } // FuncDecl represents the declaration of a function. type FuncDecl struct { Position Pos RsrvWord bool // non-posix "function f" style Parens bool // with () parentheses, only meaningful with RsrvWord=true Name *Lit Body *Stmt } func (f *FuncDecl) Pos() Pos { return f.Position } func (f *FuncDecl) End() Pos { return f.Body.End() } // Word represents a shell word, containing one or more word parts contiguous to // each other. The word is delimeted by word boundaries, such as spaces, // newlines, semicolons, or parentheses. type Word struct { Parts []WordPart } func (w *Word) Pos() Pos { return w.Parts[0].Pos() } func (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() } // Lit returns the word as a literal value, if the word consists of *Lit nodes // only. An empty string is returned otherwise. Words with multiple literals, // which can appear in some edge cases, are handled properly. // // For example, the word "foo" will return "foo", but the word "foo${bar}" will // return "". func (w *Word) Lit() string { // In the usual case, we'll have either a single part that's a literal, // or one of the parts being a non-literal. Using strings.Join instead // of a strings.Builder avoids extra work in these cases, since a single // part is a shortcut, and many parts don't incur string copies. lits := make([]string, 0, 1) for _, part := range w.Parts { lit, ok := part.(*Lit) if !ok { return "" } lits = append(lits, lit.Value) } return strings.Join(lits, "") } // WordPart represents all nodes that can form part of a word. // // These are *Lit, *SglQuoted, *DblQuoted, *ParamExp, *CmdSubst, *ArithmExp, // *ProcSubst, and *ExtGlob. type WordPart interface { Node wordPartNode() } func (*Lit) wordPartNode() {} func (*SglQuoted) wordPartNode() {} func (*DblQuoted) wordPartNode() {} func (*ParamExp) wordPartNode() {} func (*CmdSubst) wordPartNode() {} func (*ArithmExp) wordPartNode() {} func (*ProcSubst) wordPartNode() {} func (*ExtGlob) wordPartNode() {} func (*BraceExp) wordPartNode() {} // Lit represents a string literal. // // Note that a parsed string literal may not appear as-is in the original source // code, as it is possible to split literals by escaping newlines. The splitting // is lost, but the end position is not. type Lit struct { ValuePos, ValueEnd Pos Value string } func (l *Lit) Pos() Pos { return l.ValuePos } func (l *Lit) End() Pos { return l.ValueEnd } // SglQuoted represents a string within single quotes. type SglQuoted struct { Left, Right Pos Dollar bool // $'' Value string } func (q *SglQuoted) Pos() Pos { return q.Left } func (q *SglQuoted) End() Pos { return posAddCol(q.Right, 1) } // DblQuoted represents a list of nodes within double quotes. type DblQuoted struct { Left, Right Pos Dollar bool // $"" Parts []WordPart } func (q *DblQuoted) Pos() Pos { return q.Left } func (q *DblQuoted) End() Pos { return posAddCol(q.Right, 1) } // CmdSubst represents a command substitution. type CmdSubst struct { Left, Right Pos Stmts []*Stmt Last []Comment Backquotes bool // deprecated `foo` TempFile bool // mksh's ${ foo;} ReplyVar bool // mksh's ${|foo;} } func (c *CmdSubst) Pos() Pos { return c.Left } func (c *CmdSubst) End() Pos { return posAddCol(c.Right, 1) } // ParamExp represents a parameter expansion. type ParamExp struct { Dollar, Rbrace Pos Short bool // $a instead of ${a} Excl bool // ${!a} Length bool // ${#a} Width bool // ${%a} Param *Lit Index ArithmExpr // ${a[i]}, ${a["k"]} Slice *Slice // ${a:x:y} Repl *Replace // ${a/x/y} Names ParNamesOperator // ${!prefix*} or ${!prefix@} Exp *Expansion // ${a:-b}, ${a#b}, etc } func (p *ParamExp) Pos() Pos { return p.Dollar } func (p *ParamExp) End() Pos { if !p.Short { return posAddCol(p.Rbrace, 1) } if p.Index != nil { return posAddCol(p.Index.End(), 1) } return p.Param.End() } func (p *ParamExp) nakedIndex() bool { return p.Short && p.Index != nil } // Slice represents a character slicing expression inside a ParamExp. // // This node will only appear in LangBash and LangMirBSDKorn. type Slice struct { Offset, Length ArithmExpr } // Replace represents a search and replace expression inside a ParamExp. type Replace struct { All bool Orig, With *Word } // Expansion represents string manipulation in a ParamExp other than those // covered by Replace. type Expansion struct { Op ParExpOperator Word *Word } // ArithmExp represents an arithmetic expansion. type ArithmExp struct { Left, Right Pos Bracket bool // deprecated $[expr] form Unsigned bool // mksh's $((# expr)) X ArithmExpr } func (a *ArithmExp) Pos() Pos { return a.Left } func (a *ArithmExp) End() Pos { if a.Bracket { return posAddCol(a.Right, 1) } return posAddCol(a.Right, 2) } // ArithmCmd represents an arithmetic command. // // This node will only appear in LangBash and LangMirBSDKorn. type ArithmCmd struct { Left, Right Pos Unsigned bool // mksh's ((# expr)) X ArithmExpr } func (a *ArithmCmd) Pos() Pos { return a.Left } func (a *ArithmCmd) End() Pos { return posAddCol(a.Right, 2) } // ArithmExpr represents all nodes that form arithmetic expressions. // // These are *BinaryArithm, *UnaryArithm, *ParenArithm, and *Word. type ArithmExpr interface { Node arithmExprNode() } func (*BinaryArithm) arithmExprNode() {} func (*UnaryArithm) arithmExprNode() {} func (*ParenArithm) arithmExprNode() {} func (*Word) arithmExprNode() {} // BinaryArithm represents a binary arithmetic expression. // // If Op is any assign operator, X will be a word with a single *Lit whose value // is a valid name. // // Ternary operators like "a ? b : c" are fit into this structure. Thus, if // Op==TernQuest, Y will be a *BinaryArithm with Op==TernColon. Op can only be // TernColon in that scenario. type BinaryArithm struct { OpPos Pos Op BinAritOperator X, Y ArithmExpr } func (b *BinaryArithm) Pos() Pos { return b.X.Pos() } func (b *BinaryArithm) End() Pos { return b.Y.End() } // UnaryArithm represents an unary arithmetic expression. The unary opearator // may come before or after the sub-expression. // // If Op is Inc or Dec, X will be a word with a single *Lit whose value is a // valid name. type UnaryArithm struct { OpPos Pos Op UnAritOperator Post bool X ArithmExpr } func (u *UnaryArithm) Pos() Pos { if u.Post { return u.X.Pos() } return u.OpPos } func (u *UnaryArithm) End() Pos { if u.Post { return posAddCol(u.OpPos, 2) } return u.X.End() } // ParenArithm represents an arithmetic expression within parentheses. type ParenArithm struct { Lparen, Rparen Pos X ArithmExpr } func (p *ParenArithm) Pos() Pos { return p.Lparen } func (p *ParenArithm) End() Pos { return posAddCol(p.Rparen, 1) } // CaseClause represents a case (switch) clause. type CaseClause struct { Case, In, Esac Pos Braces bool // deprecated mksh form with braces instead of in/esac Word *Word Items []*CaseItem Last []Comment } func (c *CaseClause) Pos() Pos { return c.Case } func (c *CaseClause) End() Pos { return posAddCol(c.Esac, 4) } // CaseItem represents a pattern list (case) within a CaseClause. type CaseItem struct { Op CaseOperator OpPos Pos // unset if it was finished by "esac" Comments []Comment Patterns []*Word Stmts []*Stmt Last []Comment } func (c *CaseItem) Pos() Pos { return c.Patterns[0].Pos() } func (c *CaseItem) End() Pos { if c.OpPos.IsValid() { return posAddCol(c.OpPos, len(c.Op.String())) } return stmtsEnd(c.Stmts, c.Last) } // TestClause represents a Bash extended test clause. // // This node will only appear in LangBash and LangMirBSDKorn. type TestClause struct { Left, Right Pos X TestExpr } func (t *TestClause) Pos() Pos { return t.Left } func (t *TestClause) End() Pos { return posAddCol(t.Right, 2) } // TestExpr represents all nodes that form test expressions. // // These are *BinaryTest, *UnaryTest, *ParenTest, and *Word. type TestExpr interface { Node testExprNode() } func (*BinaryTest) testExprNode() {} func (*UnaryTest) testExprNode() {} func (*ParenTest) testExprNode() {} func (*Word) testExprNode() {} // BinaryTest represents a binary test expression. type BinaryTest struct { OpPos Pos Op BinTestOperator X, Y TestExpr } func (b *BinaryTest) Pos() Pos { return b.X.Pos() } func (b *BinaryTest) End() Pos { return b.Y.End() } // UnaryTest represents a unary test expression. The unary opearator may come // before or after the sub-expression. type UnaryTest struct { OpPos Pos Op UnTestOperator X TestExpr } func (u *UnaryTest) Pos() Pos { return u.OpPos } func (u *UnaryTest) End() Pos { return u.X.End() } // ParenTest represents a test expression within parentheses. type ParenTest struct { Lparen, Rparen Pos X TestExpr } func (p *ParenTest) Pos() Pos { return p.Lparen } func (p *ParenTest) End() Pos { return posAddCol(p.Rparen, 1) } // DeclClause represents a Bash declare clause. // // Args can contain a mix of regular and naked assignments. The naked // assignments can represent either options or variable names. // // This node will only appear with LangBash. type DeclClause struct { // Variant is one of "declare", "local", "export", "readonly", // "typeset", or "nameref". Variant *Lit Args []*Assign } func (d *DeclClause) Pos() Pos { return d.Variant.Pos() } func (d *DeclClause) End() Pos { if len(d.Args) > 0 { return d.Args[len(d.Args)-1].End() } return d.Variant.End() } // ArrayExpr represents a Bash array expression. // // This node will only appear with LangBash. type ArrayExpr struct { Lparen, Rparen Pos Elems []*ArrayElem Last []Comment } func (a *ArrayExpr) Pos() Pos { return a.Lparen } func (a *ArrayExpr) End() Pos { return posAddCol(a.Rparen, 1) } // ArrayElem represents a Bash array element. // // Index can be nil; for example, declare -a x=(value). // Value can be nil; for example, declare -A x=([index]=). // Finally, neither can be nil; for example, declare -A x=([index]=value) type ArrayElem struct { Index ArithmExpr Value *Word Comments []Comment } func (a *ArrayElem) Pos() Pos { if a.Index != nil { return a.Index.Pos() } return a.Value.Pos() } func (a *ArrayElem) End() Pos { if a.Value != nil { return a.Value.End() } return posAddCol(a.Index.Pos(), 1) } // ExtGlob represents a Bash extended globbing expression. Note that these are // parsed independently of whether shopt has been called or not. // // This node will only appear in LangBash and LangMirBSDKorn. type ExtGlob struct { OpPos Pos Op GlobOperator Pattern *Lit } func (e *ExtGlob) Pos() Pos { return e.OpPos } func (e *ExtGlob) End() Pos { return posAddCol(e.Pattern.End(), 1) } // ProcSubst represents a Bash process substitution. // // This node will only appear with LangBash. type ProcSubst struct { OpPos, Rparen Pos Op ProcOperator Stmts []*Stmt Last []Comment } func (s *ProcSubst) Pos() Pos { return s.OpPos } func (s *ProcSubst) End() Pos { return posAddCol(s.Rparen, 1) } // TimeClause represents a Bash time clause. PosixFormat corresponds to the -p // flag. // // This node will only appear in LangBash and LangMirBSDKorn. type TimeClause struct { Time Pos PosixFormat bool Stmt *Stmt } func (c *TimeClause) Pos() Pos { return c.Time } func (c *TimeClause) End() Pos { if c.Stmt == nil { return posAddCol(c.Time, 4) } return c.Stmt.End() } // CoprocClause represents a Bash coproc clause. // // This node will only appear with LangBash. type CoprocClause struct { Coproc Pos Name *Word Stmt *Stmt } func (c *CoprocClause) Pos() Pos { return c.Coproc } func (c *CoprocClause) End() Pos { return c.Stmt.End() } // LetClause represents a Bash let clause. // // This node will only appear in LangBash and LangMirBSDKorn. type LetClause struct { Let Pos Exprs []ArithmExpr } func (l *LetClause) Pos() Pos { return l.Let } func (l *LetClause) End() Pos { return l.Exprs[len(l.Exprs)-1].End() } // BraceExp represents a Bash brace expression, such as "{a,f}" or "{1..10}". // // This node will only appear as a result of SplitBraces. type BraceExp struct { Sequence bool // {x..y[..incr]} instead of {x,y[,...]} Elems []*Word } func (b *BraceExp) Pos() Pos { return posAddCol(b.Elems[0].Pos(), -1) } func (b *BraceExp) End() Pos { return posAddCol(wordLastEnd(b.Elems), 1) } // TestDecl represents the declaration of a Bats test function. type TestDecl struct { Position Pos Description *Word Body *Stmt } func (f *TestDecl) Pos() Pos { return f.Position } func (f *TestDecl) End() Pos { return f.Body.End() } func wordLastEnd(ws []*Word) Pos { if len(ws) == 0 { return Pos{} } return ws[len(ws)-1].End() } sh-3.4.3/syntax/nodes_test.go000066400000000000000000000032511420425111700161550ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "fmt" "strings" "testing" ) func TestPosition(t *testing.T) { t.Parallel() parserBash := NewParser(KeepComments(true)) parserPosix := NewParser(KeepComments(true), Variant(LangPOSIX)) parserMirBSD := NewParser(KeepComments(true), Variant(LangMirBSDKorn)) parserBats := NewParser(KeepComments(true), Variant(LangBats)) for i, c := range fileTests { for j, in := range c.Strs { t.Run(fmt.Sprintf("%03d-%d", i, j), func(t *testing.T) { parser := parserPosix if c.Bats != nil { parser = parserBats } else if c.Bash != nil { parser = parserBash } else if c.MirBSDKorn != nil { parser = parserMirBSD } prog, err := parser.Parse(strings.NewReader(in), "") if err != nil { t.Fatalf("Unexpected error in %q: %v", in, err) } v := &posWalker{ t: t, f: prog, lines: strings.Split(in, "\n"), } Walk(prog, v.Visit) }) } } } type posWalker struct { t *testing.T f *File lines []string } func (v *posWalker) Visit(n Node) bool { if n == nil { return true } p := n.Pos() if !p.IsValid() && len(v.f.Stmts) > 0 { v.t.Fatalf("Invalid Pos") } if c, ok := n.(*Comment); ok { if v.f.Pos().After(c.Pos()) { v.t.Fatalf("A Comment is before its File") } if c.End().After(v.f.End()) { v.t.Fatalf("A Comment is after its File") } } return true } func TestWeirdOperatorString(t *testing.T) { t.Parallel() op := RedirOperator(1000) want := "token(1000)" if got := op.String(); got != want { t.Fatalf("token.String() mismatch: want %s, got %s", want, got) } } sh-3.4.3/syntax/parser.go000066400000000000000000001627511420425111700153150ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "bytes" "fmt" "io" "strconv" "strings" "unicode/utf8" ) // ParserOption is a function which can be passed to NewParser // to alter its behaviour. To apply option to existing Parser // call it directly, for example KeepComments(true)(parser). type ParserOption func(*Parser) // KeepComments makes the parser parse comments and attach them to // nodes, as opposed to discarding them. func KeepComments(enabled bool) ParserOption { return func(p *Parser) { p.keepComments = enabled } } // LangVariant describes a shell language variant to use when tokenizing and // parsing shell code. The zero value is Bash. type LangVariant int const ( // LangBash corresponds to the GNU Bash language, as described in its // manual at https://www.gnu.org/software/bash/manual/bash.html. // // We currently follow Bash version 5.1. // // Its string representation is "bash". LangBash LangVariant = iota // LangPOSIX corresponds to the POSIX Shell language, as described at // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html. // // Its string representation is "posix" or "sh". LangPOSIX // LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as // mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm. // Note that it shares some features with Bash, due to the the shared // ancestry that is ksh. // // We currently follow mksh version 59. // // Its string representation is "mksh". LangMirBSDKorn // LangBats corresponds to the Bash Automated Testing System language, // as described at https://github.com/bats-core/bats-core. Note that // it's just a small extension of the Bash language. // // Its string representation is "bats". LangBats ) // Variant changes the shell language variant that the parser will // accept. // // The passed language variant must be one of the constant values defined in // this package. func Variant(l LangVariant) ParserOption { switch l { case LangBash, LangPOSIX, LangMirBSDKorn, LangBats: default: panic(fmt.Sprintf("unknown shell language variant: %d", l)) } return func(p *Parser) { p.lang = l } } func (l LangVariant) String() string { switch l { case LangBash: return "bash" case LangPOSIX: return "posix" case LangMirBSDKorn: return "mksh" case LangBats: return "bats" } return "unknown shell language variant" } func (l *LangVariant) Set(s string) error { switch s { case "bash": *l = LangBash case "posix", "sh": *l = LangPOSIX case "mksh": *l = LangMirBSDKorn case "bats": *l = LangBats default: return fmt.Errorf("unknown shell language variant: %q", s) } return nil } func (l LangVariant) isBash() bool { return l == LangBash || l == LangBats } // StopAt configures the lexer to stop at an arbitrary word, treating it // as if it were the end of the input. It can contain any characters // except whitespace, and cannot be over four bytes in size. // // This can be useful to embed shell code within another language, as // one can use a special word to mark the delimiters between the two. // // As a word, it will only apply when following whitespace or a // separating token. For example, StopAt("$$") will act on the inputs // "foo $$" and "foo;$$", but not on "foo '$$'". // // The match is done by prefix, so the example above will also act on // "foo $$bar". func StopAt(word string) ParserOption { if len(word) > 4 { panic("stop word can't be over four bytes in size") } if strings.ContainsAny(word, " \t\n\r") { panic("stop word can't contain whitespace characters") } return func(p *Parser) { p.stopAt = []byte(word) } } // NewParser allocates a new Parser and applies any number of options. func NewParser(options ...ParserOption) *Parser { p := &Parser{} for _, opt := range options { opt(p) } return p } // Parse reads and parses a shell program with an optional name. It // returns the parsed program if no issues were encountered. Otherwise, // an error is returned. Reads from r are buffered. // // Parse can be called more than once, but not concurrently. That is, a // Parser can be reused once it is done working. func (p *Parser) Parse(r io.Reader, name string) (*File, error) { p.reset() p.f = &File{Name: name} p.src = r p.rune() p.next() p.f.Stmts, p.f.Last = p.stmtList() if p.err == nil { // EOF immediately after heredoc word so no newline to // trigger it p.doHeredocs() } return p.f, p.err } // Stmts reads and parses statements one at a time, calling a function // each time one is parsed. If the function returns false, parsing is // stopped and the function is not called again. func (p *Parser) Stmts(r io.Reader, fn func(*Stmt) bool) error { p.reset() p.f = &File{} p.src = r p.rune() p.next() p.stmts(fn) if p.err == nil { // EOF immediately after heredoc word so no newline to // trigger it p.doHeredocs() } return p.err } type wrappedReader struct { *Parser io.Reader lastLine int accumulated []*Stmt fn func([]*Stmt) bool } func (w *wrappedReader) Read(p []byte) (n int, err error) { // If we lexed a newline for the first time, we just finished a line, so // we may need to give a callback for the edge cases below not covered // by Parser.Stmts. if (w.r == '\n' || w.r == escNewl) && w.line > w.lastLine { if w.Incomplete() { // Incomplete statement; call back to print "> ". if !w.fn(w.accumulated) { return 0, io.EOF } } else if len(w.accumulated) == 0 { // Nothing was parsed; call back to print another "$ ". if !w.fn(nil) { return 0, io.EOF } } w.lastLine = w.line } return w.Reader.Read(p) } // Interactive implements what is necessary to parse statements in an // interactive shell. The parser will call the given function under two // circumstances outlined below. // // If a line containing any number of statements is parsed, the function will be // called with said statements. // // If a line ending in an incomplete statement is parsed, the function will be // called with any fully parsed statents, and Parser.Incomplete will return // true. // // One can imagine a simple interactive shell implementation as follows: // // fmt.Fprintf(os.Stdout, "$ ") // parser.Interactive(os.Stdin, func(stmts []*syntax.Stmt) bool { // if parser.Incomplete() { // fmt.Fprintf(os.Stdout, "> ") // return true // } // run(stmts) // fmt.Fprintf(os.Stdout, "$ ") // return true // } // // If the callback function returns false, parsing is stopped and the function // is not called again. func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error { w := wrappedReader{Parser: p, Reader: r, fn: fn} return p.Stmts(&w, func(stmt *Stmt) bool { w.accumulated = append(w.accumulated, stmt) // We finished parsing a statement and we're at a newline token, // so we finished fully parsing a number of statements. Call // back to run the statements and print "$ ". if p.tok == _Newl { if !fn(w.accumulated) { return false } w.accumulated = w.accumulated[:0] // The callback above would already print "$ ", so we // don't want the subsequent wrappedReader.Read to cause // another "$ " print thinking that nothing was parsed. w.lastLine = w.line + 1 } return true }) } // Words reads and parses words one at a time, calling a function each time one // is parsed. If the function returns false, parsing is stopped and the function // is not called again. // // Newlines are skipped, meaning that multi-line input will work fine. If the // parser encounters a token that isn't a word, such as a semicolon, an error // will be returned. // // Note that the lexer doesn't currently tokenize spaces, so it may need to read // a non-space byte such as a newline or a letter before finishing the parsing // of a word. This will be fixed in the future. func (p *Parser) Words(r io.Reader, fn func(*Word) bool) error { p.reset() p.f = &File{} p.src = r p.rune() p.next() for { p.got(_Newl) w := p.getWord() if w == nil { if p.tok != _EOF { p.curErr("%s is not a valid word", p.tok) } return p.err } if !fn(w) { return nil } } } // Document parses a single here-document word. That is, it parses the input as // if they were lines following a < 0 || p.litBs != nil } const bufSize = 1 << 10 func (p *Parser) reset() { p.tok, p.val = illegalTok, "" p.eqlOffs = 0 p.bs, p.bsp = nil, 0 p.offs, p.line, p.col = 0, 1, 1 p.r, p.w = 0, 0 p.err, p.readErr = nil, nil p.quote, p.forbidNested = noState, false p.openStmts = 0 p.heredocs, p.buriedHdocs = p.heredocs[:0], 0 p.parsingDoc = false p.openBquotes, p.buriedBquotes = 0, 0 p.accComs, p.curComs = nil, &p.accComs } func (p *Parser) nextPos() Pos { var line, col uint32 if !p.lineOverflow { line = uint32(p.line) } if !p.colOverflow { col = uint32(p.col) } return Pos{ offs: uint32(p.offs + p.bsp - int(p.w)), lineCol: (line << colBitSize) | col, } } func (p *Parser) lit(pos Pos, val string) *Lit { if len(p.litBatch) == 0 { p.litBatch = make([]Lit, 128) } l := &p.litBatch[0] p.litBatch = p.litBatch[1:] l.ValuePos = pos l.ValueEnd = p.nextPos() l.Value = val return l } func (p *Parser) word(parts []WordPart) *Word { if len(p.wordBatch) == 0 { p.wordBatch = make([]Word, 64) } w := &p.wordBatch[0] p.wordBatch = p.wordBatch[1:] w.Parts = parts return w } func (p *Parser) wps(wp WordPart) []WordPart { if len(p.wpsBatch) == 0 { p.wpsBatch = make([]WordPart, 64) } wps := p.wpsBatch[:1:1] p.wpsBatch = p.wpsBatch[1:] wps[0] = wp return wps } func (p *Parser) stmt(pos Pos) *Stmt { if len(p.stmtBatch) == 0 { p.stmtBatch = make([]Stmt, 64) } s := &p.stmtBatch[0] p.stmtBatch = p.stmtBatch[1:] s.Position = pos return s } func (p *Parser) stList() []*Stmt { if len(p.stListBatch) == 0 { p.stListBatch = make([]*Stmt, 256) } stmts := p.stListBatch[:0:4] p.stListBatch = p.stListBatch[4:] return stmts } type callAlloc struct { ce CallExpr ws [4]*Word } func (p *Parser) call(w *Word) *CallExpr { if len(p.callBatch) == 0 { p.callBatch = make([]callAlloc, 32) } alloc := &p.callBatch[0] p.callBatch = p.callBatch[1:] ce := &alloc.ce ce.Args = alloc.ws[:1] ce.Args[0] = w return ce } //go:generate stringer -type=quoteState type quoteState uint32 const ( noState quoteState = 1 << iota subCmd subCmdBckquo dblQuotes hdocWord hdocBody hdocBodyTabs arithmExpr arithmExprLet arithmExprCmd arithmExprBrack testExpr testExprRegexp switchCase paramExpName paramExpSlice paramExpRepl paramExpExp arrayElems allKeepSpaces = paramExpRepl | dblQuotes | hdocBody | hdocBodyTabs | paramExpExp allRegTokens = noState | subCmd | subCmdBckquo | hdocWord | switchCase | arrayElems | testExpr allArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd | arithmExprBrack | paramExpSlice allParamReg = paramExpName | paramExpSlice allParamExp = allParamReg | paramExpRepl | paramExpExp | arithmExprBrack ) type saveState struct { quote quoteState buriedHdocs int } func (p *Parser) preNested(quote quoteState) (s saveState) { s.quote, s.buriedHdocs = p.quote, p.buriedHdocs p.buriedHdocs, p.quote = len(p.heredocs), quote return } func (p *Parser) postNested(s saveState) { p.quote, p.buriedHdocs = s.quote, s.buriedHdocs } func (p *Parser) unquotedWordBytes(w *Word) ([]byte, bool) { var buf bytes.Buffer didUnquote := false for _, wp := range w.Parts { if p.unquotedWordPart(&buf, wp, false) { didUnquote = true } } return buf.Bytes(), didUnquote } func (p *Parser) unquotedWordPart(buf *bytes.Buffer, wp WordPart, quotes bool) (quoted bool) { switch x := wp.(type) { case *Lit: for i := 0; i < len(x.Value); i++ { if b := x.Value[i]; b == '\\' && !quotes { if i++; i < len(x.Value) { buf.WriteByte(x.Value[i]) } quoted = true } else { buf.WriteByte(b) } } case *SglQuoted: buf.WriteString(x.Value) quoted = true case *DblQuoted: for _, wp2 := range x.Parts { p.unquotedWordPart(buf, wp2, true) } quoted = true } return } func (p *Parser) doHeredocs() { hdocs := p.heredocs[p.buriedHdocs:] if len(hdocs) == 0 { // Nothing do do; don't even issue a read. return } p.rune() // consume '\n', since we know p.tok == _Newl old := p.quote p.heredocs = p.heredocs[:p.buriedHdocs] for i, r := range hdocs { if p.err != nil { break } p.quote = hdocBody if r.Op == DashHdoc { p.quote = hdocBodyTabs } stop, quoted := p.unquotedWordBytes(r.Word) p.hdocStops = append(p.hdocStops, stop) if i > 0 && p.r == '\n' { p.rune() } lastLine := p.line if quoted { r.Hdoc = p.quotedHdocWord() } else { p.next() r.Hdoc = p.getWord() } if r.Hdoc != nil { lastLine = int(r.Hdoc.End().Line()) } if lastLine < p.line { // TODO: It seems like this triggers more often than it // should. Look into it. l := p.lit(p.nextPos(), "") if r.Hdoc == nil { r.Hdoc = p.word(p.wps(l)) } else { r.Hdoc.Parts = append(r.Hdoc.Parts, l) } } if stop := p.hdocStops[len(p.hdocStops)-1]; stop != nil { p.posErr(r.Pos(), "unclosed here-document '%s'", stop) } p.hdocStops = p.hdocStops[:len(p.hdocStops)-1] } p.quote = old } func (p *Parser) got(tok token) bool { if p.tok == tok { p.next() return true } return false } func (p *Parser) gotRsrv(val string) (Pos, bool) { pos := p.pos if p.tok == _LitWord && p.val == val { p.next() return pos, true } return pos, false } func readableStr(s string) string { // don't quote tokens like & or } if s != "" && s[0] >= 'a' && s[0] <= 'z' { return strconv.Quote(s) } return s } func (p *Parser) followErr(pos Pos, left, right string) { leftStr := readableStr(left) p.posErr(pos, "%s must be followed by %s", leftStr, right) } func (p *Parser) followErrExp(pos Pos, left string) { p.followErr(pos, left, "an expression") } func (p *Parser) follow(lpos Pos, left string, tok token) { if !p.got(tok) { p.followErr(lpos, left, tok.String()) } } func (p *Parser) followRsrv(lpos Pos, left, val string) Pos { pos, ok := p.gotRsrv(val) if !ok { p.followErr(lpos, left, fmt.Sprintf("%q", val)) } return pos } func (p *Parser) followStmts(left string, lpos Pos, stops ...string) ([]*Stmt, []Comment) { if p.got(semicolon) { return nil, nil } newLine := p.got(_Newl) stmts, last := p.stmtList(stops...) if len(stmts) < 1 && !newLine { p.followErr(lpos, left, "a statement list") } return stmts, last } func (p *Parser) followWordTok(tok token, pos Pos) *Word { w := p.getWord() if w == nil { p.followErr(pos, tok.String(), "a word") } return w } func (p *Parser) stmtEnd(n Node, start, end string) Pos { pos, ok := p.gotRsrv(end) if !ok { p.posErr(n.Pos(), "%s statement must end with %q", start, end) } return pos } func (p *Parser) quoteErr(lpos Pos, quote token) { p.posErr(lpos, "reached %s without closing quote %s", p.tok.String(), quote) } func (p *Parser) matchingErr(lpos Pos, left, right interface{}) { p.posErr(lpos, "reached %s without matching %s with %s", p.tok.String(), left, right) } func (p *Parser) matched(lpos Pos, left, right token) Pos { pos := p.pos if !p.got(right) { p.matchingErr(lpos, left, right) } return pos } func (p *Parser) errPass(err error) { if p.err == nil { p.err = err p.bsp = len(p.bs) + 1 p.r = utf8.RuneSelf p.w = 1 p.tok = _EOF } } // IsIncomplete reports whether a Parser error could have been avoided with // extra input bytes. For example, if an io.EOF was encountered while there was // an unclosed quote or parenthesis. func IsIncomplete(err error) bool { perr, ok := err.(ParseError) return ok && perr.Incomplete } // IsKeyword returns true if the given word is part of the language keywords. func IsKeyword(word string) bool { // This list has been copied from the bash 5.1 source code, file y.tab.c +4460 switch word { case "!", "[[", // only if COND_COMMAND is defined "]]", // only if COND_COMMAND is defined "case", "coproc", // only if COPROCESS_SUPPORT is defined "do", "done", "else", "esac", "fi", "for", "function", "if", "in", "select", // only if SELECT_COMMAND is defined "then", "time", // only if COMMAND_TIMING is defined "until", "while", "{", "}": return true } return false } // ParseError represents an error found when parsing a source file, from which // the parser cannot recover. type ParseError struct { Filename string Pos Text string Incomplete bool } func (e ParseError) Error() string { if e.Filename == "" { return fmt.Sprintf("%s: %s", e.Pos.String(), e.Text) } return fmt.Sprintf("%s:%s: %s", e.Filename, e.Pos.String(), e.Text) } // LangError is returned when the parser encounters code that is only valid in // other shell language variants. The error includes what feature is not present // in the current language variant, and what languages support it. type LangError struct { Filename string Pos Feature string Langs []LangVariant } func (e LangError) Error() string { var buf bytes.Buffer if e.Filename != "" { buf.WriteString(e.Filename + ":") } buf.WriteString(e.Pos.String() + ": ") buf.WriteString(e.Feature) if strings.HasSuffix(e.Feature, "s") { buf.WriteString(" are a ") } else { buf.WriteString(" is a ") } for i, lang := range e.Langs { if i > 0 { buf.WriteString("/") } buf.WriteString(lang.String()) } buf.WriteString(" feature") return buf.String() } func (p *Parser) posErr(pos Pos, format string, a ...interface{}) { p.errPass(ParseError{ Filename: p.f.Name, Pos: pos, Text: fmt.Sprintf(format, a...), Incomplete: p.tok == _EOF && p.Incomplete(), }) } func (p *Parser) curErr(format string, a ...interface{}) { p.posErr(p.pos, format, a...) } func (p *Parser) langErr(pos Pos, feature string, langs ...LangVariant) { p.errPass(LangError{ Filename: p.f.Name, Pos: pos, Feature: feature, Langs: langs, }) } func (p *Parser) stmts(fn func(*Stmt) bool, stops ...string) { gotEnd := true loop: for p.tok != _EOF { newLine := p.got(_Newl) switch p.tok { case _LitWord: for _, stop := range stops { if p.val == stop { break loop } } case rightParen: if p.quote == subCmd { break loop } case bckQuote: if p.backquoteEnd() { break loop } case dblSemicolon, semiAnd, dblSemiAnd, semiOr: if p.quote == switchCase { break loop } p.curErr("%s can only be used in a case clause", p.tok) } if !newLine && !gotEnd { p.curErr("statements must be separated by &, ; or a newline") } if p.tok == _EOF { break } p.openStmts++ s := p.getStmt(true, false, false) p.openStmts-- if s == nil { p.invalidStmtStart() break } gotEnd = s.Semicolon.IsValid() if !fn(s) { break } } } func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) { var stmts []*Stmt var last []Comment fn := func(s *Stmt) bool { if stmts == nil { stmts = p.stList() } stmts = append(stmts, s) return true } p.stmts(fn, stops...) split := len(p.accComs) if p.tok == _LitWord && (p.val == "elif" || p.val == "else" || p.val == "fi") { // Split the comments, so that any aligned with an opening token // get attached to it. For example: // // if foo; then // # inside the body // # document the else // else // fi // TODO(mvdan): look into deduplicating this with similar logic // in caseItems. for i := len(p.accComs) - 1; i >= 0; i-- { c := p.accComs[i] if c.Pos().Col() != p.pos.Col() { break } split = i } } last = p.accComs[:split] p.accComs = p.accComs[split:] return stmts, last } func (p *Parser) invalidStmtStart() { switch p.tok { case semicolon, and, or, andAnd, orOr: p.curErr("%s can only immediately follow a statement", p.tok) case rightParen: p.curErr("%s can only be used to close a subshell", p.tok) default: p.curErr("%s is not a valid start for a statement", p.tok) } } func (p *Parser) getWord() *Word { if parts := p.wordParts(); len(parts) > 0 && p.err == nil { return p.word(parts) } return nil } func (p *Parser) getLit() *Lit { switch p.tok { case _Lit, _LitWord, _LitRedir: l := p.lit(p.pos, p.val) p.next() return l } return nil } func (p *Parser) wordParts() (wps []WordPart) { for { n := p.wordPart() if n == nil { return } if wps == nil { wps = p.wps(n) } else { wps = append(wps, n) } if p.spaced { return } } } func (p *Parser) ensureNoNested() { if p.forbidNested { p.curErr("expansions not allowed in heredoc words") } } func (p *Parser) wordPart() WordPart { switch p.tok { case _Lit, _LitWord: l := p.lit(p.pos, p.val) p.next() return l case dollBrace: p.ensureNoNested() switch p.r { case '|': if p.lang != LangMirBSDKorn { p.curErr(`"${|stmts;}" is a mksh feature`) } fallthrough case ' ', '\t', '\n': if p.lang != LangMirBSDKorn { p.curErr(`"${ stmts;}" is a mksh feature`) } cs := &CmdSubst{ Left: p.pos, TempFile: p.r != '|', ReplyVar: p.r == '|', } old := p.preNested(subCmd) p.rune() // don't tokenize '|' p.next() cs.Stmts, cs.Last = p.stmtList("}") p.postNested(old) pos, ok := p.gotRsrv("}") if !ok { p.matchingErr(cs.Left, "${", "}") } cs.Right = pos return cs default: return p.paramExp() } case dollDblParen, dollBrack: p.ensureNoNested() left := p.tok ar := &ArithmExp{Left: p.pos, Bracket: left == dollBrack} var old saveState if ar.Bracket { old = p.preNested(arithmExprBrack) } else { old = p.preNested(arithmExpr) } p.next() if p.got(hash) { if p.lang != LangMirBSDKorn { p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn) } ar.Unsigned = true } ar.X = p.followArithm(left, ar.Left) if ar.Bracket { if p.tok != rightBrack { p.arithmMatchingErr(ar.Left, dollBrack, rightBrack) } p.postNested(old) ar.Right = p.pos p.next() } else { ar.Right = p.arithmEnd(dollDblParen, ar.Left, old) } return ar case dollParen: p.ensureNoNested() cs := &CmdSubst{Left: p.pos} old := p.preNested(subCmd) p.next() cs.Stmts, cs.Last = p.stmtList() p.postNested(old) cs.Right = p.matched(cs.Left, leftParen, rightParen) return cs case dollar: r := p.r switch { case singleRuneParam(r): p.tok, p.val = _LitWord, string(r) p.rune() case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z', '0' <= r && r <= '9', r == '_', r == '\\': p.advanceNameCont(r) default: l := p.lit(p.pos, "$") p.next() return l } p.ensureNoNested() pe := &ParamExp{Dollar: p.pos, Short: true} p.pos = posAddCol(p.pos, 1) pe.Param = p.getLit() if pe.Param != nil && pe.Param.Value == "" { l := p.lit(pe.Dollar, "$") // e.g. "$\\\"" within double quotes, so we must // keep the rest of the literal characters. l.ValueEnd = posAddCol(l.ValuePos, 1) return l } return pe case cmdIn, cmdOut: p.ensureNoNested() ps := &ProcSubst{Op: ProcOperator(p.tok), OpPos: p.pos} old := p.preNested(subCmd) p.next() ps.Stmts, ps.Last = p.stmtList() p.postNested(old) ps.Rparen = p.matched(ps.OpPos, token(ps.Op), rightParen) return ps case sglQuote, dollSglQuote: sq := &SglQuoted{Left: p.pos, Dollar: p.tok == dollSglQuote} r := p.r for p.newLit(r); ; r = p.rune() { switch r { case '\\': if sq.Dollar { p.rune() } case '\'': sq.Right = p.nextPos() sq.Value = p.endLit() // restore openBquotes p.openBquotes = p.buriedBquotes p.buriedBquotes = 0 p.rune() p.next() return sq case escNewl: p.litBs = append(p.litBs, '\\', '\n') case utf8.RuneSelf: p.tok = _EOF p.quoteErr(sq.Pos(), sglQuote) return nil } } case dblQuote, dollDblQuote: if p.quote == dblQuotes { // p.tok == dblQuote, as "foo$" puts $ in the lit return nil } return p.dblQuoted() case bckQuote: if p.backquoteEnd() { return nil } p.ensureNoNested() cs := &CmdSubst{Left: p.pos, Backquotes: true} old := p.preNested(subCmdBckquo) p.openBquotes++ // The lexer didn't call p.rune for us, so that it could have // the right p.openBquotes to properly handle backslashes. p.rune() p.next() cs.Stmts, cs.Last = p.stmtList() if p.tok == bckQuote && p.lastBquoteEsc < p.openBquotes-1 { // e.g. found ` before the nested backquote \` was closed. p.tok = _EOF p.quoteErr(cs.Pos(), bckQuote) } p.postNested(old) p.openBquotes-- cs.Right = p.pos // Like above, the lexer didn't call p.rune for us. p.rune() if !p.got(bckQuote) { p.quoteErr(cs.Pos(), bckQuote) } return cs case globQuest, globStar, globPlus, globAt, globExcl: if p.lang == LangPOSIX { p.langErr(p.pos, "extended globs", LangBash, LangMirBSDKorn) } eg := &ExtGlob{Op: GlobOperator(p.tok), OpPos: p.pos} lparens := 1 r := p.r globLoop: for p.newLit(r); ; r = p.rune() { switch r { case utf8.RuneSelf: break globLoop case '(': lparens++ case ')': if lparens--; lparens == 0 { break globLoop } } } eg.Pattern = p.lit(posAddCol(eg.OpPos, 2), p.endLit()) p.rune() p.next() if lparens != 0 { p.matchingErr(eg.OpPos, eg.Op, rightParen) } return eg default: return nil } } func (p *Parser) dblQuoted() *DblQuoted { q := &DblQuoted{Left: p.pos, Dollar: p.tok == dollDblQuote} old := p.quote p.quote = dblQuotes p.next() q.Parts = p.wordParts() p.quote = old q.Right = p.pos if !p.got(dblQuote) { p.quoteErr(q.Pos(), dblQuote) } return q } func singleRuneParam(r rune) bool { switch r { case '@', '*', '#', '$', '?', '!', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': return true } return false } func (p *Parser) paramExp() *ParamExp { pe := &ParamExp{Dollar: p.pos} old := p.quote p.quote = paramExpName if p.r == '#' { p.tok = hash p.pos = p.nextPos() p.rune() } else { p.next() } switch p.tok { case hash: if paramNameOp(p.r) { pe.Length = true p.next() } case perc: if p.lang != LangMirBSDKorn { p.posErr(pe.Pos(), `"${%%foo}" is a mksh feature`) } if paramNameOp(p.r) { pe.Width = true p.next() } case exclMark: if paramNameOp(p.r) { if p.lang == LangPOSIX { p.langErr(p.pos, "${!foo}", LangBash, LangMirBSDKorn) } pe.Excl = true p.next() } } op := p.tok switch p.tok { case _Lit, _LitWord: if !numberLiteral(p.val) && !ValidName(p.val) { p.curErr("invalid parameter name") } pe.Param = p.lit(p.pos, p.val) p.next() case quest, minus: if pe.Length && p.r != '}' { // actually ${#-default}, not ${#-}; fix the ambiguity pe.Length = false pe.Param = p.lit(posAddCol(p.pos, -1), "#") pe.Param.ValueEnd = p.pos break } fallthrough case at, star, hash, exclMark, dollar: pe.Param = p.lit(p.pos, p.tok.String()) p.next() default: p.curErr("parameter expansion requires a literal") } switch p.tok { case _Lit, _LitWord: p.curErr("%s cannot be followed by a word", op) case rightBrace: pe.Rbrace = p.pos p.quote = old p.next() return pe case leftBrack: if p.lang == LangPOSIX { p.langErr(p.pos, "arrays", LangBash, LangMirBSDKorn) } if !ValidName(pe.Param.Value) { p.curErr("cannot index a special parameter name") } pe.Index = p.eitherIndex() } if p.tok == rightBrace { pe.Rbrace = p.pos p.quote = old p.next() return pe } if p.tok != _EOF && (pe.Length || pe.Width) { p.curErr("cannot combine multiple parameter expansion operators") } switch p.tok { case slash, dblSlash: // pattern search and replace if p.lang == LangPOSIX { p.langErr(p.pos, "search and replace", LangBash, LangMirBSDKorn) } pe.Repl = &Replace{All: p.tok == dblSlash} p.quote = paramExpRepl p.next() pe.Repl.Orig = p.getWord() p.quote = paramExpExp if p.got(slash) { pe.Repl.With = p.getWord() } case colon: // slicing if p.lang == LangPOSIX { p.langErr(p.pos, "slicing", LangBash, LangMirBSDKorn) } pe.Slice = &Slice{} colonPos := p.pos p.quote = paramExpSlice if p.next(); p.tok != colon { pe.Slice.Offset = p.followArithm(colon, colonPos) } colonPos = p.pos if p.got(colon) { pe.Slice.Length = p.followArithm(colon, colonPos) } // Need to use a different matched style so arithm errors // get reported correctly p.quote = old pe.Rbrace = p.pos p.matchedArithm(pe.Dollar, dollBrace, rightBrace) return pe case caret, dblCaret, comma, dblComma: // upper/lower case if !p.lang.isBash() { p.langErr(p.pos, "this expansion operator", LangBash) } pe.Exp = p.paramExpExp() case at, star: switch { case p.tok == at && p.lang == LangPOSIX: p.langErr(p.pos, "this expansion operator", LangBash, LangMirBSDKorn) case p.tok == star && !pe.Excl: p.curErr("not a valid parameter expansion operator: %v", p.tok) case pe.Excl && p.r == '}': pe.Names = ParNamesOperator(p.tok) p.next() default: pe.Exp = p.paramExpExp() } case plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn, perc, dblPerc, hash, dblHash: pe.Exp = p.paramExpExp() case _EOF: default: p.curErr("not a valid parameter expansion operator: %v", p.tok) } p.quote = old pe.Rbrace = p.pos p.matched(pe.Dollar, dollBrace, rightBrace) return pe } func (p *Parser) paramExpExp() *Expansion { op := ParExpOperator(p.tok) p.quote = paramExpExp p.next() if op == OtherParamOps { switch p.tok { case _Lit, _LitWord: default: p.curErr("@ expansion operator requires a literal") } switch p.val { case "a", "u", "A", "E", "K", "L", "P", "U": if !p.lang.isBash() { p.langErr(p.pos, "this expansion operator", LangBash) } case "#": if p.lang != LangMirBSDKorn { p.langErr(p.pos, "this expansion operator", LangMirBSDKorn) } case "Q": default: p.curErr("invalid @ expansion operator") } } return &Expansion{Op: op, Word: p.getWord()} } func (p *Parser) eitherIndex() ArithmExpr { old := p.quote lpos := p.pos p.quote = arithmExprBrack p.next() if p.tok == star || p.tok == at { p.tok, p.val = _LitWord, p.tok.String() } expr := p.followArithm(leftBrack, lpos) p.quote = old p.matchedArithm(lpos, leftBrack, rightBrack) return expr } func (p *Parser) stopToken() bool { switch p.tok { case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, dblSemicolon, semiAnd, dblSemiAnd, semiOr, rightParen: return true case bckQuote: return p.backquoteEnd() } return false } func (p *Parser) backquoteEnd() bool { return p.quote == subCmdBckquo && p.lastBquoteEsc < p.openBquotes } // ValidName returns whether val is a valid name as per the POSIX spec. func ValidName(val string) bool { if val == "" { return false } for i, r := range val { switch { case 'a' <= r && r <= 'z': case 'A' <= r && r <= 'Z': case r == '_': case i > 0 && '0' <= r && r <= '9': default: return false } } return true } func numberLiteral(val string) bool { for _, r := range val { if '0' > r || r > '9' { return false } } return true } func (p *Parser) hasValidIdent() bool { if p.tok != _Lit && p.tok != _LitWord { return false } if end := p.eqlOffs; end > 0 { if p.val[end-1] == '+' && p.lang != LangPOSIX { end-- // a+=x } if ValidName(p.val[:end]) { return true } } else if !ValidName(p.val) { return false // *[i]=x } return p.r == '[' // a[i]=x } func (p *Parser) getAssign(needEqual bool) *Assign { as := &Assign{} if p.eqlOffs > 0 { // foo=bar nameEnd := p.eqlOffs if p.lang != LangPOSIX && p.val[p.eqlOffs-1] == '+' { // a+=b as.Append = true nameEnd-- } as.Name = p.lit(p.pos, p.val[:nameEnd]) // since we're not using the entire p.val as.Name.ValueEnd = posAddCol(as.Name.ValuePos, nameEnd) left := p.lit(posAddCol(p.pos, 1), p.val[p.eqlOffs+1:]) if left.Value != "" { left.ValuePos = posAddCol(left.ValuePos, p.eqlOffs) as.Value = p.word(p.wps(left)) } p.next() } else { // foo[x]=bar as.Name = p.lit(p.pos, p.val) // hasValidIdent already checks p.r is '[' p.rune() p.pos = posAddCol(p.pos, 1) as.Index = p.eitherIndex() if p.spaced || p.stopToken() { if needEqual { p.followErr(as.Pos(), "a[b]", "=") } else { as.Naked = true return as } } if len(p.val) > 0 && p.val[0] == '+' { as.Append = true p.val = p.val[1:] p.pos = posAddCol(p.pos, 1) } if len(p.val) < 1 || p.val[0] != '=' { if as.Append { p.followErr(as.Pos(), "a[b]+", "=") } else { p.followErr(as.Pos(), "a[b]", "=") } return nil } p.pos = posAddCol(p.pos, 1) p.val = p.val[1:] if p.val == "" { p.next() } } if p.spaced || p.stopToken() { return as } if as.Value == nil && p.tok == leftParen { if p.lang == LangPOSIX { p.langErr(p.pos, "arrays", LangBash, LangMirBSDKorn) } if as.Index != nil { p.curErr("arrays cannot be nested") } as.Array = &ArrayExpr{Lparen: p.pos} newQuote := p.quote if p.lang.isBash() { newQuote = arrayElems } old := p.preNested(newQuote) p.next() p.got(_Newl) for p.tok != _EOF && p.tok != rightParen { ae := &ArrayElem{} ae.Comments, p.accComs = p.accComs, nil if p.tok == leftBrack { left := p.pos ae.Index = p.eitherIndex() p.follow(left, `"[x]"`, assgn) } if ae.Value = p.getWord(); ae.Value == nil { switch p.tok { case leftParen: p.curErr("arrays cannot be nested") return nil case _Newl, rightParen, leftBrack: // TODO: support [index]=[ default: p.curErr("array element values must be words") return nil } } if len(p.accComs) > 0 { c := p.accComs[0] if c.Pos().Line() == ae.End().Line() { ae.Comments = append(ae.Comments, c) p.accComs = p.accComs[1:] } } as.Array.Elems = append(as.Array.Elems, ae) p.got(_Newl) } as.Array.Last, p.accComs = p.accComs, nil p.postNested(old) as.Array.Rparen = p.matched(as.Array.Lparen, leftParen, rightParen) } else if w := p.getWord(); w != nil { if as.Value == nil { as.Value = w } else { as.Value.Parts = append(as.Value.Parts, w.Parts...) } } return as } func (p *Parser) peekRedir() bool { switch p.tok { case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: return true } return false } func (p *Parser) doRedirect(s *Stmt) { var r *Redirect if s.Redirs == nil { var alloc struct { redirs [4]*Redirect redir Redirect } s.Redirs = alloc.redirs[:0] r = &alloc.redir s.Redirs = append(s.Redirs, r) } else { r = &Redirect{} s.Redirs = append(s.Redirs, r) } r.N = p.getLit() if !p.lang.isBash() && r.N != nil && r.N.Value[0] == '{' { p.langErr(r.N.Pos(), "{varname} redirects", LangBash) } r.Op, r.OpPos = RedirOperator(p.tok), p.pos p.next() switch r.Op { case Hdoc, DashHdoc: old := p.quote p.quote, p.forbidNested = hdocWord, true p.heredocs = append(p.heredocs, r) r.Word = p.followWordTok(token(r.Op), r.OpPos) p.quote, p.forbidNested = old, false if p.tok == _Newl { if len(p.accComs) > 0 { c := p.accComs[0] if c.Pos().Line() == s.End().Line() { s.Comments = append(s.Comments, c) p.accComs = p.accComs[1:] } } p.doHeredocs() } default: r.Word = p.followWordTok(token(r.Op), r.OpPos) } } func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { pos, ok := p.gotRsrv("!") s := p.stmt(pos) if ok { s.Negated = true if p.stopToken() { p.posErr(s.Pos(), `"!" cannot form a statement alone`) } if _, ok := p.gotRsrv("!"); ok { p.posErr(s.Pos(), `cannot negate a command multiple times`) } } if s = p.gotStmtPipe(s, false); s == nil || p.err != nil { return nil } // instead of using recursion, iterate manually for p.tok == andAnd || p.tok == orOr { if binCmd { // left associativity: in a list of BinaryCmds, the // right recursion should only read a single element return s } b := &BinaryCmd{ OpPos: p.pos, Op: BinCmdOperator(p.tok), X: s, } p.next() p.got(_Newl) b.Y = p.getStmt(false, true, false) if b.Y == nil || p.err != nil { p.followErr(b.OpPos, b.Op.String(), "a statement") return nil } s = p.stmt(s.Position) s.Cmd = b s.Comments, b.X.Comments = b.X.Comments, nil } if readEnd { switch p.tok { case semicolon: s.Semicolon = p.pos p.next() case and: s.Semicolon = p.pos p.next() s.Background = true case orAnd: s.Semicolon = p.pos p.next() s.Coprocess = true } } if len(p.accComs) > 0 && !binCmd && !fnBody { c := p.accComs[0] if c.Pos().Line() == s.End().Line() { s.Comments = append(s.Comments, c) p.accComs = p.accComs[1:] } } return s } func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { s.Comments, p.accComs = p.accComs, nil switch p.tok { case _LitWord: switch p.val { case "{": p.block(s) case "if": p.ifClause(s) case "while", "until": p.whileClause(s, p.val == "until") case "for": p.forClause(s) case "case": p.caseClause(s) case "}": p.curErr(`%q can only be used to close a block`, p.val) case "then": p.curErr(`%q can only be used in an if`, p.val) case "elif": p.curErr(`%q can only be used in an if`, p.val) case "fi": p.curErr(`%q can only be used to end an if`, p.val) case "do": p.curErr(`%q can only be used in a loop`, p.val) case "done": p.curErr(`%q can only be used to end a loop`, p.val) case "esac": p.curErr(`%q can only be used to end a case`, p.val) case "!": if !s.Negated { p.curErr(`"!" can only be used in full statements`) break } case "[[": if p.lang != LangPOSIX { p.testClause(s) } case "]]": if p.lang != LangPOSIX { p.curErr(`%q can only be used to close a test`, p.val) } case "let": if p.lang != LangPOSIX { p.letClause(s) } case "function": if p.lang != LangPOSIX { p.bashFuncDecl(s) } case "declare": if p.lang.isBash() { p.declClause(s) } case "local", "export", "readonly", "typeset", "nameref": if p.lang != LangPOSIX { p.declClause(s) } case "time": if p.lang != LangPOSIX { p.timeClause(s) } case "coproc": if p.lang.isBash() { p.coprocClause(s) } case "select": if p.lang != LangPOSIX { p.selectClause(s) } case "@test": if p.lang == LangBats { p.testDecl(s) } } if s.Cmd != nil { break } if p.hasValidIdent() { p.callExpr(s, nil, true) break } name := p.lit(p.pos, p.val) if p.next(); p.got(leftParen) { p.follow(name.ValuePos, "foo(", rightParen) if p.lang == LangPOSIX && !ValidName(name.Value) { p.posErr(name.Pos(), "invalid func name") } p.funcDecl(s, name, name.ValuePos, true) } else { p.callExpr(s, p.word(p.wps(name)), false) } case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: p.doRedirect(s) p.callExpr(s, nil, false) case bckQuote: if p.backquoteEnd() { return nil } fallthrough case _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, globQuest, globStar, globPlus, globAt, globExcl: if p.hasValidIdent() { p.callExpr(s, nil, true) break } w := p.word(p.wordParts()) if p.got(leftParen) { p.posErr(w.Pos(), "invalid func name") } p.callExpr(s, w, false) case leftParen: p.subshell(s) case dblLeftParen: p.arithmExpCmd(s) default: if len(s.Redirs) == 0 { return nil } } for p.peekRedir() { p.doRedirect(s) } // instead of using recursion, iterate manually for p.tok == or || p.tok == orAnd { if binCmd { // left associativity: in a list of BinaryCmds, the // right recursion should only read a single element return s } if p.tok == orAnd && p.lang == LangMirBSDKorn { // No need to check for LangPOSIX, as on that language // we parse |& as two tokens. break } b := &BinaryCmd{OpPos: p.pos, Op: BinCmdOperator(p.tok), X: s} p.next() p.got(_Newl) if b.Y = p.gotStmtPipe(p.stmt(p.pos), true); b.Y == nil || p.err != nil { p.followErr(b.OpPos, b.Op.String(), "a statement") break } s = p.stmt(s.Position) s.Cmd = b s.Comments, b.X.Comments = b.X.Comments, nil // in "! x | y", the bang applies to the entire pipeline s.Negated = b.X.Negated b.X.Negated = false } return s } func (p *Parser) subshell(s *Stmt) { sub := &Subshell{Lparen: p.pos} old := p.preNested(subCmd) p.next() sub.Stmts, sub.Last = p.stmtList() p.postNested(old) sub.Rparen = p.matched(sub.Lparen, leftParen, rightParen) s.Cmd = sub } func (p *Parser) arithmExpCmd(s *Stmt) { ar := &ArithmCmd{Left: p.pos} old := p.preNested(arithmExprCmd) p.next() if p.got(hash) { if p.lang != LangMirBSDKorn { p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn) } ar.Unsigned = true } ar.X = p.followArithm(dblLeftParen, ar.Left) ar.Right = p.arithmEnd(dblLeftParen, ar.Left, old) s.Cmd = ar } func (p *Parser) block(s *Stmt) { b := &Block{Lbrace: p.pos} p.next() b.Stmts, b.Last = p.stmtList("}") pos, ok := p.gotRsrv("}") b.Rbrace = pos if !ok { p.matchingErr(b.Lbrace, "{", "}") } s.Cmd = b } func (p *Parser) ifClause(s *Stmt) { rootIf := &IfClause{Position: p.pos} p.next() rootIf.Cond, rootIf.CondLast = p.followStmts("if", rootIf.Position, "then") rootIf.ThenPos = p.followRsrv(rootIf.Position, "if ", "then") rootIf.Then, rootIf.ThenLast = p.followStmts("then", rootIf.ThenPos, "fi", "elif", "else") curIf := rootIf for p.tok == _LitWord && p.val == "elif" { elf := &IfClause{Position: p.pos} curIf.Last = p.accComs p.accComs = nil p.next() elf.Cond, elf.CondLast = p.followStmts("elif", elf.Position, "then") elf.ThenPos = p.followRsrv(elf.Position, "elif ", "then") elf.Then, elf.ThenLast = p.followStmts("then", elf.ThenPos, "fi", "elif", "else") curIf.Else = elf curIf = elf } if elsePos, ok := p.gotRsrv("else"); ok { curIf.Last = p.accComs p.accComs = nil els := &IfClause{Position: elsePos} els.Then, els.ThenLast = p.followStmts("else", els.Position, "fi") curIf.Else = els curIf = els } curIf.Last = p.accComs p.accComs = nil rootIf.FiPos = p.stmtEnd(rootIf, "if", "fi") for els := rootIf.Else; els != nil; els = els.Else { // All the nested IfClauses share the same FiPos. els.FiPos = rootIf.FiPos } s.Cmd = rootIf } func (p *Parser) whileClause(s *Stmt, until bool) { wc := &WhileClause{WhilePos: p.pos, Until: until} rsrv := "while" rsrvCond := "while " if wc.Until { rsrv = "until" rsrvCond = "until " } p.next() wc.Cond, wc.CondLast = p.followStmts(rsrv, wc.WhilePos, "do") wc.DoPos = p.followRsrv(wc.WhilePos, rsrvCond, "do") wc.Do, wc.DoLast = p.followStmts("do", wc.DoPos, "done") wc.DonePos = p.stmtEnd(wc, rsrv, "done") s.Cmd = wc } func (p *Parser) forClause(s *Stmt) { fc := &ForClause{ForPos: p.pos} p.next() fc.Loop = p.loop(fc.ForPos) start, end := "do", "done" if pos, ok := p.gotRsrv("{"); ok { if p.lang == LangPOSIX { p.langErr(pos, "for loops with braces", LangBash, LangMirBSDKorn) } fc.DoPos = pos fc.Braces = true start, end = "{", "}" } else { fc.DoPos = p.followRsrv(fc.ForPos, "for foo [in words]", start) } s.Comments = append(s.Comments, p.accComs...) p.accComs = nil fc.Do, fc.DoLast = p.followStmts(start, fc.DoPos, end) fc.DonePos = p.stmtEnd(fc, "for", end) s.Cmd = fc } func (p *Parser) loop(fpos Pos) Loop { if !p.lang.isBash() { switch p.tok { case leftParen, dblLeftParen: p.langErr(p.pos, "c-style fors", LangBash) } } if p.tok == dblLeftParen { cl := &CStyleLoop{Lparen: p.pos} old := p.preNested(arithmExprCmd) p.next() cl.Init = p.arithmExpr(false) if !p.got(dblSemicolon) { p.follow(p.pos, "expr", semicolon) cl.Cond = p.arithmExpr(false) p.follow(p.pos, "expr", semicolon) } cl.Post = p.arithmExpr(false) cl.Rparen = p.arithmEnd(dblLeftParen, cl.Lparen, old) p.got(semicolon) p.got(_Newl) return cl } return p.wordIter("for", fpos) } func (p *Parser) wordIter(ftok string, fpos Pos) *WordIter { wi := &WordIter{} if wi.Name = p.getLit(); wi.Name == nil { p.followErr(fpos, ftok, "a literal") } if p.got(semicolon) { p.got(_Newl) return wi } p.got(_Newl) if pos, ok := p.gotRsrv("in"); ok { wi.InPos = pos for !p.stopToken() { if w := p.getWord(); w == nil { p.curErr("word list can only contain words") } else { wi.Items = append(wi.Items, w) } } p.got(semicolon) p.got(_Newl) } else if p.tok == _LitWord && p.val == "do" { } else { p.followErr(fpos, ftok+" foo", `"in", "do", ;, or a newline`) } return wi } func (p *Parser) selectClause(s *Stmt) { fc := &ForClause{ForPos: p.pos, Select: true} p.next() fc.Loop = p.wordIter("select", fc.ForPos) fc.DoPos = p.followRsrv(fc.ForPos, "select foo [in words]", "do") fc.Do, fc.DoLast = p.followStmts("do", fc.DoPos, "done") fc.DonePos = p.stmtEnd(fc, "select", "done") s.Cmd = fc } func (p *Parser) caseClause(s *Stmt) { cc := &CaseClause{Case: p.pos} p.next() cc.Word = p.getWord() if cc.Word == nil { p.followErr(cc.Case, "case", "a word") } end := "esac" p.got(_Newl) if pos, ok := p.gotRsrv("{"); ok { cc.In = pos cc.Braces = true if p.lang != LangMirBSDKorn { p.posErr(cc.Pos(), `"case i {" is a mksh feature`) } end = "}" } else { cc.In = p.followRsrv(cc.Case, "case x", "in") } cc.Items = p.caseItems(end) cc.Last, p.accComs = p.accComs, nil cc.Esac = p.stmtEnd(cc, "case", end) s.Cmd = cc } func (p *Parser) caseItems(stop string) (items []*CaseItem) { p.got(_Newl) for p.tok != _EOF && !(p.tok == _LitWord && p.val == stop) { ci := &CaseItem{} ci.Comments, p.accComs = p.accComs, nil p.got(leftParen) for p.tok != _EOF { if w := p.getWord(); w == nil { p.curErr("case patterns must consist of words") } else { ci.Patterns = append(ci.Patterns, w) } if p.tok == rightParen { break } if !p.got(or) { p.curErr("case patterns must be separated with |") } } old := p.preNested(switchCase) p.next() ci.Stmts, ci.Last = p.stmtList(stop) p.postNested(old) switch p.tok { case dblSemicolon, semiAnd, dblSemiAnd, semiOr: default: ci.Op = Break items = append(items, ci) return } ci.Last = append(ci.Last, p.accComs...) p.accComs = nil ci.OpPos = p.pos ci.Op = CaseOperator(p.tok) p.next() p.got(_Newl) split := len(p.accComs) if p.tok == _LitWord && p.val != stop { for i := len(p.accComs) - 1; i >= 0; i-- { c := p.accComs[i] if c.Pos().Col() != p.pos.Col() { break } split = i } } ci.Comments = append(ci.Comments, p.accComs[:split]...) p.accComs = p.accComs[split:] items = append(items, ci) } return } func (p *Parser) testClause(s *Stmt) { tc := &TestClause{Left: p.pos} old := p.preNested(testExpr) p.next() if _, ok := p.gotRsrv("]]"); ok || p.tok == _EOF { p.posErr(tc.Left, "test clause requires at least one expression") } tc.X = p.testExpr(dblLeftBrack, tc.Left, false) if tc.X == nil { p.followErrExp(tc.Left, "[[") } tc.Right = p.pos if _, ok := p.gotRsrv("]]"); !ok { p.matchingErr(tc.Left, "[[", "]]") } p.postNested(old) s.Cmd = tc } func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { p.got(_Newl) var left TestExpr if pastAndOr { left = p.testExprBase() } else { left = p.testExpr(ftok, fpos, true) } if left == nil { return left } p.got(_Newl) switch p.tok { case andAnd, orOr: case _LitWord: if p.val == "]]" { return left } if p.tok = token(testBinaryOp(p.val)); p.tok == illegalTok { p.curErr("not a valid test operator: %s", p.val) } case rdrIn, rdrOut: case _EOF, rightParen: return left case _Lit: p.curErr("test operator words must consist of a single literal") default: p.curErr("not a valid test operator: %v", p.tok) } b := &BinaryTest{ OpPos: p.pos, Op: BinTestOperator(p.tok), X: left, } // Save the previous quoteState, since we change it in TsReMatch. oldQuote := p.quote switch b.Op { case AndTest, OrTest: p.next() if b.Y = p.testExpr(token(b.Op), b.OpPos, false); b.Y == nil { p.followErrExp(b.OpPos, b.Op.String()) } case TsReMatch: if !p.lang.isBash() { p.langErr(p.pos, "regex tests", LangBash) } p.rxOpenParens = 0 p.rxFirstPart = true // TODO(mvdan): Using nested states within a regex will break in // all sorts of ways. The better fix is likely to use a stop // token, like we do with heredocs. p.quote = testExprRegexp fallthrough default: if _, ok := b.X.(*Word); !ok { p.posErr(b.OpPos, "expected %s, %s or %s after complex expr", AndTest, OrTest, "]]") } p.next() b.Y = p.followWordTok(token(b.Op), b.OpPos) } p.quote = oldQuote return b } func (p *Parser) testExprBase() TestExpr { switch p.tok { case _EOF, rightParen: return nil case _LitWord: op := token(testUnaryOp(p.val)) switch op { case illegalTok: case tsRefVar, tsModif: // not available in mksh if p.lang.isBash() { p.tok = op } default: p.tok = op } } switch p.tok { case exclMark: u := &UnaryTest{OpPos: p.pos, Op: TsNot} p.next() if u.X = p.testExpr(token(u.Op), u.OpPos, false); u.X == nil { p.followErrExp(u.OpPos, u.Op.String()) } return u case tsExists, tsRegFile, tsDirect, tsCharSp, tsBlckSp, tsNmPipe, tsSocket, tsSmbLink, tsSticky, tsGIDSet, tsUIDSet, tsGrpOwn, tsUsrOwn, tsModif, tsRead, tsWrite, tsExec, tsNoEmpty, tsFdTerm, tsEmpStr, tsNempStr, tsOptSet, tsVarSet, tsRefVar: u := &UnaryTest{OpPos: p.pos, Op: UnTestOperator(p.tok)} p.next() u.X = p.followWordTok(token(u.Op), u.OpPos) return u case leftParen: pe := &ParenTest{Lparen: p.pos} p.next() if pe.X = p.testExpr(leftParen, pe.Lparen, false); pe.X == nil { p.followErrExp(pe.Lparen, "(") } pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) return pe case _LitWord: if p.val == "]]" { return nil } fallthrough default: if w := p.getWord(); w != nil { return w } // otherwise we'd return a typed nil above return nil } } func (p *Parser) declClause(s *Stmt) { ds := &DeclClause{Variant: p.lit(p.pos, p.val)} p.next() for !p.stopToken() && !p.peekRedir() { if p.hasValidIdent() { ds.Args = append(ds.Args, p.getAssign(false)) } else if p.eqlOffs > 0 { p.curErr("invalid var name") } else if p.tok == _LitWord && ValidName(p.val) { ds.Args = append(ds.Args, &Assign{ Naked: true, Name: p.getLit(), }) } else if w := p.getWord(); w != nil { ds.Args = append(ds.Args, &Assign{ Naked: true, Value: w, }) } else { p.followErr(p.pos, ds.Variant.Value, "names or assignments") } } s.Cmd = ds } func isBashCompoundCommand(tok token, val string) bool { switch tok { case leftParen, dblLeftParen: return true case _LitWord: switch val { case "{", "if", "while", "until", "for", "case", "[[", "coproc", "let", "function", "declare", "local", "export", "readonly", "typeset", "nameref": return true } } return false } func (p *Parser) timeClause(s *Stmt) { tc := &TimeClause{Time: p.pos} p.next() if _, ok := p.gotRsrv("-p"); ok { tc.PosixFormat = true } tc.Stmt = p.gotStmtPipe(p.stmt(p.pos), false) s.Cmd = tc } func (p *Parser) coprocClause(s *Stmt) { cc := &CoprocClause{Coproc: p.pos} if p.next(); isBashCompoundCommand(p.tok, p.val) { // has no name cc.Stmt = p.gotStmtPipe(p.stmt(p.pos), false) s.Cmd = cc return } cc.Name = p.getWord() cc.Stmt = p.gotStmtPipe(p.stmt(p.pos), false) if cc.Stmt == nil { if cc.Name == nil { p.posErr(cc.Coproc, "coproc clause requires a command") return } // name was in fact the stmt cc.Stmt = p.stmt(cc.Name.Pos()) cc.Stmt.Cmd = p.call(cc.Name) cc.Name = nil } else if cc.Name != nil { if call, ok := cc.Stmt.Cmd.(*CallExpr); ok { // name was in fact the start of a call call.Args = append([]*Word{cc.Name}, call.Args...) cc.Name = nil } } s.Cmd = cc } func (p *Parser) letClause(s *Stmt) { lc := &LetClause{Let: p.pos} old := p.preNested(arithmExprLet) p.next() for !p.stopToken() && !p.peekRedir() { x := p.arithmExpr(true) if x == nil { break } lc.Exprs = append(lc.Exprs, x) } if len(lc.Exprs) == 0 { p.followErrExp(lc.Let, "let") } p.postNested(old) s.Cmd = lc } func (p *Parser) bashFuncDecl(s *Stmt) { fpos := p.pos if p.next(); p.tok != _LitWord { p.followErr(fpos, "function", "a name") } name := p.lit(p.pos, p.val) hasParens := false if p.next(); p.got(leftParen) { hasParens = true p.follow(name.ValuePos, "foo(", rightParen) } p.funcDecl(s, name, fpos, hasParens) } func (p *Parser) testDecl(s *Stmt) { td := &TestDecl{Position: p.pos} p.next() if td.Description = p.getWord(); td.Description == nil { p.followErr(td.Position, "@test", "a description word") } if td.Body = p.getStmt(false, false, true); td.Body == nil { p.followErr(td.Position, `@test "desc"`, "a statement") } s.Cmd = td } func (p *Parser) callExpr(s *Stmt, w *Word, assign bool) { ce := p.call(w) if w == nil { ce.Args = ce.Args[:0] } if assign { ce.Assigns = append(ce.Assigns, p.getAssign(true)) } loop: for { switch p.tok { case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, dblSemicolon, semiAnd, dblSemiAnd, semiOr: break loop case _LitWord: if len(ce.Args) == 0 && p.hasValidIdent() { ce.Assigns = append(ce.Assigns, p.getAssign(true)) break } ce.Args = append(ce.Args, p.word( p.wps(p.lit(p.pos, p.val)), )) p.next() case _Lit: if len(ce.Args) == 0 && p.hasValidIdent() { ce.Assigns = append(ce.Assigns, p.getAssign(true)) break } ce.Args = append(ce.Args, p.word(p.wordParts())) case bckQuote: if p.backquoteEnd() { break loop } fallthrough case dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, globQuest, globStar, globPlus, globAt, globExcl: ce.Args = append(ce.Args, p.word(p.wordParts())) case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: p.doRedirect(s) case dblLeftParen: p.curErr("%s can only be used to open an arithmetic cmd", p.tok) case rightParen: if p.quote == subCmd { break loop } fallthrough default: p.curErr("a command can only contain words and redirects; encountered %s", p.tok) } } if len(ce.Assigns) == 0 && len(ce.Args) == 0 { return } if len(ce.Args) == 0 { ce.Args = nil } else { for _, asgn := range ce.Assigns { if asgn.Index != nil || asgn.Array != nil { p.posErr(asgn.Pos(), "inline variables cannot be arrays") } } } s.Cmd = ce } func (p *Parser) funcDecl(s *Stmt, name *Lit, pos Pos, withParens bool) { fd := &FuncDecl{ Position: pos, RsrvWord: pos != name.ValuePos, Parens: withParens, Name: name, } p.got(_Newl) if fd.Body = p.getStmt(false, false, true); fd.Body == nil { p.followErr(fd.Pos(), "foo()", "a statement") } s.Cmd = fd } sh-3.4.3/syntax/parser_arithm.go000066400000000000000000000202601420425111700166450ustar00rootroot00000000000000package syntax // compact specifies whether we allow spaces between expressions. // This is true for let func (p *Parser) arithmExpr(compact bool) ArithmExpr { return p.arithmExprComma(compact) } // These function names are inspired by Bash's expr.c func (p *Parser) arithmExprComma(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprAssign, Comma) } func (p *Parser) arithmExprAssign(compact bool) ArithmExpr { // Assign is different from the other binary operators because it's // right-associative and needs to check that it's placed after a name value := p.arithmExprTernary(compact) switch BinAritOperator(p.tok) { case AddAssgn, SubAssgn, MulAssgn, QuoAssgn, RemAssgn, AndAssgn, OrAssgn, XorAssgn, ShlAssgn, ShrAssgn, Assgn: if compact && p.spaced { return value } if !isArithName(value) { p.posErr(p.pos, "%s must follow a name", p.tok.String()) } pos := p.pos tok := p.tok p.nextArithOp(compact) y := p.arithmExprAssign(compact) if y == nil { p.followErrExp(pos, tok.String()) } return &BinaryArithm{ OpPos: pos, Op: BinAritOperator(tok), X: value, Y: y, } } return value } func (p *Parser) arithmExprTernary(compact bool) ArithmExpr { value := p.arithmExprLor(compact) if BinAritOperator(p.tok) != TernQuest || (compact && p.spaced) { return value } if value == nil { p.curErr("%s must follow an expression", p.tok.String()) } questPos := p.pos p.nextArithOp(compact) if BinAritOperator(p.tok) == TernColon { p.followErrExp(questPos, TernQuest.String()) } trueExpr := p.arithmExpr(compact) if trueExpr == nil { p.followErrExp(questPos, TernQuest.String()) } if BinAritOperator(p.tok) != TernColon { p.posErr(questPos, "ternary operator missing : after ?") } colonPos := p.pos p.nextArithOp(compact) falseExpr := p.arithmExprTernary(compact) if falseExpr == nil { p.followErrExp(colonPos, TernColon.String()) } return &BinaryArithm{ OpPos: questPos, Op: TernQuest, X: value, Y: &BinaryArithm{ OpPos: colonPos, Op: TernColon, X: trueExpr, Y: falseExpr, }, } } func (p *Parser) arithmExprLor(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprLand, OrArit) } func (p *Parser) arithmExprLand(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprBor, AndArit) } func (p *Parser) arithmExprBor(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprBxor, Or) } func (p *Parser) arithmExprBxor(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprBand, Xor) } func (p *Parser) arithmExprBand(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprEquality, And) } func (p *Parser) arithmExprEquality(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprComparison, Eql, Neq) } func (p *Parser) arithmExprComparison(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprShift, Lss, Gtr, Leq, Geq) } func (p *Parser) arithmExprShift(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprAddition, Shl, Shr) } func (p *Parser) arithmExprAddition(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprMultiplication, Add, Sub) } func (p *Parser) arithmExprMultiplication(compact bool) ArithmExpr { return p.arithmExprBinary(compact, p.arithmExprPower, Mul, Quo, Rem) } func (p *Parser) arithmExprPower(compact bool) ArithmExpr { // Power is different from the other binary operators because it's right-associative value := p.arithmExprUnary(compact) if BinAritOperator(p.tok) != Pow || (compact && p.spaced) { return value } if value == nil { p.curErr("%s must follow an expression", p.tok.String()) } op := p.tok pos := p.pos p.nextArithOp(compact) y := p.arithmExprPower(compact) if y == nil { p.followErrExp(pos, op.String()) } return &BinaryArithm{ OpPos: pos, Op: BinAritOperator(op), X: value, Y: y, } } func (p *Parser) arithmExprUnary(compact bool) ArithmExpr { if !compact { p.got(_Newl) } switch UnAritOperator(p.tok) { case Not, BitNegation, Plus, Minus: ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} p.nextArithOp(compact) if ue.X = p.arithmExprUnary(compact); ue.X == nil { p.followErrExp(ue.OpPos, ue.Op.String()) } return ue } return p.arithmExprValue(compact) } func (p *Parser) arithmExprValue(compact bool) ArithmExpr { var x ArithmExpr switch p.tok { case addAdd, subSub: ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} p.nextArith(compact) if p.tok != _LitWord { p.followErr(ue.OpPos, token(ue.Op).String(), "a literal") } ue.X = p.arithmExprValue(compact) return ue case leftParen: pe := &ParenArithm{Lparen: p.pos} p.nextArithOp(compact) pe.X = p.followArithm(leftParen, pe.Lparen) pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) x = pe case leftBrack: p.curErr("[ must follow a name") case colon: p.curErr("ternary operator missing ? before :") case _LitWord: l := p.getLit() if p.tok != leftBrack { x = p.word(p.wps(l)) break } pe := &ParamExp{Dollar: l.ValuePos, Short: true, Param: l} pe.Index = p.eitherIndex() x = p.word(p.wps(pe)) case bckQuote: if p.quote == arithmExprLet && p.openBquotes > 0 { return nil } fallthrough default: if w := p.getWord(); w != nil { x = w } else { return nil } } if compact && p.spaced { return x } if !compact { p.got(_Newl) } // we want real nil, not (*Word)(nil) as that // sets the type to non-nil and then x != nil if p.tok == addAdd || p.tok == subSub { if !isArithName(x) { p.curErr("%s must follow a name", p.tok.String()) } u := &UnaryArithm{ Post: true, OpPos: p.pos, Op: UnAritOperator(p.tok), X: x, } p.nextArith(compact) return u } return x } // nextArith consumes a token. // It returns true if compact and the token was followed by spaces func (p *Parser) nextArith(compact bool) bool { p.next() if compact && p.spaced { return true } if !compact { p.got(_Newl) } return false } func (p *Parser) nextArithOp(compact bool) { pos := p.pos tok := p.tok if p.nextArith(compact) { p.followErrExp(pos, tok.String()) } } // arithmExprBinary is used for all left-associative binary operators func (p *Parser) arithmExprBinary(compact bool, nextOp func(bool) ArithmExpr, operators ...BinAritOperator) ArithmExpr { value := nextOp(compact) for { var foundOp BinAritOperator for _, op := range operators { if p.tok == token(op) { foundOp = op break } } if token(foundOp) == illegalTok || (compact && p.spaced) { return value } if value == nil { p.curErr("%s must follow an expression", p.tok.String()) } pos := p.pos p.nextArithOp(compact) y := nextOp(compact) if y == nil { p.followErrExp(pos, foundOp.String()) } value = &BinaryArithm{ OpPos: pos, Op: foundOp, X: value, Y: y, } } } func isArithName(left ArithmExpr) bool { w, ok := left.(*Word) if !ok || len(w.Parts) != 1 { return false } switch x := w.Parts[0].(type) { case *Lit: return ValidName(x.Value) case *ParamExp: return x.nakedIndex() default: return false } } func (p *Parser) followArithm(ftok token, fpos Pos) ArithmExpr { x := p.arithmExpr(false) if x == nil { p.followErrExp(fpos, ftok.String()) } return x } func (p *Parser) peekArithmEnd() bool { return p.tok == rightParen && p.r == ')' } func (p *Parser) arithmMatchingErr(pos Pos, left, right token) { switch p.tok { case _Lit, _LitWord: p.curErr("not a valid arithmetic operator: %s", p.val) case leftBrack: p.curErr("[ must follow a name") case colon: p.curErr("ternary operator missing ? before :") case rightParen, _EOF: p.matchingErr(pos, left, right) default: if p.quote == arithmExpr { p.curErr("not a valid arithmetic operator: %v", p.tok) } p.matchingErr(pos, left, right) } } func (p *Parser) matchedArithm(lpos Pos, left, right token) { if !p.got(right) { p.arithmMatchingErr(lpos, left, right) } } func (p *Parser) arithmEnd(ltok token, lpos Pos, old saveState) Pos { if !p.peekArithmEnd() { p.arithmMatchingErr(lpos, ltok, dblRightParen) } p.rune() p.postNested(old) pos := p.pos p.next() return pos } sh-3.4.3/syntax/parser_test.go000066400000000000000000001527371420425111700163570ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "reflect" "regexp" "strings" "sync" "testing" "github.com/kr/pretty" ) func TestKeepComments(t *testing.T) { t.Parallel() in := "# foo\ncmd\n# bar" want := &File{ Stmts: []*Stmt{{ Comments: []Comment{{Text: " foo"}}, Cmd: litCall("cmd"), }}, Last: []Comment{{Text: " bar"}}, } singleParse(NewParser(KeepComments(true)), in, want)(t) } func TestParseBash(t *testing.T) { t.Parallel() p := NewParser() for i, c := range append(fileTests, fileTestsNoPrint...) { want := c.Bash if want == nil { continue } for j, in := range c.Strs { t.Run(fmt.Sprintf("#%03d-%d", i, j), singleParse(p, in, want)) } } } func TestParsePosOverflow(t *testing.T) { t.Parallel() // Consider using a custom reader to save memory. tests := []struct { name, in, want string }{ { "LineOverflowIsValid", strings.Repeat("\n", lineMax) + "foo; bar", "", }, { "LineOverflowPosString", strings.Repeat("\n", lineMax) + ")", "?:1: ) can only be used to close a subshell", }, { "LineOverflowExtraPosString", strings.Repeat("\n", lineMax+5) + ")", "?:1: ) can only be used to close a subshell", }, { "ColOverflowPosString", strings.Repeat(" ", colMax) + ")", "1:?: ) can only be used to close a subshell", }, { "ColOverflowExtraPosString", strings.Repeat(" ", colMax) + ")", "1:?: ) can only be used to close a subshell", }, { "ColOverflowSkippedPosString", strings.Repeat(" ", colMax+5) + "\n)", "2:1: ) can only be used to close a subshell", }, { "LargestLineNumber", strings.Repeat("\n", lineMax-1) + ")", "262143:1: ) can only be used to close a subshell", }, { "LargestColNumber", strings.Repeat(" ", colMax-1) + ")", "1:16383: ) can only be used to close a subshell", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { test := test t.Parallel() p := NewParser() _, err := p.Parse(strings.NewReader(test.in), "") got := fmt.Sprint(err) if got != test.want { t.Fatalf("want error %q, got %q", test.want, got) } }) } } func TestParsePosix(t *testing.T) { t.Parallel() p := NewParser(Variant(LangPOSIX)) for i, c := range append(fileTests, fileTestsNoPrint...) { want := c.Posix if want == nil { continue } for j, in := range c.Strs { t.Run(fmt.Sprintf("#%03d-%d", i, j), singleParse(p, in, want)) } } } func TestParseMirBSDKorn(t *testing.T) { t.Parallel() p := NewParser(Variant(LangMirBSDKorn)) for i, c := range append(fileTests, fileTestsNoPrint...) { want := c.MirBSDKorn if want == nil { continue } for j, in := range c.Strs { t.Run(fmt.Sprintf("#%03d-%d", i, j), singleParse(p, in, want)) } } } func TestParseBats(t *testing.T) { t.Parallel() p := NewParser(Variant(LangBats)) for i, c := range append(fileTests, fileTestsNoPrint...) { want := c.Bats if want == nil { continue } for j, in := range c.Strs { t.Run(fmt.Sprintf("#%03d-%d", i, j), singleParse(p, in, want)) } } } func TestMain(m *testing.M) { // Set the locale to computer-friendly English and UTF-8. // Some systems like Arch miss C.UTF8, so fall back to the US English locale. if out, _ := exec.Command("locale", "-a").Output(); strings.Contains( strings.ToLower(string(out)), "c.utf", ) { os.Setenv("LANGUAGE", "C.UTF-8") os.Setenv("LC_ALL", "C.UTF-8") } else { os.Setenv("LANGUAGE", "en_US.UTF-8") os.Setenv("LC_ALL", "en_US.UTF-8") } os.Exit(m.Run()) } var ( storedHasBash51 bool onceHasBash51 sync.Once storedHasDash059 bool onceHasDash059 sync.Once storedHasMksh59 bool onceHasMksh59 sync.Once ) func hasBash51(tb testing.TB) { onceHasBash51.Do(func() { storedHasBash51 = cmdContains("version 5.1", "bash", "--version") }) if !storedHasBash51 { tb.Skipf("bash 5.1 required to run") } } func hasDash059(tb testing.TB) { // dash provides no way to check its version, so we have to // check if it's new enough as to not have the bug that breaks // our integration tests. // This also means our check does not require a specific version. onceHasDash059.Do(func() { storedHasDash059 = cmdContains("Bad subst", "dash", "-c", "echo ${#<}") }) if !storedHasDash059 { tb.Skipf("dash 0.5.9+ required to run") } } func hasMksh59(tb testing.TB) { onceHasMksh59.Do(func() { storedHasMksh59 = cmdContains(" R59 ", "mksh", "-c", "echo $KSH_VERSION") }) if !storedHasMksh59 { tb.Skipf("mksh 59 required to run") } } func cmdContains(substr, cmd string, args ...string) bool { out, err := exec.Command(cmd, args...).CombinedOutput() got := string(out) if err != nil { got += "\n" + err.Error() } return strings.Contains(got, substr) } var extGlobRe = regexp.MustCompile(`[@?*+!]\(`) func confirmParse(in, cmd string, wantErr bool) func(*testing.T) { return func(t *testing.T) { t.Parallel() var opts []string if cmd == "bash" && extGlobRe.MatchString(in) { // otherwise bash refuses to parse these // properly. Also avoid -n since that too makes // bash bail. in = "shopt -s extglob\n" + in } else if !wantErr { // -n makes bash accept invalid inputs like // "let" or "`{`", so only use it in // non-erroring tests. Should be safe to not use // -n anyway since these are supposed to just // fail. // also, -n will break if we are using extglob // as extglob is not actually applied. opts = append(opts, "-n") } cmd := exec.Command(cmd, opts...) cmd.Stdin = strings.NewReader(in) var stderr bytes.Buffer cmd.Stderr = &stderr err := cmd.Run() if stderr.Len() > 0 { // bash sometimes likes to error on an input via stderr // while forgetting to set the exit code to non-zero. // Fun. if s := stderr.String(); !strings.Contains(s, ": warning: ") { err = errors.New(s) } } if err != nil && strings.Contains(err.Error(), "command not found") { err = nil } if wantErr && err == nil { t.Fatalf("Expected error in %q of %q, found none", strings.Join(cmd.Args, " "), in) } else if !wantErr && err != nil { t.Fatalf("Unexpected error in %q of %q: %v", strings.Join(cmd.Args, " "), in, err) } } } func TestParseBashConfirm(t *testing.T) { if testing.Short() { t.Skip("calling bash is slow.") } hasBash51(t) i := 0 for _, c := range append(fileTests, fileTestsNoPrint...) { if c.Bash == nil { continue } for j, in := range c.Strs { t.Run(fmt.Sprintf("#%03d-%d", i, j), confirmParse(in, "bash", false)) } i++ } } func TestParsePosixConfirm(t *testing.T) { if testing.Short() { t.Skip("calling dash is slow.") } hasDash059(t) i := 0 for _, c := range append(fileTests, fileTestsNoPrint...) { if c.Posix == nil { continue } for j, in := range c.Strs { t.Run(fmt.Sprintf("#%03d-%d", i, j), confirmParse(in, "dash", false)) } i++ } } func TestParseMirBSDKornConfirm(t *testing.T) { if testing.Short() { t.Skip("calling mksh is slow.") } hasMksh59(t) i := 0 for _, c := range append(fileTests, fileTestsNoPrint...) { if c.MirBSDKorn == nil { continue } for j, in := range c.Strs { t.Run(fmt.Sprintf("#%03d-%d", i, j), confirmParse(in, "mksh", false)) } i++ } } func TestParseErrBashConfirm(t *testing.T) { if testing.Short() { t.Skip("calling bash is slow.") } hasBash51(t) for _, c := range shellTests { want := c.common if c.bsmk != nil { want = c.bsmk } if c.bash != nil { want = c.bash } if want == nil { continue } wantErr := !strings.Contains(want.(string), " #NOERR") t.Run("", confirmParse(c.in, "bash", wantErr)) } } func TestParseErrPosixConfirm(t *testing.T) { if testing.Short() { t.Skip("calling dash is slow.") } hasDash059(t) for _, c := range shellTests { want := c.common if c.posix != nil { want = c.posix } if want == nil { continue } wantErr := !strings.Contains(want.(string), " #NOERR") t.Run("", confirmParse(c.in, "dash", wantErr)) } } func TestParseErrMirBSDKornConfirm(t *testing.T) { if testing.Short() { t.Skip("calling mksh is slow.") } hasMksh59(t) for _, c := range shellTests { want := c.common if c.bsmk != nil { want = c.bsmk } if c.mksh != nil { want = c.mksh } if want == nil { continue } wantErr := !strings.Contains(want.(string), " #NOERR") t.Run("", confirmParse(c.in, "mksh", wantErr)) } } func singleParse(p *Parser, in string, want *File) func(t *testing.T) { return func(t *testing.T) { t.Helper() got, err := p.Parse(newStrictReader(in), "") if err != nil { t.Fatalf("Unexpected error in %q: %v", in, err) } clearPosRecurse(t, in, got) if !reflect.DeepEqual(got, want) { t.Fatalf("syntax tree mismatch in %q\ndiff:\n%s", in, strings.Join(pretty.Diff(want, got), "\n")) } } } func BenchmarkParse(b *testing.B) { b.ReportAllocs() src := "" + strings.Repeat("\n\n\t\t \n", 10) + "# " + strings.Repeat("foo bar ", 10) + "\n" + strings.Repeat("longlit_", 10) + "\n" + "'" + strings.Repeat("foo bar ", 10) + "'\n" + `"` + strings.Repeat("foo bar ", 10) + `"` + "\n" + strings.Repeat("aa bb cc dd; ", 6) + "a() { (b); { c; }; }; $(d; `e`)\n" + "foo=bar; a=b; c=d$foo${bar}e $simple ${complex:-default}\n" + "if a; then while b; do for c in d e; do f; done; done; fi\n" + "a | b && c || d | e && g || f\n" + "foo >a &1 <f { foo; }", common: `1:11: "}" can only be used to close a block`, }, { in: ">f foo() { bar; }", common: `1:7: a command can only contain words and redirects; encountered (`, }, { in: ">f if foo; then bar; fi", common: `1:12: "then" can only be used in an if`, }, { in: "if done; then b; fi", common: `1:4: "done" can only be used to end a loop`, }, { in: "'", common: `1:1: reached EOF without closing quote '`, }, { in: `"`, common: `1:1: reached EOF without closing quote "`, }, { in: `'\''`, common: `1:4: reached EOF without closing quote '`, }, { in: ";", common: `1:1: ; can only immediately follow a statement`, }, { in: "{ ; }", common: `1:3: ; can only immediately follow a statement`, }, { in: `"foo"(){ :; }`, common: `1:1: invalid func name`, mksh: `1:1: invalid func name #NOERR`, }, { in: `foo$bar(){ :; }`, common: `1:1: invalid func name`, }, { in: "{", common: `1:1: reached EOF without matching { with }`, }, { in: "{ #}", common: `1:1: reached EOF without matching { with }`, }, { in: "(", common: `1:1: reached EOF without matching ( with )`, }, { in: ")", common: `1:1: ) can only be used to close a subshell`, }, { in: "`", common: "1:1: reached EOF without closing quote `", }, { in: ";;", common: `1:1: ;; can only be used in a case clause`, }, { in: "( foo;", common: `1:1: reached EOF without matching ( with )`, }, { in: "&", common: `1:1: & can only immediately follow a statement`, }, { in: "|", common: `1:1: | can only immediately follow a statement`, }, { in: "&&", common: `1:1: && can only immediately follow a statement`, }, { in: "||", common: `1:1: || can only immediately follow a statement`, }, { in: "foo; || bar", common: `1:6: || can only immediately follow a statement`, }, { in: "echo & || bar", common: `1:8: || can only immediately follow a statement`, }, { in: "echo & ; bar", common: `1:8: ; can only immediately follow a statement`, }, { in: "foo;;", common: `1:4: ;; can only be used in a case clause`, }, { in: "foo(", common: `1:1: "foo(" must be followed by )`, }, { in: "foo(bar", common: `1:1: "foo(" must be followed by )`, }, { in: "à(", common: `1:1: "foo(" must be followed by )`, }, { in: "foo'", common: `1:4: reached EOF without closing quote '`, }, { in: `foo"`, common: `1:4: reached EOF without closing quote "`, }, { in: `"foo`, common: `1:1: reached EOF without closing quote "`, }, { in: `"foobar\`, common: `1:1: reached EOF without closing quote "`, }, { in: `"foo\a`, common: `1:1: reached EOF without closing quote "`, }, { in: "foo()", common: `1:1: "foo()" must be followed by a statement`, mksh: `1:1: "foo()" must be followed by a statement #NOERR`, }, { in: "foo() {", common: `1:7: reached EOF without matching { with }`, }, { in: "foo-bar() { x; }", posix: `1:1: invalid func name`, }, { in: "foò() { x; }", posix: `1:1: invalid func name`, }, { in: "echo foo(", common: `1:9: a command can only contain words and redirects; encountered (`, }, { in: "echo &&", common: `1:6: && must be followed by a statement`, }, { in: "echo |", common: `1:6: | must be followed by a statement`, }, { in: "echo ||", common: `1:6: || must be followed by a statement`, }, { in: "echo | #bar", common: `1:6: | must be followed by a statement`, }, { in: "echo && #bar", common: `1:6: && must be followed by a statement`, }, { in: "`echo &&`", common: `1:7: && must be followed by a statement`, }, { in: "`echo |`", common: `1:7: | must be followed by a statement`, }, { in: "echo | ! bar", common: `1:8: "!" can only be used in full statements`, }, { in: "echo >", common: `1:6: > must be followed by a word`, }, { in: "echo >>", common: `1:6: >> must be followed by a word`, }, { in: "echo <", common: `1:6: < must be followed by a word`, }, { in: "echo 2>", common: `1:7: > must be followed by a word`, }, { in: "echo <\nbar", common: `1:6: < must be followed by a word`, }, { in: "echo | < #bar", common: `1:8: < must be followed by a word`, }, { in: "echo && > #", common: `1:9: > must be followed by a word`, }, { in: "<<", common: `1:1: << must be followed by a word`, }, { in: "<" must be followed by "then"`, }, { in: "if true then", common: `1:1: "if " must be followed by "then"`, }, { in: "if true; then bar;", common: `1:1: if statement must end with "fi"`, }, { in: "if true; then bar; fi#etc", common: `1:1: if statement must end with "fi"`, }, { in: "if a; then b; elif c;", common: `1:15: "elif " must be followed by "then"`, }, { in: "'foo' '", common: `1:7: reached EOF without closing quote '`, }, { in: "'foo\n' '", common: `2:3: reached EOF without closing quote '`, }, { in: "while", common: `1:1: "while" must be followed by a statement list`, }, { in: "while true;", common: `1:1: "while " must be followed by "do"`, }, { in: "while true; do bar", common: `1:1: while statement must end with "done"`, }, { in: "while true; do bar;", common: `1:1: while statement must end with "done"`, }, { in: "until", common: `1:1: "until" must be followed by a statement list`, }, { in: "until true;", common: `1:1: "until " must be followed by "do"`, }, { in: "until true; do bar", common: `1:1: until statement must end with "done"`, }, { in: "until true; do bar;", common: `1:1: until statement must end with "done"`, }, { in: "for", common: `1:1: "for" must be followed by a literal`, }, { in: "for i", common: `1:1: "for foo" must be followed by "in", "do", ;, or a newline`, }, { in: "for i in;", common: `1:1: "for foo [in words]" must be followed by "do"`, }, { in: "for i in 1 2 3;", common: `1:1: "for foo [in words]" must be followed by "do"`, }, { in: "for i in 1 2 &", common: `1:1: "for foo [in words]" must be followed by "do"`, }, { in: "for i in 1 2 (", common: `1:14: word list can only contain words`, }, { in: "for i in 1 2 3; do echo $i;", common: `1:1: for statement must end with "done"`, }, { in: "for i in 1 2 3; echo $i;", common: `1:1: "for foo [in words]" must be followed by "do"`, }, { in: "for 'i' in 1 2 3; do echo $i; done", common: `1:1: "for" must be followed by a literal`, }, { in: "for in 1 2 3; do echo $i; done", common: `1:1: "for foo" must be followed by "in", "do", ;, or a newline`, }, { in: "select", bsmk: `1:1: "select" must be followed by a literal`, }, { in: "select i", bsmk: `1:1: "select foo" must be followed by "in", "do", ;, or a newline`, }, { in: "select i in;", bsmk: `1:1: "select foo [in words]" must be followed by "do"`, }, { in: "select i in 1 2 3;", bsmk: `1:1: "select foo [in words]" must be followed by "do"`, }, { in: "select i in 1 2 3; do echo $i;", bsmk: `1:1: select statement must end with "done"`, }, { in: "select i in 1 2 3; echo $i;", bsmk: `1:1: "select foo [in words]" must be followed by "do"`, }, { in: "select 'i' in 1 2 3; do echo $i; done", bsmk: `1:1: "select" must be followed by a literal`, }, { in: "select in 1 2 3; do echo $i; done", bsmk: `1:1: "select foo" must be followed by "in", "do", ;, or a newline`, }, { in: "echo foo &\n;", common: `2:1: ; can only immediately follow a statement`, }, { in: "echo $(foo", common: `1:6: reached EOF without matching ( with )`, }, { in: "echo $((foo", common: `1:6: reached EOF without matching $(( with ))`, }, { in: `echo $((\`, common: `1:6: reached EOF without matching $(( with ))`, }, { in: `echo $((o\`, common: `1:6: reached EOF without matching $(( with ))`, }, { in: `echo $((foo\a`, common: `1:6: reached EOF without matching $(( with ))`, }, { in: `echo $(($(a"`, common: `1:12: reached EOF without closing quote "`, }, { in: "echo $((`echo 0`", common: `1:6: reached EOF without matching $(( with ))`, }, { in: `echo $((& $(`, common: `1:9: & must follow an expression`, }, { in: `echo $((a'`, common: `1:10: reached EOF without closing quote '`, }, { in: `echo $((a b"`, common: `1:11: not a valid arithmetic operator: b`, }, { in: "echo $(())", common: `1:6: $(( must be followed by an expression #NOERR`, }, { in: "echo $((()))", common: `1:9: ( must be followed by an expression`, }, { in: "echo $(((3))", common: `1:6: reached ) without matching $(( with ))`, }, { in: "echo $((+))", common: `1:9: + must be followed by an expression`, }, { in: "echo $((a b c))", common: `1:11: not a valid arithmetic operator: b`, }, { in: "echo $((a ; c))", common: `1:11: not a valid arithmetic operator: ;`, }, { in: "echo $((foo) )", bsmk: `1:6: reached ) without matching $(( with )) #NOERR`, }, { in: "echo $((a *))", common: `1:11: * must be followed by an expression`, }, { in: "echo $((++))", common: `1:9: ++ must be followed by a literal`, }, { in: "echo $((a ? b))", common: `1:11: ternary operator missing : after ?`, }, { in: "echo $((a : b))", common: `1:11: ternary operator missing ? before :`, }, { in: "echo $((/", common: `1:9: / must follow an expression`, }, { in: "echo $((:", common: `1:9: ternary operator missing ? before :`, }, { in: "echo $(((a)+=b))", common: `1:12: += must follow a name`, mksh: `1:12: += must follow a name #NOERR`, }, { in: "echo $((1=2))", common: `1:10: = must follow a name`, }, { in: "echo $(($0=2))", common: `1:11: = must follow a name #NOERR`, }, { in: "echo $(($(a)=2))", common: `1:13: = must follow a name #NOERR`, }, { in: "echo $((1'2'))", // TODO: Take a look at this again, since this no longer fails // after fixing github.com/mvdan/sh/issues/587. // Note that Bash seems to treat code inside $(()) as if it were // within double quotes, yet still requires single quotes to be // matched. // common: `1:10: not a valid arithmetic operator: '`, }, { in: "<", bsmk: `1:1: [[ must be followed by an expression`, }, { in: "local (", bash: `1:7: "local" must be followed by names or assignments`, }, { in: "declare 0=${o})", bash: `1:9: invalid var name`, }, { in: "a=(<)", bsmk: `1:4: array element values must be words`, }, { in: "a=([)", bash: `1:4: [ must be followed by an expression`, }, { in: "a=([i)", bash: `1:4: reached ) without matching [ with ]`, }, { in: "a=([i])", bash: `1:4: "[x]" must be followed by = #NOERR`, }, { in: "a[i]=(y)", bash: `1:6: arrays cannot be nested`, }, { in: "a=([i]=(y))", bash: `1:8: arrays cannot be nested`, }, { in: "o=([0]=#", bash: `1:8: array element values must be words`, }, { in: "a[b] ==[", bash: `1:1: "a[b]" must be followed by = #NOERR stringifies`, }, { in: "a[b] +=c", bash: `1:1: "a[b]" must be followed by = #NOERR stringifies`, }, { in: "a=(x y) foo", bash: `1:1: inline variables cannot be arrays #NOERR stringifies`, }, { in: "a[2]=x foo", bash: `1:1: inline variables cannot be arrays #NOERR stringifies`, }, { in: "function", bsmk: `1:1: "function" must be followed by a name`, }, { in: "function foo(", bsmk: `1:10: "foo(" must be followed by )`, }, { in: "function `function", bsmk: `1:1: "function" must be followed by a name`, }, { in: `function "foo"(){}`, bsmk: `1:1: "function" must be followed by a name`, }, { in: "function foo()", bsmk: `1:1: "foo()" must be followed by a statement`, }, { in: "echo <<<", bsmk: `1:6: <<< must be followed by a word`, }, { in: "a[", bsmk: `1:2: [ must be followed by an expression`, }, { in: "a[b", bsmk: `1:2: reached EOF without matching [ with ]`, }, { in: "a[]", bsmk: `1:2: [ must be followed by an expression #NOERR is cmd`, }, { in: "a[[", bsmk: `1:3: [ must follow a name`, }, { in: "echo $((a[))", bsmk: `1:10: [ must be followed by an expression`, }, { in: "echo $((a[b))", bsmk: `1:10: reached ) without matching [ with ]`, }, { in: "echo $((a[]))", bash: `1:10: [ must be followed by an expression`, mksh: `1:10: [ must be followed by an expression #NOERR wrong?`, }, { in: "echo $((x$t[", bsmk: `1:12: [ must follow a name`, }, { in: "a[1]", bsmk: `1:1: "a[b]" must be followed by = #NOERR is cmd`, }, { in: "a[i]+", bsmk: `1:1: "a[b]+" must be followed by = #NOERR is cmd`, }, { in: "a[1]#", bsmk: `1:1: "a[b]" must be followed by = #NOERR is cmd`, }, { in: "echo $[foo", bash: `1:6: reached EOF without matching $[ with ]`, }, { in: "echo $'", bsmk: `1:6: reached EOF without closing quote '`, }, { in: `echo $"`, bsmk: `1:6: reached EOF without closing quote "`, }, { in: "echo @(", bsmk: `1:6: reached EOF without matching @( with )`, }, { in: "echo @(a", bsmk: `1:6: reached EOF without matching @( with )`, }, { in: "((@(", bsmk: `1:1: reached ( without matching (( with ))`, }, { in: "time {", bsmk: `1:6: reached EOF without matching { with }`, }, { in: "time ! foo", bash: `1:6: "!" can only be used in full statements #NOERR wrong`, mksh: `1:6: "!" can only be used in full statements`, }, { in: "coproc", bash: `1:1: coproc clause requires a command`, }, { in: "coproc\n$", bash: `1:1: coproc clause requires a command`, }, { in: "coproc declare (", bash: `1:16: "declare" must be followed by names or assignments`, }, { in: "echo ${foo[1 2]}", bsmk: `1:14: not a valid arithmetic operator: 2`, }, { in: "echo ${foo[}", bsmk: `1:11: [ must be followed by an expression`, }, { in: "echo ${foo]}", bsmk: `1:11: not a valid parameter expansion operator: ]`, }, { in: "echo ${foo[]}", bash: `1:11: [ must be followed by an expression`, mksh: `1:11: [ must be followed by an expression #NOERR wrong?`, }, { in: "echo ${a/\n", bsmk: `1:6: reached EOF without matching ${ with }`, }, { in: "echo ${a/''", bsmk: `1:6: reached EOF without matching ${ with }`, }, { in: "echo ${a-\n", bsmk: `1:6: reached EOF without matching ${ with }`, }, { in: "echo ${foo:", bsmk: `1:11: : must be followed by an expression`, }, { in: "echo ${foo:1 2}", bsmk: `1:14: not a valid arithmetic operator: 2 #NOERR lazy eval`, }, { in: "echo ${foo:1", bsmk: `1:6: reached EOF without matching ${ with }`, }, { in: "echo ${foo:1:", bsmk: `1:13: : must be followed by an expression`, }, { in: "echo ${foo:1:2", bsmk: `1:6: reached EOF without matching ${ with }`, }, { in: "echo ${foo,", bash: `1:6: reached EOF without matching ${ with }`, }, { in: "echo ${foo@", bash: `1:11: @ expansion operator requires a literal`, }, { in: "echo ${foo@}", bash: `1:12: @ expansion operator requires a literal #NOERR empty string fallback`, }, { in: "echo ${foo@Q", bash: `1:6: reached EOF without matching ${ with }`, }, { in: "echo ${foo@bar}", bash: `1:12: invalid @ expansion operator #NOERR at runtime`, }, { in: "echo ${foo@'Q'}", bash: `1:12: @ expansion operator requires a literal #NOERR at runtime`, }, { in: `echo $((echo a); (echo b))`, bsmk: `1:14: not a valid arithmetic operator: a #NOERR backtrack`, }, { in: `((echo a); (echo b))`, bsmk: `1:8: not a valid arithmetic operator: a #NOERR backtrack`, }, { in: "for ((;;", bash: `1:5: reached EOF without matching (( with ))`, }, { in: "for ((;;0000000", bash: `1:5: reached EOF without matching (( with ))`, }, { in: "function foo() { bar; }", posix: `1:13: a command can only contain words and redirects; encountered (`, }, { in: "echo <(", posix: `1:6: < must be followed by a word`, mksh: `1:6: < must be followed by a word`, }, { in: "echo >(", posix: `1:6: > must be followed by a word`, mksh: `1:6: > must be followed by a word`, }, { // shells treat {var} as an argument, but we are a bit stricter // so that users won't think this will work like they expect in // POSIX shell. in: "echo {var}>foo", posix: `1:6: {varname} redirects are a bash feature #NOERR`, mksh: `1:6: {varname} redirects are a bash feature #NOERR`, }, { in: "echo ;&", posix: `1:7: & can only immediately follow a statement`, bsmk: `1:6: ;& can only be used in a case clause`, }, { in: "echo ;;&", posix: `1:6: ;; can only be used in a case clause`, mksh: `1:6: ;; can only be used in a case clause`, }, { in: "echo ;|", posix: `1:7: | can only immediately follow a statement`, bash: `1:7: | can only immediately follow a statement`, }, { in: "for i in 1 2 3; { echo; }", posix: `1:17: for loops with braces are a bash/mksh feature`, }, { in: "for ((i=0; i<5; i++)); do echo; done", posix: `1:5: c-style fors are a bash feature`, mksh: `1:5: c-style fors are a bash feature`, }, { in: "echo !(a)", posix: `1:6: extended globs are a bash/mksh feature`, }, { in: "echo $a@(b)", posix: `1:8: extended globs are a bash/mksh feature`, }, { in: "foo=(1 2)", posix: `1:5: arrays are a bash/mksh feature`, }, { in: "a=$c\n'", common: `2:1: reached EOF without closing quote '`, }, { in: "echo ${!foo}", posix: `1:8: ${!foo} is a bash/mksh feature`, }, { in: "echo ${foo[1]}", posix: `1:11: arrays are a bash/mksh feature`, }, { in: "echo ${foo/a/b}", posix: `1:11: search and replace is a bash/mksh feature`, }, { in: "echo ${foo:1}", posix: `1:11: slicing is a bash/mksh feature`, }, { in: "echo ${foo,bar}", posix: `1:11: this expansion operator is a bash feature`, mksh: `1:11: this expansion operator is a bash feature`, }, { in: "echo ${foo@Q}", posix: `1:11: this expansion operator is a bash/mksh feature`, }, { in: "echo ${foo@a}", mksh: `1:12: this expansion operator is a bash feature`, }, { in: "echo ${foo@u}", mksh: `1:12: this expansion operator is a bash feature`, }, { in: "echo ${foo@A}", mksh: `1:12: this expansion operator is a bash feature`, }, { in: "echo ${foo@E}", mksh: `1:12: this expansion operator is a bash feature`, }, { in: "echo ${foo@K}", mksh: `1:12: this expansion operator is a bash feature`, }, { in: "echo ${foo@L}", mksh: `1:12: this expansion operator is a bash feature`, }, { in: "echo ${foo@P}", mksh: `1:12: this expansion operator is a bash feature`, }, { in: "echo ${foo@U}", mksh: `1:12: this expansion operator is a bash feature`, }, { in: "echo ${foo@#}", bash: `1:12: this expansion operator is a mksh feature #NOERR`, }, { in: "`\"`\\", common: "1:3: reached EOF without closing quote `", }, } func checkError(p *Parser, in, want string) func(*testing.T) { return func(t *testing.T) { if i := strings.Index(want, " #NOERR"); i >= 0 { want = want[:i] } _, err := p.Parse(newStrictReader(in), "") if err == nil { t.Fatalf("Expected error in %q: %v", in, want) } if got := err.Error(); got != want { t.Fatalf("Error mismatch in %q\nwant: %s\ngot: %s", in, want, got) } } } func TestParseErrPosix(t *testing.T) { t.Parallel() p := NewParser(KeepComments(true), Variant(LangPOSIX)) for _, c := range shellTests { want := c.common if c.posix != nil { want = c.posix } if want == nil { continue } t.Run("", checkError(p, c.in, want.(string))) } } func TestParseErrBash(t *testing.T) { t.Parallel() p := NewParser(KeepComments(true)) for _, c := range shellTests { want := c.common if c.bsmk != nil { want = c.bsmk } if c.bash != nil { want = c.bash } if want == nil { continue } t.Run("", checkError(p, c.in, want.(string))) } } func TestParseErrMirBSDKorn(t *testing.T) { t.Parallel() p := NewParser(KeepComments(true), Variant(LangMirBSDKorn)) for _, c := range shellTests { want := c.common if c.bsmk != nil { want = c.bsmk } if c.mksh != nil { want = c.mksh } if want == nil { continue } t.Run("", checkError(p, c.in, want.(string))) } } func TestInputName(t *testing.T) { t.Parallel() in := "(" want := "some-file.sh:1:1: reached EOF without matching ( with )" p := NewParser() _, err := p.Parse(strings.NewReader(in), "some-file.sh") if err == nil { t.Fatalf("Expected error in %q: %v", in, want) } got := err.Error() if got != want { t.Fatalf("Error mismatch in %q\nwant: %s\ngot: %s", in, want, got) } } var errBadReader = fmt.Errorf("write: expected error") type badReader struct{} func (b badReader) Read(p []byte) (int, error) { return 0, errBadReader } func TestReadErr(t *testing.T) { t.Parallel() p := NewParser() _, err := p.Parse(badReader{}, "") if err == nil { t.Fatalf("Expected error with bad reader") } if err != errBadReader { t.Fatalf("Error mismatch with bad reader:\nwant: %v\ngot: %v", errBadReader, err) } } type strictStringReader struct { *strings.Reader gaveEOF bool } func newStrictReader(s string) *strictStringReader { return &strictStringReader{Reader: strings.NewReader(s)} } func (r *strictStringReader) Read(p []byte) (int, error) { n, err := r.Reader.Read(p) if err == io.EOF { if r.gaveEOF { return n, fmt.Errorf("duplicate EOF read") } r.gaveEOF = true } return n, err } func TestParseStmts(t *testing.T) { t.Parallel() p := NewParser() inReader, inWriter := io.Pipe() recv := make(chan bool, 10) errc := make(chan error, 1) go func() { errc <- p.Stmts(inReader, func(s *Stmt) bool { recv <- true return true }) }() io.WriteString(inWriter, "foo\n") <-recv io.WriteString(inWriter, "bar; baz") inWriter.Close() <-recv <-recv if err := <-errc; err != nil { t.Fatalf("Expected no error: %v", err) } } func TestParseStmtsStopEarly(t *testing.T) { t.Parallel() p := NewParser() inReader, inWriter := io.Pipe() defer inWriter.Close() recv := make(chan bool, 10) errc := make(chan error, 1) go func() { errc <- p.Stmts(inReader, func(s *Stmt) bool { recv <- true return !s.Background }) }() io.WriteString(inWriter, "a\n") <-recv io.WriteString(inWriter, "b &\n") // stop here <-recv if err := <-errc; err != nil { t.Fatalf("Expected no error: %v", err) } } func TestParseStmtsError(t *testing.T) { t.Parallel() in := "foo; )" p := NewParser() recv := make(chan bool, 10) errc := make(chan error, 1) go func() { errc <- p.Stmts(strings.NewReader(in), func(s *Stmt) bool { recv <- true return true }) }() <-recv if err := <-errc; err == nil { t.Fatalf("Expected an error in %q, but got nil", in) } } func TestParseWords(t *testing.T) { t.Parallel() p := NewParser() inReader, inWriter := io.Pipe() recv := make(chan bool, 10) errc := make(chan error, 1) go func() { errc <- p.Words(inReader, func(w *Word) bool { recv <- true return true }) }() // TODO: Allow a single space to end parsing a word. At the moment, the // parser must read the next non-space token (the next literal or // newline, in this case) to finish parsing a word. io.WriteString(inWriter, "foo ") io.WriteString(inWriter, "bar\n") <-recv io.WriteString(inWriter, "baz etc") inWriter.Close() <-recv <-recv <-recv if err := <-errc; err != nil { t.Fatalf("Expected no error: %v", err) } } func TestParseWordsStopEarly(t *testing.T) { t.Parallel() p := NewParser() r := strings.NewReader("a\nb\nc\n") parsed := 0 err := p.Words(r, func(w *Word) bool { parsed++ return w.Lit() != "b" }) if err != nil { t.Fatalf("Expected no error: %v", err) } if want := 2; parsed != want { t.Fatalf("wanted %d words parsed, got %d", want, parsed) } } func TestParseWordsError(t *testing.T) { t.Parallel() in := "foo )" p := NewParser() recv := make(chan bool, 10) errc := make(chan error, 1) go func() { errc <- p.Words(strings.NewReader(in), func(w *Word) bool { recv <- true return true }) }() <-recv want := "1:5: ) is not a valid word" got := fmt.Sprintf("%v", <-errc) if got != want { t.Fatalf("Expected %q as an error, but got %q", want, got) } } var documentTests = []struct { in string want []WordPart }{ { "foo", []WordPart{lit("foo")}, }, { " foo $bar", []WordPart{ lit(" foo "), litParamExp("bar"), }, }, { "$bar\n\n", []WordPart{ litParamExp("bar"), lit("\n\n"), }, }, } func TestParseDocument(t *testing.T) { t.Parallel() p := NewParser() for _, tc := range documentTests { t.Run("", func(t *testing.T) { got, err := p.Document(strings.NewReader(tc.in)) if err != nil { t.Fatal(err) } clearPosRecurse(t, "", got) want := &Word{Parts: tc.want} if !reflect.DeepEqual(got, want) { t.Fatalf("syntax tree mismatch in %q\ndiff:\n%s", tc.in, strings.Join(pretty.Diff(want, got), "\n")) } }) } } func TestParseDocumentError(t *testing.T) { t.Parallel() in := "foo $(" p := NewParser() _, err := p.Document(strings.NewReader(in)) want := "1:5: reached EOF without matching ( with )" got := fmt.Sprintf("%v", err) if got != want { t.Fatalf("Expected %q as an error, but got %q", want, got) } } var arithmeticTests = []struct { in string want ArithmExpr }{ { "foo", litWord("foo"), }, { "3 + 4", &BinaryArithm{ Op: Add, X: litWord("3"), Y: litWord("4"), }, }, { "3 + 4 + 5", &BinaryArithm{ Op: Add, X: &BinaryArithm{ Op: Add, X: litWord("3"), Y: litWord("4"), }, Y: litWord("5"), }, }, { "1 ? 0 : 2", &BinaryArithm{ Op: TernQuest, X: litWord("1"), Y: &BinaryArithm{ Op: TernColon, X: litWord("0"), Y: litWord("2"), }, }, }, { "a = 3, ++a, a--", &BinaryArithm{ Op: Comma, X: &BinaryArithm{ Op: Comma, X: &BinaryArithm{ Op: Assgn, X: litWord("a"), Y: litWord("3"), }, Y: &UnaryArithm{ Op: Inc, X: litWord("a"), }, }, Y: &UnaryArithm{ Op: Dec, Post: true, X: litWord("a"), }, }, }, } func TestParseArithmetic(t *testing.T) { t.Parallel() p := NewParser() for _, tc := range arithmeticTests { t.Run("", func(t *testing.T) { got, err := p.Arithmetic(strings.NewReader(tc.in)) if err != nil { t.Fatal(err) } clearPosRecurse(t, "", got) if !reflect.DeepEqual(got, tc.want) { t.Fatalf("syntax tree mismatch in %q\ndiff:\n%s", tc.in, strings.Join(pretty.Diff(tc.want, got), "\n")) } }) } } func TestParseArithmeticError(t *testing.T) { t.Parallel() in := "3 +" p := NewParser() _, err := p.Arithmetic(strings.NewReader(in)) want := "1:3: + must be followed by an expression" got := fmt.Sprintf("%v", err) if got != want { t.Fatalf("Expected %q as an error, but got %q", want, got) } } var stopAtTests = []struct { in string stop string want interface{} }{ { "foo bar", "$$", litCall("foo", "bar"), }, { "$foo $", "$$", call(word(litParamExp("foo")), litWord("$")), }, { "echo foo $$", "$$", litCall("echo", "foo"), }, { "$$", "$$", &File{}, }, { "echo foo\n$$\n", "$$", litCall("echo", "foo"), }, { "echo foo; $$", "$$", litCall("echo", "foo"), }, { "echo foo; $$", "$$", litCall("echo", "foo"), }, { "echo foo;$$", "$$", litCall("echo", "foo"), }, { "echo '$$'", "$$", call(litWord("echo"), word(sglQuoted("$$"))), }, } func TestParseStmtsStopAt(t *testing.T) { t.Parallel() for _, c := range stopAtTests { p := NewParser(StopAt(c.stop)) want := fullProg(c.want) t.Run("", singleParse(p, c.in, want)) } } func TestValidName(t *testing.T) { t.Parallel() tests := []struct { name string in string want bool }{ {"Empty", "", false}, {"Simple", "foo", true}, {"MixedCase", "Foo", true}, {"Underscore", "_foo", true}, {"NumberPrefix", "3foo", false}, {"NumberSuffix", "foo3", true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := ValidName(tc.in) if got != tc.want { t.Fatalf("ValidName(%q) got %t, wanted %t", tc.in, got, tc.want) } }) } } func TestIsIncomplete(t *testing.T) { t.Parallel() tests := []struct { in string want bool }{ {"foo\n", false}, {"foo;", false}, {"\n", false}, {"'incomp", true}, {"foo; 'incomp", true}, {" (incomp", true}, {"badsyntax)", false}, } p := NewParser() for i, tc := range tests { t.Run(fmt.Sprintf("Parse%02d", i), func(t *testing.T) { r := strings.NewReader(tc.in) _, err := p.Parse(r, "") if got := IsIncomplete(err); got != tc.want { t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want) } }) t.Run(fmt.Sprintf("Interactive%02d", i), func(t *testing.T) { r := strings.NewReader(tc.in) err := p.Interactive(r, func([]*Stmt) bool { return false }) if got := IsIncomplete(err); got != tc.want { t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want) } }) } } sh-3.4.3/syntax/printer.go000066400000000000000000001007111420425111700154700ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "bufio" "bytes" "fmt" "io" "strings" "text/tabwriter" "unicode" ) // PrinterOption is a function which can be passed to NewPrinter // to alter its behaviour. To apply option to existing Printer // call it directly, for example KeepPadding(true)(printer). type PrinterOption func(*Printer) // Indent sets the number of spaces used for indentation. If set to 0, // tabs will be used instead. func Indent(spaces uint) PrinterOption { return func(p *Printer) { p.indentSpaces = spaces } } // BinaryNextLine will make binary operators appear on the next line // when a binary command, such as a pipe, spans multiple lines. A // backslash will be used. func BinaryNextLine(enabled bool) PrinterOption { return func(p *Printer) { p.binNextLine = enabled } } // SwitchCaseIndent will make switch cases be indented. As such, switch // case bodies will be two levels deeper than the switch itself. func SwitchCaseIndent(enabled bool) PrinterOption { return func(p *Printer) { p.swtCaseIndent = enabled } } // TODO(v4): consider turning this into a "space all operators" option, to also // allow foo=( bar baz ), (( x + y )), and so on. // SpaceRedirects will put a space after most redirection operators. The // exceptions are '>&', '<&', '>(', and '<('. func SpaceRedirects(enabled bool) PrinterOption { return func(p *Printer) { p.spaceRedirects = enabled } } // KeepPadding will keep most nodes and tokens in the same column that // they were in the original source. This allows the user to decide how // to align and pad their code with spaces. // // Note that this feature is best-effort and will only keep the // alignment stable, so it may need some human help the first time it is // run. func KeepPadding(enabled bool) PrinterOption { return func(p *Printer) { if enabled && !p.keepPadding { // Enable the flag, and set up the writer wrapper. p.keepPadding = true p.cols.Writer = p.bufWriter.(*bufio.Writer) p.bufWriter = &p.cols } else if !enabled && p.keepPadding { // Ensure we reset the state to that of NewPrinter. p.keepPadding = false p.bufWriter = p.cols.Writer p.cols = colCounter{} } } } // Minify will print programs in a way to save the most bytes possible. // For example, indentation and comments are skipped, and extra // whitespace is avoided when possible. func Minify(enabled bool) PrinterOption { return func(p *Printer) { p.minify = enabled } } // SingleLine will attempt to print programs in one line. For example, lists of // commands or nested blocks do not use newlines in this mode. Note that some // newlines must still appear, such as those following comments or around // here-documents. // // Print's trailing newline when given a *File is not affected by this option. func SingleLine(enabled bool) PrinterOption { return func(p *Printer) { p.singleLine = enabled } } // FunctionNextLine will place a function's opening braces on the next line. func FunctionNextLine(enabled bool) PrinterOption { return func(p *Printer) { p.funcNextLine = enabled } } // NewPrinter allocates a new Printer and applies any number of options. func NewPrinter(opts ...PrinterOption) *Printer { p := &Printer{ bufWriter: bufio.NewWriter(nil), tabWriter: new(tabwriter.Writer), } for _, opt := range opts { opt(p) } return p } // Print "pretty-prints" the given syntax tree node to the given writer. Writes // to w are buffered. // // The node types supported at the moment are *File, *Stmt, *Word, any Command // node, and any WordPart node. A trailing newline will only be printed when a // *File is used. func (p *Printer) Print(w io.Writer, node Node) error { p.reset() if p.minify && p.singleLine { return fmt.Errorf("Minify and SingleLine together are not supported yet; please file an issue describing your use case: https://github.com/mvdan/sh/issues") } // TODO: consider adding a raw mode to skip the tab writer, much like in // go/printer. twmode := tabwriter.DiscardEmptyColumns | tabwriter.StripEscape tabwidth := 8 if p.indentSpaces == 0 { // indenting with tabs twmode |= tabwriter.TabIndent } else { // indenting with spaces tabwidth = int(p.indentSpaces) } p.tabWriter.Init(w, 0, tabwidth, 1, ' ', twmode) w = p.tabWriter p.bufWriter.Reset(w) switch x := node.(type) { case *File: p.stmtList(x.Stmts, x.Last) p.newline(Pos{}) case *Stmt: p.stmtList([]*Stmt{x}, nil) case Command: p.command(x, nil) case *Word: p.line = x.Pos().Line() p.word(x) case WordPart: p.line = x.Pos().Line() p.wordPart(x, nil) default: return fmt.Errorf("unsupported node type: %T", x) } p.flushHeredocs() p.flushComments() // flush the writers if err := p.bufWriter.Flush(); err != nil { return err } if tw, _ := w.(*tabwriter.Writer); tw != nil { if err := tw.Flush(); err != nil { return err } } return nil } type bufWriter interface { Write([]byte) (int, error) WriteString(string) (int, error) WriteByte(byte) error Reset(io.Writer) Flush() error } type colCounter struct { *bufio.Writer column int lineStart bool } func (c *colCounter) addByte(b byte) { switch b { case '\n': c.column = 0 c.lineStart = true case '\t', ' ', tabwriter.Escape: default: c.lineStart = false } c.column++ } func (c *colCounter) WriteByte(b byte) error { c.addByte(b) return c.Writer.WriteByte(b) } func (c *colCounter) WriteString(s string) (int, error) { for _, b := range []byte(s) { c.addByte(b) } return c.Writer.WriteString(s) } func (c *colCounter) Reset(w io.Writer) { c.column = 1 c.lineStart = true c.Writer.Reset(w) } // Printer holds the internal state of the printing mechanism of a // program. type Printer struct { bufWriter tabWriter *tabwriter.Writer cols colCounter indentSpaces uint binNextLine bool swtCaseIndent bool spaceRedirects bool keepPadding bool minify bool singleLine bool funcNextLine bool wantSpace bool // space is wanted or required wantNewline bool // newline is wanted for pretty-printing; ignored by singleLine; ignored by singleLine mustNewline bool // newline is required to keep shell syntax valid wroteSemi bool // wrote ';' for the current statement // pendingComments are any comments in the current line or statement // that we have yet to print. This is useful because that way, we can // ensure that all comments are written immediately before a newline. // Otherwise, in some edge cases we might wrongly place words after a // comment in the same line, breaking programs. pendingComments []Comment // firstLine means we are still writing the first line firstLine bool // line is the current line number line uint // lastLevel is the last level of indentation that was used. lastLevel uint // level is the current level of indentation. level uint // levelIncs records which indentation level increments actually // took place, to revert them once their section ends. levelIncs []bool nestedBinary bool // pendingHdocs is the list of pending heredocs to write. pendingHdocs []*Redirect // used when printing <<- heredocs with tab indentation tabsPrinter *Printer } func (p *Printer) reset() { p.wantSpace, p.wantNewline, p.mustNewline = false, false, false p.pendingComments = p.pendingComments[:0] // minification uses its own newline logic p.firstLine = !p.minify p.line = 0 p.lastLevel, p.level = 0, 0 p.levelIncs = p.levelIncs[:0] p.nestedBinary = false p.pendingHdocs = p.pendingHdocs[:0] } func (p *Printer) spaces(n uint) { for i := uint(0); i < n; i++ { p.WriteByte(' ') } } func (p *Printer) space() { p.WriteByte(' ') p.wantSpace = false } func (p *Printer) spacePad(pos Pos) { if p.cols.lineStart && p.indentSpaces == 0 { // Never add padding at the start of a line unless we are indenting // with spaces, since this may result in mixing of spaces and tabs. return } if p.wantSpace { p.WriteByte(' ') p.wantSpace = false } for p.cols.column > 0 && p.cols.column < int(pos.Col()) { p.WriteByte(' ') } } // wantsNewline reports whether we want to print at least one newline before // printing a node at a given position. A zero position can be given to simply // tell if we want a newline following what's just been printed. func (p *Printer) wantsNewline(pos Pos) bool { if p.mustNewline { // We must have a newline here. return true } if p.singleLine { // The newline is optional, and singleLine turns it off. return false } // THe newline is optional, and we want it via either wantNewline or via // the position's line. return p.wantNewline || pos.Line() > p.line } func (p *Printer) bslashNewl() { if p.wantSpace { p.space() } p.WriteString("\\\n") p.line++ p.indent() } func (p *Printer) spacedString(s string, pos Pos) { p.spacePad(pos) p.WriteString(s) p.wantSpace = true } func (p *Printer) spacedToken(s string, pos Pos) { if p.minify { p.WriteString(s) p.wantSpace = false return } p.spacePad(pos) p.WriteString(s) p.wantSpace = true } func (p *Printer) semiOrNewl(s string, pos Pos) { if p.wantsNewline(Pos{}) { p.newline(pos) p.indent() } else { if !p.wroteSemi { p.WriteByte(';') } if !p.minify { p.space() } p.line = pos.Line() } p.WriteString(s) p.wantSpace = true } func (p *Printer) writeLit(s string) { // If p.tabWriter is nil, this is the nested printer being used to print // <<- heredoc bodies, so the parent printer will add the escape bytes // later. if p.tabWriter != nil && strings.Contains(s, "\t") { p.WriteByte(tabwriter.Escape) defer p.WriteByte(tabwriter.Escape) } p.WriteString(s) } func (p *Printer) incLevel() { inc := false if p.level <= p.lastLevel || len(p.levelIncs) == 0 { p.level++ inc = true } else if last := &p.levelIncs[len(p.levelIncs)-1]; *last { *last = false inc = true } p.levelIncs = append(p.levelIncs, inc) } func (p *Printer) decLevel() { if p.levelIncs[len(p.levelIncs)-1] { p.level-- } p.levelIncs = p.levelIncs[:len(p.levelIncs)-1] } func (p *Printer) indent() { if p.minify { return } p.lastLevel = p.level switch { case p.level == 0: case p.indentSpaces == 0: p.WriteByte(tabwriter.Escape) for i := uint(0); i < p.level; i++ { p.WriteByte('\t') } p.WriteByte(tabwriter.Escape) default: p.spaces(p.indentSpaces * p.level) } } // TODO(mvdan): add an indent call at the end of newline? func (p *Printer) newline(pos Pos) { p.flushHeredocs() p.flushComments() p.WriteByte('\n') p.wantNewline, p.mustNewline, p.wantSpace = false, false, false if p.line < pos.Line() { p.line++ } } func (p *Printer) flushHeredocs() { if len(p.pendingHdocs) == 0 { return } hdocs := p.pendingHdocs p.pendingHdocs = p.pendingHdocs[:0] coms := p.pendingComments p.pendingComments = nil if len(coms) > 0 { c := coms[0] if c.Pos().Line() == p.line { p.pendingComments = append(p.pendingComments, c) p.flushComments() coms = coms[1:] } } // Reuse the last indentation level, as // indentation levels are usually changed before // newlines are printed along with their // subsequent indentation characters. newLevel := p.level p.level = p.lastLevel for _, r := range hdocs { p.line++ p.WriteByte('\n') p.wantNewline, p.wantNewline, p.wantSpace = false, false, false if r.Op == DashHdoc && p.indentSpaces == 0 && !p.minify { if r.Hdoc != nil { extra := extraIndenter{ bufWriter: p.bufWriter, baseIndent: int(p.level + 1), firstIndent: -1, } p.tabsPrinter = &Printer{ bufWriter: &extra, // The options need to persist. indentSpaces: p.indentSpaces, binNextLine: p.binNextLine, swtCaseIndent: p.swtCaseIndent, spaceRedirects: p.spaceRedirects, keepPadding: p.keepPadding, minify: p.minify, funcNextLine: p.funcNextLine, line: r.Hdoc.Pos().Line(), } p.tabsPrinter.wordParts(r.Hdoc.Parts, true) } p.indent() } else if r.Hdoc != nil { p.wordParts(r.Hdoc.Parts, true) } p.unquotedWord(r.Word) if r.Hdoc != nil { // Overwrite p.line, since printing r.Word again can set // p.line to the beginning of the heredoc again. p.line = r.Hdoc.End().Line() } p.wantSpace = false } p.level = newLevel p.pendingComments = coms p.mustNewline = true } func (p *Printer) newlines(pos Pos) { if p.firstLine && len(p.pendingComments) == 0 { p.firstLine = false return // no empty lines at the top } if !p.wantsNewline(pos) { return } p.newline(pos) if pos.Line() > p.line { if !p.minify { // preserve single empty lines p.WriteByte('\n') } p.line++ } p.indent() } func (p *Printer) rightParen(pos Pos) { if !p.minify { p.newlines(pos) } p.WriteByte(')') p.wantSpace = true } func (p *Printer) semiRsrv(s string, pos Pos) { if p.wantsNewline(pos) { p.newlines(pos) } else { if !p.wroteSemi { p.WriteByte(';') } if !p.minify { p.spacePad(pos) } } p.WriteString(s) p.wantSpace = true } func (p *Printer) flushComments() { for i, c := range p.pendingComments { if i == 0 { // Flush any pending heredocs first. Otherwise, the // comments would become part of a heredoc body. p.flushHeredocs() } p.firstLine = false // We can't call any of the newline methods, as they call this // function and we'd recurse forever. cline := c.Hash.Line() switch { case p.mustNewline, i > 0, cline > p.line && p.line > 0: p.WriteByte('\n') if cline > p.line+1 { p.WriteByte('\n') } p.indent() p.wantSpace = false p.spacePad(c.Pos()) case p.wantSpace: if p.keepPadding { p.spacePad(c.Pos()) } else { p.WriteByte('\t') } } // don't go back one line, which may happen in some edge cases if p.line < cline { p.line = cline } p.WriteByte('#') p.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace)) p.wantNewline = true p.mustNewline = true } p.pendingComments = nil } func (p *Printer) comments(comments ...Comment) { if p.minify { return } p.pendingComments = append(p.pendingComments, comments...) } func (p *Printer) wordParts(wps []WordPart, quoted bool) { for i, wp := range wps { var next WordPart if i+1 < len(wps) { next = wps[i+1] } // Can't use p.wantsNewline here, since this is only about // escaped newlines. for !p.singleLine && wp.Pos().Line() > p.line { if quoted { // No extra spacing or indentation if quoted. p.WriteString("\\\n") p.line++ } else { p.bslashNewl() } } p.wordPart(wp, next) p.line = wp.End().Line() } } func (p *Printer) wordPart(wp, next WordPart) { switch x := wp.(type) { case *Lit: p.writeLit(x.Value) case *SglQuoted: if x.Dollar { p.WriteByte('$') } p.WriteByte('\'') p.writeLit(x.Value) p.WriteByte('\'') p.line = x.End().Line() case *DblQuoted: p.dblQuoted(x) case *CmdSubst: p.line = x.Pos().Line() switch { case x.TempFile: p.WriteString("${") p.wantSpace = true p.nestedStmts(x.Stmts, x.Last, x.Right) p.wantSpace = false p.semiRsrv("}", x.Right) case x.ReplyVar: p.WriteString("${|") p.nestedStmts(x.Stmts, x.Last, x.Right) p.wantSpace = false p.semiRsrv("}", x.Right) default: p.WriteString("$(") p.wantSpace = len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) p.nestedStmts(x.Stmts, x.Last, x.Right) p.rightParen(x.Right) } case *ParamExp: litCont := ";" if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" { litCont = nextLit.Value[:1] } name := x.Param.Value switch { case !p.minify: case x.Excl, x.Length, x.Width: case x.Index != nil, x.Slice != nil: case x.Repl != nil, x.Exp != nil: case len(name) > 1 && !ValidName(name): // ${10} case ValidName(name + litCont): // ${var}cont default: x2 := *x x2.Short = true p.paramExp(&x2) return } p.paramExp(x) case *ArithmExp: p.WriteString("$((") if x.Unsigned { p.WriteString("# ") } p.arithmExpr(x.X, false, false) p.WriteString("))") case *ExtGlob: p.WriteString(x.Op.String()) p.writeLit(x.Pattern.Value) p.WriteByte(')') case *ProcSubst: // avoid conflict with << and others if p.wantSpace { p.space() } p.WriteString(x.Op.String()) p.nestedStmts(x.Stmts, x.Last, x.Rparen) p.rightParen(x.Rparen) } } func (p *Printer) dblQuoted(dq *DblQuoted) { if dq.Dollar { p.WriteByte('$') } p.WriteByte('"') if len(dq.Parts) > 0 { p.wordParts(dq.Parts, true) } // Add any trailing escaped newlines. for p.line < dq.Right.Line() { p.WriteString("\\\n") p.line++ } p.WriteByte('"') } func (p *Printer) wroteIndex(index ArithmExpr) bool { if index == nil { return false } p.WriteByte('[') p.arithmExpr(index, false, false) p.WriteByte(']') return true } func (p *Printer) paramExp(pe *ParamExp) { if pe.nakedIndex() { // arr[x] p.writeLit(pe.Param.Value) p.wroteIndex(pe.Index) return } if pe.Short { // $var p.WriteByte('$') p.writeLit(pe.Param.Value) return } // ${var...} p.WriteString("${") switch { case pe.Length: p.WriteByte('#') case pe.Width: p.WriteByte('%') case pe.Excl: p.WriteByte('!') } p.writeLit(pe.Param.Value) p.wroteIndex(pe.Index) switch { case pe.Slice != nil: p.WriteByte(':') p.arithmExpr(pe.Slice.Offset, true, true) if pe.Slice.Length != nil { p.WriteByte(':') p.arithmExpr(pe.Slice.Length, true, false) } case pe.Repl != nil: if pe.Repl.All { p.WriteByte('/') } p.WriteByte('/') if pe.Repl.Orig != nil { p.word(pe.Repl.Orig) } p.WriteByte('/') if pe.Repl.With != nil { p.word(pe.Repl.With) } case pe.Names != 0: p.writeLit(pe.Names.String()) case pe.Exp != nil: p.WriteString(pe.Exp.Op.String()) if pe.Exp.Word != nil { p.word(pe.Exp.Word) } } p.WriteByte('}') } func (p *Printer) loop(loop Loop) { switch x := loop.(type) { case *WordIter: p.writeLit(x.Name.Value) if x.InPos.IsValid() { p.spacedString(" in", Pos{}) p.wordJoin(x.Items) } case *CStyleLoop: p.WriteString("((") if x.Init == nil { p.space() } p.arithmExpr(x.Init, false, false) p.WriteString("; ") p.arithmExpr(x.Cond, false, false) p.WriteString("; ") p.arithmExpr(x.Post, false, false) p.WriteString("))") } } func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) { if p.minify { compact = true } switch x := expr.(type) { case *Word: p.word(x) case *BinaryArithm: if compact { p.arithmExpr(x.X, compact, spacePlusMinus) p.WriteString(x.Op.String()) p.arithmExpr(x.Y, compact, false) } else { p.arithmExpr(x.X, compact, spacePlusMinus) if x.Op != Comma { p.space() } p.WriteString(x.Op.String()) p.space() p.arithmExpr(x.Y, compact, false) } case *UnaryArithm: if x.Post { p.arithmExpr(x.X, compact, spacePlusMinus) p.WriteString(x.Op.String()) } else { if spacePlusMinus { switch x.Op { case Plus, Minus: p.space() } } p.WriteString(x.Op.String()) p.arithmExpr(x.X, compact, false) } case *ParenArithm: p.WriteByte('(') p.arithmExpr(x.X, false, false) p.WriteByte(')') } } func (p *Printer) testExpr(expr TestExpr) { // Multi-line test expressions don't need to escape newlines. if expr.Pos().Line() > p.line { p.newlines(expr.Pos()) p.spacePad(expr.Pos()) } else if p.wantSpace { p.space() } p.testExprSameLine(expr) } func (p *Printer) testExprSameLine(expr TestExpr) { p.line = expr.Pos().Line() switch x := expr.(type) { case *Word: p.word(x) case *BinaryTest: p.testExprSameLine(x.X) p.space() p.WriteString(x.Op.String()) switch x.Op { case AndTest, OrTest: p.wantSpace = true p.testExpr(x.Y) default: p.space() p.testExprSameLine(x.Y) } case *UnaryTest: p.WriteString(x.Op.String()) p.space() p.testExprSameLine(x.X) case *ParenTest: p.WriteByte('(') p.wantSpace = startsWithLparen(x.X) p.testExpr(x.X) p.WriteByte(')') } } func (p *Printer) word(w *Word) { p.wordParts(w.Parts, false) p.wantSpace = true } func (p *Printer) unquotedWord(w *Word) { for _, wp := range w.Parts { switch x := wp.(type) { case *SglQuoted: p.writeLit(x.Value) case *DblQuoted: p.wordParts(x.Parts, true) case *Lit: for i := 0; i < len(x.Value); i++ { if b := x.Value[i]; b == '\\' { if i++; i < len(x.Value) { p.WriteByte(x.Value[i]) } } else { p.WriteByte(b) } } } } } func (p *Printer) wordJoin(ws []*Word) { anyNewline := false for _, w := range ws { if pos := w.Pos(); pos.Line() > p.line && !p.singleLine { if !anyNewline { p.incLevel() anyNewline = true } p.bslashNewl() } p.spacePad(w.Pos()) p.word(w) } if anyNewline { p.decLevel() } } func (p *Printer) casePatternJoin(pats []*Word) { anyNewline := false for i, w := range pats { if i > 0 { p.spacedToken("|", Pos{}) } if p.wantsNewline(w.Pos()) { if !anyNewline { p.incLevel() anyNewline = true } p.bslashNewl() } else { p.spacePad(w.Pos()) } p.word(w) } if anyNewline { p.decLevel() } } func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) { p.incLevel() for _, el := range elems { var left []Comment for _, c := range el.Comments { if c.Pos().After(el.Pos()) { left = append(left, c) break } p.comments(c) } // Multi-line array expressions don't need to escape newlines. if el.Pos().Line() > p.line { p.newlines(el.Pos()) p.spacePad(el.Pos()) } else if p.wantSpace { p.space() } if p.wroteIndex(el.Index) { p.WriteByte('=') } if el.Value != nil { p.word(el.Value) } p.comments(left...) } if len(last) > 0 { p.comments(last...) p.flushComments() } p.decLevel() } func (p *Printer) stmt(s *Stmt) { p.wroteSemi = false if s.Negated { p.spacedString("!", s.Pos()) } var startRedirs int if s.Cmd != nil { startRedirs = p.command(s.Cmd, s.Redirs) } p.incLevel() for _, r := range s.Redirs[startRedirs:] { if p.wantsNewline(r.OpPos) { p.bslashNewl() } if p.wantSpace { p.spacePad(r.Pos()) } if r.N != nil { p.writeLit(r.N.Value) } p.WriteString(r.Op.String()) if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { p.space() } else { p.wantSpace = true } p.word(r.Word) if r.Op == Hdoc || r.Op == DashHdoc { p.pendingHdocs = append(p.pendingHdocs, r) } } sep := s.Semicolon.IsValid() && s.Semicolon.Line() > p.line && !p.singleLine if sep || s.Background || s.Coprocess { if sep { p.bslashNewl() } else if !p.minify { p.space() } if s.Background { p.WriteString("&") } else if s.Coprocess { p.WriteString("|&") } else { p.WriteString(";") } p.wroteSemi = true p.wantSpace = true } p.decLevel() } func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.line = cmd.Pos().Line() p.spacePad(cmd.Pos()) switch x := cmd.(type) { case *CallExpr: p.assigns(x.Assigns) if len(x.Args) <= 1 { p.wordJoin(x.Args) return 0 } p.wordJoin(x.Args[:1]) for _, r := range redirs { if r.Pos().After(x.Args[1].Pos()) || r.Op == Hdoc || r.Op == DashHdoc { break } if p.wantSpace { p.spacePad(r.Pos()) } if r.N != nil { p.writeLit(r.N.Value) } p.WriteString(r.Op.String()) if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { p.space() } else { p.wantSpace = true } p.word(r.Word) startRedirs++ } p.wordJoin(x.Args[1:]) case *Block: p.WriteByte('{') p.wantSpace = true // Forbid "foo()\n{ bar; }" p.wantNewline = p.wantNewline || p.funcNextLine p.nestedStmts(x.Stmts, x.Last, x.Rbrace) p.semiRsrv("}", x.Rbrace) case *IfClause: p.ifClause(x, false) case *Subshell: p.WriteByte('(') p.wantSpace = len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) p.spacePad(stmtsPos(x.Stmts, x.Last)) p.nestedStmts(x.Stmts, x.Last, x.Rparen) p.wantSpace = false p.spacePad(x.Rparen) p.rightParen(x.Rparen) case *WhileClause: if x.Until { p.spacedString("until", x.Pos()) } else { p.spacedString("while", x.Pos()) } p.nestedStmts(x.Cond, x.CondLast, Pos{}) p.semiOrNewl("do", x.DoPos) p.nestedStmts(x.Do, x.DoLast, x.DonePos) p.semiRsrv("done", x.DonePos) case *ForClause: if x.Select { p.WriteString("select ") } else { p.WriteString("for ") } p.loop(x.Loop) p.semiOrNewl("do", x.DoPos) p.nestedStmts(x.Do, x.DoLast, x.DonePos) p.semiRsrv("done", x.DonePos) case *BinaryCmd: p.stmt(x.X) if p.minify || p.singleLine || x.Y.Pos().Line() <= p.line { // leave p.nestedBinary untouched p.spacedToken(x.Op.String(), x.OpPos) p.line = x.Y.Pos().Line() p.stmt(x.Y) break } indent := !p.nestedBinary if indent { p.incLevel() } if p.binNextLine { if len(p.pendingHdocs) == 0 { p.bslashNewl() } p.spacedToken(x.Op.String(), x.OpPos) if len(x.Y.Comments) > 0 { p.wantSpace = false p.newline(x.Y.Pos()) p.indent() p.comments(x.Y.Comments...) p.newline(Pos{}) p.indent() } } else { p.spacedToken(x.Op.String(), x.OpPos) p.line = x.OpPos.Line() p.comments(x.Y.Comments...) p.newline(Pos{}) p.indent() } p.line = x.Y.Pos().Line() _, p.nestedBinary = x.Y.Cmd.(*BinaryCmd) p.stmt(x.Y) if indent { p.decLevel() } p.nestedBinary = false case *FuncDecl: if x.RsrvWord { p.WriteString("function ") } p.writeLit(x.Name.Value) if !x.RsrvWord || x.Parens { p.WriteString("()") } if p.funcNextLine { p.newline(Pos{}) p.indent() } else if !x.Parens || !p.minify { p.space() } p.line = x.Body.Pos().Line() p.comments(x.Body.Comments...) p.stmt(x.Body) case *CaseClause: p.WriteString("case ") p.word(x.Word) p.WriteString(" in") p.wantSpace = true if p.swtCaseIndent { p.incLevel() } if len(x.Items) == 0 { // Apparently "case x in; esac" is invalid shell. p.mustNewline = true } for i, ci := range x.Items { var last []Comment for i, c := range ci.Comments { if c.Pos().After(ci.Pos()) { last = ci.Comments[i:] break } p.comments(c) } p.newlines(ci.Pos()) p.spacePad(ci.Pos()) p.casePatternJoin(ci.Patterns) p.WriteByte(')') p.wantSpace = !p.minify bodyPos := stmtsPos(ci.Stmts, ci.Last) bodyEnd := stmtsEnd(ci.Stmts, ci.Last) sep := len(ci.Stmts) > 1 || bodyPos.Line() > p.line || (bodyEnd.IsValid() && ci.OpPos.Line() > bodyEnd.Line()) p.nestedStmts(ci.Stmts, ci.Last, ci.OpPos) p.level++ if !p.minify || i != len(x.Items)-1 { if sep { p.newlines(ci.OpPos) p.wantNewline = true } p.spacedToken(ci.Op.String(), ci.OpPos) // avoid ; directly after tokens like ;; p.wroteSemi = true } p.comments(last...) p.flushComments() p.level-- } p.comments(x.Last...) if p.swtCaseIndent { p.flushComments() p.decLevel() } p.semiRsrv("esac", x.Esac) case *ArithmCmd: p.WriteString("((") if x.Unsigned { p.WriteString("# ") } p.arithmExpr(x.X, false, false) p.WriteString("))") case *TestClause: p.WriteString("[[ ") p.incLevel() p.testExpr(x.X) p.decLevel() p.spacedString("]]", x.Right) case *DeclClause: p.spacedString(x.Variant.Value, x.Pos()) p.assigns(x.Args) case *TimeClause: p.spacedString("time", x.Pos()) if x.PosixFormat { p.spacedString("-p", x.Pos()) } if x.Stmt != nil { p.stmt(x.Stmt) } case *CoprocClause: p.spacedString("coproc", x.Pos()) if x.Name != nil { p.space() p.word(x.Name) } p.space() p.stmt(x.Stmt) case *LetClause: p.spacedString("let", x.Pos()) for _, n := range x.Exprs { p.space() p.arithmExpr(n, true, false) } case *TestDecl: p.spacedString("@test", x.Pos()) p.space() p.word(x.Description) p.space() p.stmt(x.Body) default: panic(fmt.Sprintf("syntax.Printer: unexpected node type %T", x)) } return startRedirs } func (p *Printer) ifClause(ic *IfClause, elif bool) { if !elif { p.spacedString("if", ic.Pos()) } p.nestedStmts(ic.Cond, ic.CondLast, Pos{}) p.semiOrNewl("then", ic.ThenPos) thenEnd := ic.FiPos el := ic.Else if el != nil { thenEnd = el.Position } p.nestedStmts(ic.Then, ic.ThenLast, thenEnd) if el != nil && el.ThenPos.IsValid() { p.comments(ic.Last...) p.semiRsrv("elif", el.Position) p.ifClause(el, true) return } if el == nil { p.comments(ic.Last...) } else { var left []Comment for _, c := range ic.Last { if c.Pos().After(el.Position) { left = append(left, c) break } p.comments(c) } p.semiRsrv("else", el.Position) p.comments(left...) p.nestedStmts(el.Then, el.ThenLast, ic.FiPos) p.comments(el.Last...) } p.semiRsrv("fi", ic.FiPos) } func startsWithLparen(node Node) bool { switch node := node.(type) { case *Stmt: return startsWithLparen(node.Cmd) case *BinaryCmd: return startsWithLparen(node.X) case *Subshell: return true // keep ( ( case *ArithmCmd: return true // keep ( (( } return false } func (p *Printer) stmtList(stmts []*Stmt, last []Comment) { sep := p.wantNewline || (len(stmts) > 0 && stmts[0].Pos().Line() > p.line) for i, s := range stmts { if i > 0 && p.singleLine && p.wantNewline && !p.wroteSemi { // In singleLine mode, ensure we use semicolons between // statements. p.WriteByte(';') p.wantSpace = true } pos := s.Pos() var midComs, endComs []Comment for _, c := range s.Comments { // Comments after the end of this command. Note that // this includes "< 1: // Force a newline if we find: // { stmt; stmt; } p.wantNewline = true case closing.Line() > p.line && len(stmts) > 0 && stmtsEnd(stmts, last).Line() < closing.Line(): // Force a newline if we find: // { stmt // } p.wantNewline = true case len(p.pendingComments) > 0 && len(stmts) > 0: // Force a newline if we find: // for i in a b # stmt // do foo; done p.wantNewline = true } p.stmtList(stmts, last) if closing.IsValid() { p.flushComments() } p.decLevel() } func (p *Printer) assigns(assigns []*Assign) { p.incLevel() for _, a := range assigns { if p.wantsNewline(a.Pos()) { p.bslashNewl() } else { p.spacePad(a.Pos()) } if a.Name != nil { p.writeLit(a.Name.Value) p.wroteIndex(a.Index) if a.Append { p.WriteByte('+') } if !a.Naked { p.WriteByte('=') } } if a.Value != nil { // Ensure we don't use an escaped newline after '=', // because that can result in indentation, thus // splitting "foo=bar" into "foo= bar". p.line = a.Value.Pos().Line() p.word(a.Value) } else if a.Array != nil { p.wantSpace = false p.WriteByte('(') p.elemJoin(a.Array.Elems, a.Array.Last) p.rightParen(a.Array.Rparen) } p.wantSpace = true } p.decLevel() } sh-3.4.3/syntax/printer_test.go000066400000000000000000000736011420425111700165360ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "bytes" "fmt" "io" "os" "regexp" "strings" "testing" ) func TestPrintCompact(t *testing.T) { t.Parallel() parserBash := NewParser(KeepComments(true)) parserPosix := NewParser(KeepComments(true), Variant(LangPOSIX)) parserMirBSD := NewParser(KeepComments(true), Variant(LangMirBSDKorn)) parserBats := NewParser(KeepComments(true), Variant(LangBats)) printer := NewPrinter() for _, c := range fileTests { t.Run("", func(t *testing.T) { in := c.Strs[0] parser := parserPosix if c.Bats != nil { parser = parserBats } else if c.Bash != nil { parser = parserBash } else if c.MirBSDKorn != nil { parser = parserMirBSD } printTest(t, parser, printer, in, in) }) } } func strPrint(p *Printer, node Node) (string, error) { var buf bytes.Buffer err := p.Print(&buf, node) return buf.String(), err } type printCase struct { in, want string } func samePrint(s string) printCase { return printCase{in: s, want: s} } var printTests = []printCase{ samePrint(`fo○ b\år`), samePrint(`"fo○ b\år"`), samePrint(`'fo○ b\år'`), samePrint(`${a#fo○ b\år}`), samePrint(`#fo○ b\år`), samePrint("<(a)", ">(a)"}, // escaped newline at end of chunk {"a" + strings.Repeat(" ", bufSize-2) + "\\\nb", "a \\\n\tb"}, // panics if padding is only 4 (utf8.UTFMax) {strings.Repeat(" ", bufSize-10) + "${a/b//○}", "${a/b//○}"}, // multiple p.fill calls {"a" + strings.Repeat(" ", bufSize*4) + "b", "a b"}, // newline at the beginning of second chunk {"a" + strings.Repeat(" ", bufSize-2) + "\nb", "a\nb"}, {"foo; bar", "foo\nbar"}, {"foo\n\n\nbar", "foo\n\nbar"}, {"foo\n\n", "foo"}, {"\n\nfoo", "foo"}, {"# foo \n # bar\t", "# foo\n# bar"}, samePrint("#"), samePrint("#c1\\\n#c2"), samePrint("#\\\n#"), samePrint("{\n\t# foo \\\n}"), samePrint("foo\\\\\nbar"), samePrint("a=b # inline\nbar"), samePrint("a=$(b) # inline"), samePrint("foo # inline\n# after"), samePrint("$(a) $(b)"), {"if a\nthen\n\tb\nfi", "if a; then\n\tb\nfi"}, samePrint("if a; then\n\tb\nelse\nfi"), {"if a; then b\nelse c\nfi", "if a; then\n\tb\nelse\n\tc\nfi"}, samePrint("foo >&2 &2 bar &2 bar &2 bar bar2 bar\netc", "foo \\\n\t>bar\netc", }, { "foo \\\nfoo2 \\\n>bar", "foo \\\n\tfoo2 \\\n\t>bar", }, samePrint("> >(foo)"), samePrint("x > >(foo) y"), samePrint("a | () |\n\tb"), samePrint("a | (\n\tx\n\ty\n) |\n\tb"), samePrint("a |\n\tif foo; then\n\t\tbar\n\tfi |\n\tb"), samePrint("a | if foo; then\n\tbar\nfi"), samePrint("a | b | if foo; then\n\tbar\nfi"), { "case $i in\n1)\nfoo\n;;\nesac", "case $i in\n1)\n\tfoo\n\t;;\nesac", }, { "case $i in\n1)\nfoo\nesac", "case $i in\n1)\n\tfoo\n\t;;\nesac", }, { "case $i in\n1) foo\nesac", "case $i in\n1) foo ;;\nesac", }, { "case $i in\n1) foo; bar\nesac", "case $i in\n1)\n\tfoo\n\tbar\n\t;;\nesac", }, { "case $i in\n1) foo; bar;;\nesac", "case $i in\n1)\n\tfoo\n\tbar\n\t;;\nesac", }, { "case $i in\n1)\n#foo \t\n;;\nesac", "case $i in\n1)\n\t#foo\n\t;;\nesac", }, samePrint("case $i in\n1)\n\ta\n\t#b\n\t;;\nesac"), samePrint("case $i in\n1) foo() { bar; } ;;\nesac"), samePrint("case $i in\n1) ;; #foo\nesac"), samePrint("case $i in\n#foo\nesac"), samePrint("case $i in\n#before\n1) ;;\nesac"), samePrint("case $i in\n#bef\n1) ;; #inl\nesac"), samePrint("case $i in\n1) ;; #inl1\n2) ;; #inl2\nesac"), samePrint("case $i in\n#bef\n1) #inl\n\tfoo\n\t;;\nesac"), samePrint("case $i in\n1) #inl\n\t;;\nesac"), samePrint("case $i in\n1) a \\\n\tb ;;\nesac"), samePrint("case $i in\n1 | 2 | \\\n\t3 | 4) a b ;;\nesac"), samePrint("case $i in\n1 | 2 | \\\n\t3 | 4)\n\ta b\n\t;;\nesac"), samePrint("case $i in\nx) ;;\ny) for n in 1; do echo $n; done ;;\nesac"), samePrint("case a in b) [[ x =~ y ]] ;; esac"), samePrint("case a in b) [[ a =~ b$ || c =~ d$ ]] ;; esac"), samePrint("case a in b) [[ a =~ (b) ]] ;; esac"), samePrint("[[ (a =~ b$) ]]"), samePrint("[[ a && ((b || c) && d) ]]"), samePrint("[[ a &&\n\tb ]]"), samePrint("[[ a ||\n\tb ]]"), { "[[ -f \\\n\tfoo ]]", "[[ -f foo ]]", }, { "[[ foo \\\n\t-ef \\\n\tbar ]]", "[[ foo -ef bar ]]", }, { "[[ a && \\\nb \\\n && c ]]", "[[ a &&\n\tb &&\n\tc ]]", }, samePrint("{\n\t[[ a || b ]]\n}"), { "a=(\nb\nc\n) b=c", "a=(\n\tb\n\tc\n) b=c", }, samePrint("a=(\n\t#before\n\tb #inline\n)"), samePrint("a=(\n\tb #foo\n\tc #bar\n)"), samePrint("a=(\n\tb\n\n\t#foo\n\t#bar\n\tc\n)"), samePrint("a=(\n\t#foo\n\t#bar\n\tc\n)"), samePrint("a=(\n\t#lone\n)"), samePrint("a=(\n\n)"), samePrint("a=(\n\tx\n\n\ty\n)"), samePrint("foo <redir"), { ">redir \\\n\tfoo", "foo >redir", }, samePrint("$(declare)"), { "`declare`", "$(declare)", }, } func TestPrintWeirdFormat(t *testing.T) { t.Parallel() parser := NewParser(KeepComments(true)) printer := NewPrinter() for i, tc := range printTests { t.Run(fmt.Sprintf("#%03d", i), func(t *testing.T) { printTest(t, parser, printer, tc.in, tc.want) }) t.Run(fmt.Sprintf("#%03d-nl", i), func(t *testing.T) { printTest(t, parser, printer, "\n"+tc.in+"\n", tc.want) }) t.Run(fmt.Sprintf("#%03d-redo", i), func(t *testing.T) { printTest(t, parser, printer, tc.want, tc.want) }) } } func parsePath(tb testing.TB, path string) *File { f, err := os.Open(path) if err != nil { tb.Fatal(err) } defer f.Close() prog, err := NewParser(KeepComments(true)).Parse(f, "") if err != nil { tb.Fatal(err) } return prog } const canonicalPath = "canonical.sh" func TestPrintMultiline(t *testing.T) { t.Parallel() prog := parsePath(t, canonicalPath) got, err := strPrint(NewPrinter(), prog) if err != nil { t.Fatal(err) } wantBs, err := os.ReadFile(canonicalPath) if err != nil { t.Fatal(err) } // If we're on Windows and it was set up to automatically replace LF // with CRLF, that might make this test fail. Just ignore \r characters. want := strings.ReplaceAll(string(wantBs), "\r", "") got = strings.ReplaceAll(got, "\r", "") if got != want { t.Fatalf("Print mismatch in canonical.sh") } } func BenchmarkPrint(b *testing.B) { b.ReportAllocs() prog := parsePath(b, canonicalPath) printer := NewPrinter() for i := 0; i < b.N; i++ { if err := printer.Print(io.Discard, prog); err != nil { b.Fatal(err) } } } func TestPrintSpaces(t *testing.T) { t.Parallel() spaceFormats := [...]struct { spaces uint in, want string }{ { 0, "{\nfoo \\\nbar\n}", "{\n\tfoo \\\n\t\tbar\n}", }, { 2, "{\nfoo \\\nbar\n}", "{\n foo \\\n bar\n}", }, { 4, "{\nfoo \\\nbar\n}", "{\n foo \\\n bar\n}", }, { 2, "if foo; then # inline1\nbar # inline2\n# withfi\nfi", "if foo; then # inline1\n bar # inline2\n# withfi\nfi", }, { 2, "array=('one'\n # 'two'\n 'three')", "array=('one'\n # 'two'\n 'three')", }, } parser := NewParser(KeepComments(true)) for _, tc := range spaceFormats { t.Run("", func(t *testing.T) { printer := NewPrinter(Indent(tc.spaces)) printTest(t, parser, printer, tc.in, tc.want) }) } } var errBadWriter = fmt.Errorf("write: expected error") type badWriter struct{} func (b badWriter) Write(p []byte) (int, error) { return 0, errBadWriter } func TestWriteErr(t *testing.T) { t.Parallel() f := &File{Stmts: []*Stmt{ { Redirs: []*Redirect{{ Op: RdrOut, Word: litWord("foo"), }}, Cmd: &Subshell{}, }, }} err := NewPrinter().Print(badWriter{}, f) if err == nil { t.Fatalf("Expected error with bad writer") } if err != errBadWriter { t.Fatalf("Error mismatch with bad writer:\nwant: %v\ngot: %v", errBadWriter, err) } } func TestPrintBinaryNextLine(t *testing.T) { t.Parallel() tests := [...]printCase{ { "foo < f"), samePrint("echo > f foo bar"), samePrint("echo >(cmd)"), samePrint("echo > >(cmd)"), samePrint("<< EOF\nfoo\nEOF"), samePrint("<<- EOF\n\t$(< foo)\nEOF"), samePrint("echo 2> f"), samePrint("echo foo bar >&1"), samePrint("echo 2<&1 foo bar"), } parser := NewParser(KeepComments(true)) printer := NewPrinter(SpaceRedirects(true)) for _, tc := range tests { t.Run("", func(t *testing.T) { printTest(t, parser, printer, tc.in, tc.want) }) } } func TestPrintKeepPadding(t *testing.T) { t.Parallel() tests := [...]printCase{ samePrint("echo foo bar"), samePrint("echo foo bar"), samePrint("a=b c=d bar"), samePrint("echo foo >bar"), samePrint("echo foo 2>bar"), samePrint("{ foo; }"), samePrint("a() { foo; }"), samePrint("a && b"), samePrint("a | b"), samePrint("a | b"), samePrint("{ a b c; }"), samePrint("foo # x\nbaaar # y"), samePrint("{ { a; }; }"), samePrint("{ a; }"), samePrint("( a )"), samePrint("'foo\nbar' # x"), {"\tfoo", "foo"}, {" if foo; then bar; fi", "if foo; then bar; fi"}, samePrint("echo '★' || true"), { "1234 || { x; y; }", "1234 || {\n\tx\n\ty\n}", }, { "array=('one'\n\t\t# 'two'\n\t\t'three')", "array=('one'\n\t# 'two'\n\t'three')", }, } parser := NewParser(KeepComments(true)) printer := NewPrinter(KeepPadding(true)) for _, tc := range tests { t.Run("", func(t *testing.T) { // ensure that Reset does properly reset colCounter printer.WriteByte('x') printer.Reset(nil) printTest(t, parser, printer, tc.in, tc.want) }) } } func TestPrintKeepPaddingSpaces(t *testing.T) { t.Parallel() tests := [...]printCase{ samePrint("array=('one'\n # 'two'\n 'three')"), samePrint(" abc=123"), samePrint("foo \\\n bar \\\n baz"), samePrint("{\n foo\n bar\n}"), samePrint("# foo\n # bar"), } parser := NewParser(KeepComments(true)) printer := NewPrinter(KeepPadding(true), Indent(2)) for _, tc := range tests { t.Run("", func(t *testing.T) { printTest(t, parser, printer, tc.in, tc.want) }) } } func TestPrintMinify(t *testing.T) { t.Parallel() tests := [...]printCase{ samePrint("echo foo bar $a $(b)"), { "#comment", "", }, { "foo #comment", "foo", }, { "foo\n\nbar", "foo\nbar", }, { "foo &", "foo&", }, samePrint("foo >bar 2>baz bar 2>baz bar 2>baz b", "a >b", }, samePrint("foo >bar 2>baz // See LICENSE for licensing information package syntax import ( "fmt" "strings" "unicode" "unicode/utf8" ) type QuoteError struct { ByteOffset int Message string } func (e QuoteError) Error() string { return fmt.Sprintf("cannot quote character at byte %d: %s", e.ByteOffset, e.Message) } const ( quoteErrNull = "shell strings cannot contain null bytes" quoteErrPOSIX = "POSIX shell lacks escape sequences" quoteErrRange = "rune out of range" quoteErrMksh = "mksh cannot escape codepoints above 16 bits" ) // Quote returns a quoted version of the input string, // so that the quoted version is expanded or interpreted // as the original string in the given language variant. // // Quoting is necessary when using arbitrary literal strings // as words in a shell script or command. // Without quoting, one can run into syntax errors, // as well as the possibility of running unintended code. // // An error is returned when a string cannot be quoted for a variant. // For instance, POSIX lacks escape sequences for non-printable characters, // and no language variant can represent a string containing null bytes. // In such cases, the returned error type will be *QuoteError. // // The quoting strategy is chosen on a best-effort basis, // to minimize the amount of extra bytes necessary. // // Some strings do not require any quoting and are returned unchanged. // Those strings can be directly surrounded in single quotes as well. func Quote(s string, lang LangVariant) (string, error) { if s == "" { // Special case; an empty string must always be quoted, // as otherwise it expands to zero fields. return "''", nil } shellChars := false nonPrintable := false offs := 0 for rem := s; len(rem) > 0; { r, size := utf8.DecodeRuneInString(rem) switch r { // Like regOps; token characters. case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`', // Whitespace; might result in multiple fields. ' ', '\t', '\r', '\n', // Escape sequences would be expanded. '\\', // Would start a comment unless quoted. '#', // Might result in brace expansion. '{', // Might result in tilde expansion. '~', // Might result in globbing. '*', '?', '[', // Might result in an assignment. '=': shellChars = true case '\x00': return "", &QuoteError{ByteOffset: offs, Message: quoteErrNull} } if r == utf8.RuneError || !unicode.IsPrint(r) { if lang == LangPOSIX { return "", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX} } nonPrintable = true } rem = rem[size:] offs += size } if !shellChars && !nonPrintable && !IsKeyword(s) { // Nothing to quote; avoid allocating. return s, nil } // Single quotes are usually best, // as they don't require any escaping of characters. // If we have any invalid utf8 or non-printable runes, // use $'' so that we can escape them. // Note that we can't use double quotes for those. var b strings.Builder if nonPrintable { b.WriteString("$'") lastRequoteIfHex := false offs := 0 for rem := s; len(rem) > 0; { nextRequoteIfHex := false r, size := utf8.DecodeRuneInString(rem) switch { case r == '\'', r == '\\': b.WriteByte('\\') b.WriteRune(r) case unicode.IsPrint(r) && r != utf8.RuneError: if lastRequoteIfHex && isHex(r) { b.WriteString("'$'") } b.WriteRune(r) case r == '\a': b.WriteString(`\a`) case r == '\b': b.WriteString(`\b`) case r == '\f': b.WriteString(`\f`) case r == '\n': b.WriteString(`\n`) case r == '\r': b.WriteString(`\r`) case r == '\t': b.WriteString(`\t`) case r == '\v': b.WriteString(`\v`) case r < utf8.RuneSelf, r == utf8.RuneError && size == 1: // \xXX, fixed at two hexadecimal characters. fmt.Fprintf(&b, "\\x%02x", rem[0]) // Unfortunately, mksh allows \x to consume more hex characters. // Ensure that we don't allow it to read more than two. if lang == LangMirBSDKorn { nextRequoteIfHex = true } case r > utf8.MaxRune: // Not a valid Unicode code point? return "", &QuoteError{ByteOffset: offs, Message: quoteErrRange} case lang == LangMirBSDKorn && r > 0xFFFD: // From the CAVEATS section in R59's man page: // // mksh currently uses OPTU-16 internally, which is the same as // UTF-8 and CESU-8 with 0000..FFFD being valid codepoints. return "", &QuoteError{ByteOffset: offs, Message: quoteErrMksh} case r < 0x10000: // \uXXXX, fixed at four hexadecimal characters. fmt.Fprintf(&b, "\\u%04x", r) default: // \UXXXXXXXX, fixed at eight hexadecimal characters. fmt.Fprintf(&b, "\\U%08x", r) } rem = rem[size:] lastRequoteIfHex = nextRequoteIfHex offs += size } b.WriteString("'") return b.String(), nil } // Single quotes without any need for escaping. if !strings.Contains(s, "'") { return "'" + s + "'", nil } // The string contains single quotes, // so fall back to double quotes. b.WriteByte('"') for _, r := range s { switch r { case '"', '\\', '`', '$': b.WriteByte('\\') } b.WriteRune(r) } b.WriteByte('"') return b.String(), nil } func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') } sh-3.4.3/syntax/quote_test.go000066400000000000000000000025311420425111700162020ustar00rootroot00000000000000// Copyright (c) 2021, Daniel Martí // See LICENSE for licensing information package syntax import ( "testing" qt "github.com/frankban/quicktest" ) func TestQuote(t *testing.T) { t.Parallel() tests := [...]struct { str string lang LangVariant want interface{} }{ {"", LangBash, `''`}, {"\a", LangBash, `$'\a'`}, {"\b", LangBash, `$'\b'`}, {"\f", LangBash, `$'\f'`}, {"\n", LangBash, `$'\n'`}, {"\r", LangBash, `$'\r'`}, {"\t", LangBash, `$'\t'`}, {"\v", LangBash, `$'\v'`}, {"null\x00", LangBash, &QuoteError{4, quoteErrNull}}, {"posix\x1b", LangPOSIX, &QuoteError{5, quoteErrPOSIX}}, {"posix\n", LangPOSIX, &QuoteError{5, quoteErrPOSIX}}, {"mksh16\U00086199", LangMirBSDKorn, &QuoteError{6, quoteErrMksh}}, {"\x1b\x1caaa", LangBash, `$'\x1b\x1caaa'`}, {"\x1b\x1caaa", LangMirBSDKorn, `$'\x1b\x1c'$'aaa'`}, {"\xff\x00", LangBash, &QuoteError{1, quoteErrNull}}, } for _, test := range tests { test := test t.Run("", func(t *testing.T) { t.Parallel() got, gotErr := Quote(test.str, test.lang) switch want := test.want.(type) { case string: qt.Assert(t, got, qt.Equals, want) qt.Assert(t, gotErr, qt.IsNil) case *QuoteError: qt.Assert(t, got, qt.Equals, "") qt.Assert(t, gotErr, qt.DeepEquals, want) default: t.Fatalf("unexpected type: %T", want) } }) } } sh-3.4.3/syntax/quotestate_string.go000066400000000000000000000034511420425111700175740ustar00rootroot00000000000000// Code generated by "stringer -type=quoteState"; DO NOT EDIT. package syntax import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[noState-1] _ = x[subCmd-2] _ = x[subCmdBckquo-4] _ = x[dblQuotes-8] _ = x[hdocWord-16] _ = x[hdocBody-32] _ = x[hdocBodyTabs-64] _ = x[arithmExpr-128] _ = x[arithmExprLet-256] _ = x[arithmExprCmd-512] _ = x[arithmExprBrack-1024] _ = x[testExpr-2048] _ = x[testExprRegexp-4096] _ = x[switchCase-8192] _ = x[paramExpName-16384] _ = x[paramExpSlice-32768] _ = x[paramExpRepl-65536] _ = x[paramExpExp-131072] _ = x[arrayElems-262144] } const _quoteState_name = "noStatesubCmdsubCmdBckquodblQuoteshdocWordhdocBodyhdocBodyTabsarithmExprarithmExprLetarithmExprCmdarithmExprBracktestExprtestExprRegexpswitchCaseparamExpNameparamExpSliceparamExpReplparamExpExparrayElems" var _quoteState_map = map[quoteState]string{ 1: _quoteState_name[0:7], 2: _quoteState_name[7:13], 4: _quoteState_name[13:25], 8: _quoteState_name[25:34], 16: _quoteState_name[34:42], 32: _quoteState_name[42:50], 64: _quoteState_name[50:62], 128: _quoteState_name[62:72], 256: _quoteState_name[72:85], 512: _quoteState_name[85:98], 1024: _quoteState_name[98:113], 2048: _quoteState_name[113:121], 4096: _quoteState_name[121:135], 8192: _quoteState_name[135:145], 16384: _quoteState_name[145:157], 32768: _quoteState_name[157:170], 65536: _quoteState_name[170:182], 131072: _quoteState_name[182:193], 262144: _quoteState_name[193:203], } func (i quoteState) String() string { if str, ok := _quoteState_map[i]; ok { return str } return "quoteState(" + strconv.FormatInt(int64(i), 10) + ")" } sh-3.4.3/syntax/simplify.go000066400000000000000000000126361420425111700156510ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package syntax import "bytes" // Simplify modifies a node to remove redundant pieces of syntax, and returns // whether any changes were made. // // The changes currently applied are: // // Remove clearly useless parentheses $(( (expr) )) // Remove dollars from vars in exprs (($var)) // Remove duplicate subshells $( (stmts) ) // Remove redundant quotes [[ "$var" == str ]] // Merge negations with unary operators [[ ! -n $var ]] // Use single quotes to shorten literals "\$foo" func Simplify(n Node) bool { s := simplifier{} Walk(n, s.visit) return s.modified } type simplifier struct { modified bool } func (s *simplifier) visit(node Node) bool { switch x := node.(type) { case *Assign: x.Index = s.removeParensArithm(x.Index) // Don't inline params, as x[i] and x[$i] mean // different things when x is an associative // array; the first means "i", the second "$i". case *ParamExp: x.Index = s.removeParensArithm(x.Index) // don't inline params - same as above. if x.Slice == nil { break } x.Slice.Offset = s.removeParensArithm(x.Slice.Offset) x.Slice.Offset = s.inlineSimpleParams(x.Slice.Offset) x.Slice.Length = s.removeParensArithm(x.Slice.Length) x.Slice.Length = s.inlineSimpleParams(x.Slice.Length) case *ArithmExp: x.X = s.removeParensArithm(x.X) x.X = s.inlineSimpleParams(x.X) case *ArithmCmd: x.X = s.removeParensArithm(x.X) x.X = s.inlineSimpleParams(x.X) case *ParenArithm: x.X = s.removeParensArithm(x.X) x.X = s.inlineSimpleParams(x.X) case *BinaryArithm: x.X = s.inlineSimpleParams(x.X) x.Y = s.inlineSimpleParams(x.Y) case *CmdSubst: x.Stmts = s.inlineSubshell(x.Stmts) case *Subshell: x.Stmts = s.inlineSubshell(x.Stmts) case *Word: x.Parts = s.simplifyWord(x.Parts) case *TestClause: x.X = s.removeParensTest(x.X) x.X = s.removeNegateTest(x.X) case *ParenTest: x.X = s.removeParensTest(x.X) x.X = s.removeNegateTest(x.X) case *BinaryTest: x.X = s.unquoteParams(x.X) x.X = s.removeNegateTest(x.X) if x.Op == TsMatchShort { s.modified = true x.Op = TsMatch } switch x.Op { case TsMatch, TsNoMatch: // unquoting enables globbing default: x.Y = s.unquoteParams(x.Y) } x.Y = s.removeNegateTest(x.Y) case *UnaryTest: x.X = s.unquoteParams(x.X) } return true } func (s *simplifier) simplifyWord(wps []WordPart) []WordPart { parts: for i, wp := range wps { dq, _ := wp.(*DblQuoted) if dq == nil || len(dq.Parts) != 1 { break } lit, _ := dq.Parts[0].(*Lit) if lit == nil { break } var buf bytes.Buffer escaped := false for _, r := range lit.Value { switch r { case '\\': escaped = !escaped if escaped { continue } case '\'': continue parts case '$', '"', '`': escaped = false default: if escaped { continue parts } escaped = false } buf.WriteRune(r) } newVal := buf.String() if newVal == lit.Value { break } s.modified = true wps[i] = &SglQuoted{ Left: dq.Pos(), Right: dq.End(), Dollar: dq.Dollar, Value: newVal, } } return wps } func (s *simplifier) removeParensArithm(x ArithmExpr) ArithmExpr { for { par, _ := x.(*ParenArithm) if par == nil { return x } s.modified = true x = par.X } } func (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr { w, _ := x.(*Word) if w == nil || len(w.Parts) != 1 { return x } pe, _ := w.Parts[0].(*ParamExp) if pe == nil || !ValidName(pe.Param.Value) { // Not a parameter expansion, or not a valid name, like $3. return x } if pe.Excl || pe.Length || pe.Width || pe.Slice != nil || pe.Repl != nil || pe.Exp != nil || pe.Index != nil { // A complex parameter expansion can't be simplified. // // Note that index expressions can't generally be simplified // either. It's fine to turn ${a[0]} into a[0], but others like // a[*] are invalid in many shells including Bash. return x } s.modified = true return &Word{Parts: []WordPart{pe.Param}} } func (s *simplifier) inlineSubshell(stmts []*Stmt) []*Stmt { for len(stmts) == 1 { st := stmts[0] if st.Negated || st.Background || st.Coprocess || len(st.Redirs) > 0 { break } sub, _ := st.Cmd.(*Subshell) if sub == nil { break } s.modified = true stmts = sub.Stmts } return stmts } func (s *simplifier) unquoteParams(x TestExpr) TestExpr { w, _ := x.(*Word) if w == nil || len(w.Parts) != 1 { return x } dq, _ := w.Parts[0].(*DblQuoted) if dq == nil || len(dq.Parts) != 1 { return x } if _, ok := dq.Parts[0].(*ParamExp); !ok { return x } s.modified = true w.Parts = dq.Parts return w } func (s *simplifier) removeParensTest(x TestExpr) TestExpr { for { par, _ := x.(*ParenTest) if par == nil { return x } s.modified = true x = par.X } } func (s *simplifier) removeNegateTest(x TestExpr) TestExpr { u, _ := x.(*UnaryTest) if u == nil || u.Op != TsNot { return x } switch y := u.X.(type) { case *UnaryTest: switch y.Op { case TsEmpStr: y.Op = TsNempStr s.modified = true return y case TsNempStr: y.Op = TsEmpStr s.modified = true return y case TsNot: s.modified = true return y.X } case *BinaryTest: switch y.Op { case TsMatch: y.Op = TsNoMatch s.modified = true return y case TsNoMatch: y.Op = TsMatch s.modified = true return y } } return x } sh-3.4.3/syntax/simplify_test.go000066400000000000000000000045431420425111700167060ustar00rootroot00000000000000// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package syntax import ( "bytes" "strings" "testing" ) type simplifyTest struct { in, want string } func noSimple(in string) simplifyTest { return simplifyTest{in: in, want: in} } var simplifyTests = [...]simplifyTest{ // arithmetic exprs {"$((a + ((b - c))))", "$((a + (b - c)))"}, {"$((a + (((b - c)))))", "$((a + (b - c)))"}, {"$(((b - c)))", "$((b - c))"}, {"(((b - c)))", "((b - c))"}, {"${foo[(1)]}", "${foo[1]}"}, {"${foo:(1):(2)}", "${foo:1:2}"}, {"a[(1)]=2", "a[1]=2"}, {"$(($a + ${b}))", "$((a + b))"}, noSimple("$((${!a} + ${#b}))"), noSimple("a[$b]=2"), noSimple("${a[$b]}"), noSimple("${a[@]}"), noSimple("((${a[@]}))"), noSimple("((${a[*]}))"), noSimple("((${a[0]}))"), noSimple("(($3 == $#))"), // test exprs {`[[ "$foo" == "bar" ]]`, `[[ $foo == "bar" ]]`}, {`[[ (-z "$foo") ]]`, `[[ -z $foo ]]`}, {`[[ "a b" > "$c" ]]`, `[[ "a b" > $c ]]`}, {`[[ ! -n $foo ]]`, `[[ -z $foo ]]`}, {`[[ ! ! -e a && ! -z $b ]]`, `[[ -e a && -n $b ]]`}, {`[[ (! a == b) || (! c != d) ]]`, `[[ (a != b) || (c == d) ]]`}, noSimple(`[[ -n a$b && -n $c ]]`), noSimple(`[[ ! -e foo ]]`), noSimple(`[[ foo == bar ]]`), {`[[ foo = bar ]]`, `[[ foo == bar ]]`}, // stmts {"$( (sts))", "$(sts)"}, {"( ( (sts)))", "(sts)"}, noSimple("( (sts) >f)"), noSimple("(\n\tx\n\t(sts)\n)"), // strings noSimple(`"foo"`), noSimple(`"foo$bar"`), noSimple(`"$bar"`), noSimple(`"f'o\\o"`), noSimple(`"fo\'o"`), noSimple(`"fo\\'o"`), noSimple(`"fo\no"`), {`"fo\$o"`, `'fo$o'`}, {`"fo\"o"`, `'fo"o'`}, {"\"fo\\`o\"", "'fo`o'"}, noSimple(`fo"o"bar`), noSimple(`foo""bar`), } func TestSimplify(t *testing.T) { t.Parallel() parser := NewParser() printer := NewPrinter() for _, tc := range simplifyTests { t.Run("", func(t *testing.T) { prog, err := parser.Parse(strings.NewReader(tc.in), "") if err != nil { t.Fatal(err) } simplified := Simplify(prog) var buf bytes.Buffer printer.Print(&buf, prog) want := tc.want + "\n" if got := buf.String(); got != want { t.Fatalf("Simplify mismatch of %q\nwant: %q\ngot: %q", tc.in, want, got) } if simplified && tc.in == tc.want { t.Fatalf("returned true but did not simplify") } else if !simplified && tc.in != tc.want { t.Fatalf("returned false but did simplify") } }) } } sh-3.4.3/syntax/testdata/000077500000000000000000000000001420425111700152675ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/000077500000000000000000000000001420425111700162655ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuote/000077500000000000000000000000001420425111700202415ustar00rootroot0000000000000023cf0175e40438e8033b11cdd1441a2d2893a99144c4ac0f2b5f4caa113c9edd000066400000000000000000000000561420425111700307240ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuotego test fuzz v1 string("\uffff") byte('\x02') 25f36feab4af00bc4dfc3cf56da02b842b62ba8c5ac44862b5b3b776a0d519b4000066400000000000000000000000551420425111700313140ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuotego test fuzz v1 string("\xb3c") byte('\x02') 2788bd30d386289e06a1024a030ad5ab7f363c703bea8a5d035de174491029bf000066400000000000000000000000551420425111700305200ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuotego test fuzz v1 string("\x0fC") byte('\x00') 39d5fdf93d52b2cd50fb9582b27c82d159de0575623865538ced2a7780499fa6000066400000000000000000000000571420425111700306150ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuotego test fuzz v1 string("\u05f5A") byte('\x00') 6fcce067200fb8ae6d4c2b1b7c1f55d3f7e4b38f4ee4f05e50e496a7c399f2d8000066400000000000000000000000621420425111700313040ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuotego test fuzz v1 string("\U00086199") byte('\x02') b26cd471412059c6ab6aa27b6153d42d2d00cbb00ad11d3cd88a192a7dfd2cdf000066400000000000000000000000541420425111700312050ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuotego test fuzz v1 string("\xb6") byte('\x01') df6b5d69da50c7d58ca13f6dde15e2a7224a53ce7bd72a02d49893e580b6775b000066400000000000000000000000551420425111700311370ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuotego test fuzz v1 string("\x050") byte('\x02') ea14da9b0299f4463c20659e2a51808fef8d5fb0de6324f0de64153511d4b1f8000066400000000000000000000000631420425111700307040ustar00rootroot00000000000000sh-3.4.3/syntax/testdata/fuzz/FuzzQuotego test fuzz v1 string("\U000600a04") byte('\x00') sh-3.4.3/syntax/token_string.go000066400000000000000000000074241420425111700165220ustar00rootroot00000000000000// Code generated by "stringer -type token -linecomment -trimprefix _"; DO NOT EDIT. package syntax import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[illegalTok-0] _ = x[_EOF-1] _ = x[_Newl-2] _ = x[_Lit-3] _ = x[_LitWord-4] _ = x[_LitRedir-5] _ = x[sglQuote-6] _ = x[dblQuote-7] _ = x[bckQuote-8] _ = x[and-9] _ = x[andAnd-10] _ = x[orOr-11] _ = x[or-12] _ = x[orAnd-13] _ = x[dollar-14] _ = x[dollSglQuote-15] _ = x[dollDblQuote-16] _ = x[dollBrace-17] _ = x[dollBrack-18] _ = x[dollParen-19] _ = x[dollDblParen-20] _ = x[leftBrack-21] _ = x[dblLeftBrack-22] _ = x[leftParen-23] _ = x[dblLeftParen-24] _ = x[rightBrace-25] _ = x[rightBrack-26] _ = x[rightParen-27] _ = x[dblRightParen-28] _ = x[semicolon-29] _ = x[dblSemicolon-30] _ = x[semiAnd-31] _ = x[dblSemiAnd-32] _ = x[semiOr-33] _ = x[exclMark-34] _ = x[tilde-35] _ = x[addAdd-36] _ = x[subSub-37] _ = x[star-38] _ = x[power-39] _ = x[equal-40] _ = x[nequal-41] _ = x[lequal-42] _ = x[gequal-43] _ = x[addAssgn-44] _ = x[subAssgn-45] _ = x[mulAssgn-46] _ = x[quoAssgn-47] _ = x[remAssgn-48] _ = x[andAssgn-49] _ = x[orAssgn-50] _ = x[xorAssgn-51] _ = x[shlAssgn-52] _ = x[shrAssgn-53] _ = x[rdrOut-54] _ = x[appOut-55] _ = x[rdrIn-56] _ = x[rdrInOut-57] _ = x[dplIn-58] _ = x[dplOut-59] _ = x[clbOut-60] _ = x[hdoc-61] _ = x[dashHdoc-62] _ = x[wordHdoc-63] _ = x[rdrAll-64] _ = x[appAll-65] _ = x[cmdIn-66] _ = x[cmdOut-67] _ = x[plus-68] _ = x[colPlus-69] _ = x[minus-70] _ = x[colMinus-71] _ = x[quest-72] _ = x[colQuest-73] _ = x[assgn-74] _ = x[colAssgn-75] _ = x[perc-76] _ = x[dblPerc-77] _ = x[hash-78] _ = x[dblHash-79] _ = x[caret-80] _ = x[dblCaret-81] _ = x[comma-82] _ = x[dblComma-83] _ = x[at-84] _ = x[slash-85] _ = x[dblSlash-86] _ = x[colon-87] _ = x[tsExists-88] _ = x[tsRegFile-89] _ = x[tsDirect-90] _ = x[tsCharSp-91] _ = x[tsBlckSp-92] _ = x[tsNmPipe-93] _ = x[tsSocket-94] _ = x[tsSmbLink-95] _ = x[tsSticky-96] _ = x[tsGIDSet-97] _ = x[tsUIDSet-98] _ = x[tsGrpOwn-99] _ = x[tsUsrOwn-100] _ = x[tsModif-101] _ = x[tsRead-102] _ = x[tsWrite-103] _ = x[tsExec-104] _ = x[tsNoEmpty-105] _ = x[tsFdTerm-106] _ = x[tsEmpStr-107] _ = x[tsNempStr-108] _ = x[tsOptSet-109] _ = x[tsVarSet-110] _ = x[tsRefVar-111] _ = x[tsReMatch-112] _ = x[tsNewer-113] _ = x[tsOlder-114] _ = x[tsDevIno-115] _ = x[tsEql-116] _ = x[tsNeq-117] _ = x[tsLeq-118] _ = x[tsGeq-119] _ = x[tsLss-120] _ = x[tsGtr-121] _ = x[globQuest-122] _ = x[globStar-123] _ = x[globPlus-124] _ = x[globAt-125] _ = x[globExcl-126] } const _token_name = "illegalTokEOFNewlLitLitWordLitRedir'\"`&&&||||&$$'$\"${$[$($(([[[(((}])));;;;&;;&;|!~++--***==!=<=>=+=-=*=/=%=&=|=^=<<=>>=>>><<><&>&>|<<<<-<<<&>&>><(>(+:+-:-?:?=:=%%%###^^^,,,@///:-e-f-d-c-b-p-S-L-k-g-u-G-O-N-r-w-x-s-t-z-n-o-v-R=~-nt-ot-ef-eq-ne-le-ge-lt-gt?(*(+(@(!(" var _token_index = [...]uint16{0, 10, 13, 17, 20, 27, 35, 36, 37, 38, 39, 41, 43, 44, 46, 47, 49, 51, 53, 55, 57, 60, 61, 63, 64, 66, 67, 68, 69, 71, 72, 74, 76, 79, 81, 82, 83, 85, 87, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 117, 120, 121, 123, 124, 126, 128, 130, 132, 134, 137, 140, 142, 145, 147, 149, 150, 152, 153, 155, 156, 158, 159, 161, 162, 164, 165, 167, 168, 170, 171, 173, 174, 175, 177, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 231, 234, 237, 240, 243, 246, 249, 252, 255, 257, 259, 261, 263, 265} func (i token) String() string { if i >= token(len(_token_index)-1) { return "token(" + strconv.FormatInt(int64(i), 10) + ")" } return _token_name[_token_index[i]:_token_index[i+1]] } sh-3.4.3/syntax/tokens.go000066400000000000000000000231351420425111700153140ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax //go:generate stringer -type token -linecomment -trimprefix _ type token uint32 // The list of all possible tokens. const ( illegalTok token = iota _EOF _Newl _Lit _LitWord _LitRedir sglQuote // ' dblQuote // " bckQuote // ` and // & andAnd // && orOr // || or // | orAnd // |& dollar // $ dollSglQuote // $' dollDblQuote // $" dollBrace // ${ dollBrack // $[ dollParen // $( dollDblParen // $(( leftBrack // [ dblLeftBrack // [[ leftParen // ( dblLeftParen // (( rightBrace // } rightBrack // ] rightParen // ) dblRightParen // )) semicolon // ; dblSemicolon // ;; semiAnd // ;& dblSemiAnd // ;;& semiOr // ;| exclMark // ! tilde // ~ addAdd // ++ subSub // -- star // * power // ** equal // == nequal // != lequal // <= gequal // >= addAssgn // += subAssgn // -= mulAssgn // *= quoAssgn // /= remAssgn // %= andAssgn // &= orAssgn // |= xorAssgn // ^= shlAssgn // <<= shrAssgn // >>= rdrOut // > appOut // >> rdrIn // < rdrInOut // <> dplIn // <& dplOut // >& clbOut // >| hdoc // << dashHdoc // <<- wordHdoc // <<< rdrAll // &> appAll // &>> cmdIn // <( cmdOut // >( plus // + colPlus // :+ minus // - colMinus // :- quest // ? colQuest // :? assgn // = colAssgn // := perc // % dblPerc // %% hash // # dblHash // ## caret // ^ dblCaret // ^^ comma // , dblComma // ,, at // @ slash // / dblSlash // // colon // : tsExists // -e tsRegFile // -f tsDirect // -d tsCharSp // -c tsBlckSp // -b tsNmPipe // -p tsSocket // -S tsSmbLink // -L tsSticky // -k tsGIDSet // -g tsUIDSet // -u tsGrpOwn // -G tsUsrOwn // -O tsModif // -N tsRead // -r tsWrite // -w tsExec // -x tsNoEmpty // -s tsFdTerm // -t tsEmpStr // -z tsNempStr // -n tsOptSet // -o tsVarSet // -v tsRefVar // -R tsReMatch // =~ tsNewer // -nt tsOlder // -ot tsDevIno // -ef tsEql // -eq tsNeq // -ne tsLeq // -le tsGeq // -ge tsLss // -lt tsGtr // -gt globQuest // ?( globStar // *( globPlus // +( globAt // @( globExcl // !( ) type RedirOperator token const ( RdrOut = RedirOperator(rdrOut) + iota // > AppOut // >> RdrIn // < RdrInOut // <> DplIn // <& DplOut // >& ClbOut // >| Hdoc // << DashHdoc // <<- WordHdoc // <<< RdrAll // &> AppAll // &>> ) type ProcOperator token const ( CmdIn = ProcOperator(cmdIn) + iota // <( CmdOut // >( ) type GlobOperator token const ( GlobZeroOrOne = GlobOperator(globQuest) + iota // ?( GlobZeroOrMore // *( GlobOneOrMore // +( GlobOne // @( GlobExcept // !( ) type BinCmdOperator token const ( AndStmt = BinCmdOperator(andAnd) + iota // && OrStmt // || Pipe // | PipeAll // |& ) type CaseOperator token const ( Break = CaseOperator(dblSemicolon) + iota // ;; Fallthrough // ;& Resume // ;;& ResumeKorn // ;| ) type ParNamesOperator token const ( NamesPrefix = ParNamesOperator(star) // * NamesPrefixWords = ParNamesOperator(at) // @ ) type ParExpOperator token const ( AlternateUnset = ParExpOperator(plus) + iota // + AlternateUnsetOrNull // :+ DefaultUnset // - DefaultUnsetOrNull // :- ErrorUnset // ? ErrorUnsetOrNull // :? AssignUnset // = AssignUnsetOrNull // := RemSmallSuffix // % RemLargeSuffix // %% RemSmallPrefix // # RemLargePrefix // ## UpperFirst // ^ UpperAll // ^^ LowerFirst // , LowerAll // ,, OtherParamOps // @ ) type UnAritOperator token const ( Not = UnAritOperator(exclMark) + iota // ! BitNegation // ~ Inc // ++ Dec // -- Plus = UnAritOperator(plus) // + Minus = UnAritOperator(minus) // - ) type BinAritOperator token const ( Add = BinAritOperator(plus) // + Sub = BinAritOperator(minus) // - Mul = BinAritOperator(star) // * Quo = BinAritOperator(slash) // / Rem = BinAritOperator(perc) // % Pow = BinAritOperator(power) // ** Eql = BinAritOperator(equal) // == Gtr = BinAritOperator(rdrOut) // > Lss = BinAritOperator(rdrIn) // < Neq = BinAritOperator(nequal) // != Leq = BinAritOperator(lequal) // <= Geq = BinAritOperator(gequal) // >= And = BinAritOperator(and) // & Or = BinAritOperator(or) // | Xor = BinAritOperator(caret) // ^ Shr = BinAritOperator(appOut) // >> Shl = BinAritOperator(hdoc) // << AndArit = BinAritOperator(andAnd) // && OrArit = BinAritOperator(orOr) // || Comma = BinAritOperator(comma) // , TernQuest = BinAritOperator(quest) // ? TernColon = BinAritOperator(colon) // : Assgn = BinAritOperator(assgn) // = AddAssgn = BinAritOperator(addAssgn) // += SubAssgn = BinAritOperator(subAssgn) // -= MulAssgn = BinAritOperator(mulAssgn) // *= QuoAssgn = BinAritOperator(quoAssgn) // /= RemAssgn = BinAritOperator(remAssgn) // %= AndAssgn = BinAritOperator(andAssgn) // &= OrAssgn = BinAritOperator(orAssgn) // |= XorAssgn = BinAritOperator(xorAssgn) // ^= ShlAssgn = BinAritOperator(shlAssgn) // <<= ShrAssgn = BinAritOperator(shrAssgn) // >>= ) type UnTestOperator token const ( TsExists = UnTestOperator(tsExists) + iota // -e TsRegFile // -f TsDirect // -d TsCharSp // -c TsBlckSp // -b TsNmPipe // -p TsSocket // -S TsSmbLink // -L TsSticky // -k TsGIDSet // -g TsUIDSet // -u TsGrpOwn // -G TsUsrOwn // -O TsModif // -N TsRead // -r TsWrite // -w TsExec // -x TsNoEmpty // -s TsFdTerm // -t TsEmpStr // -z TsNempStr // -n TsOptSet // -o TsVarSet // -v TsRefVar // -R TsNot = UnTestOperator(exclMark) // ! ) type BinTestOperator token const ( TsReMatch = BinTestOperator(tsReMatch) + iota // =~ TsNewer // -nt TsOlder // -ot TsDevIno // -ef TsEql // -eq TsNeq // -ne TsLeq // -le TsGeq // -ge TsLss // -lt TsGtr // -gt AndTest = BinTestOperator(andAnd) // && OrTest = BinTestOperator(orOr) // || TsMatchShort = BinTestOperator(assgn) // = TsMatch = BinTestOperator(equal) // == TsNoMatch = BinTestOperator(nequal) // != TsBefore = BinTestOperator(rdrIn) // < TsAfter = BinTestOperator(rdrOut) // > ) func (o RedirOperator) String() string { return token(o).String() } func (o ProcOperator) String() string { return token(o).String() } func (o GlobOperator) String() string { return token(o).String() } func (o BinCmdOperator) String() string { return token(o).String() } func (o CaseOperator) String() string { return token(o).String() } func (o ParNamesOperator) String() string { return token(o).String() } func (o ParExpOperator) String() string { return token(o).String() } func (o UnAritOperator) String() string { return token(o).String() } func (o BinAritOperator) String() string { return token(o).String() } func (o UnTestOperator) String() string { return token(o).String() } func (o BinTestOperator) String() string { return token(o).String() } sh-3.4.3/syntax/walk.go000066400000000000000000000131271420425111700147470ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "fmt" "io" "reflect" ) func walkStmts(stmts []*Stmt, last []Comment, f func(Node) bool) { for _, s := range stmts { Walk(s, f) } for _, c := range last { Walk(&c, f) } } func walkWords(words []*Word, f func(Node) bool) { for _, w := range words { Walk(w, f) } } // Walk traverses a syntax tree in depth-first order: It starts by calling // f(node); node must not be nil. If f returns true, Walk invokes f // recursively for each of the non-nil children of node, followed by // f(nil). func Walk(node Node, f func(Node) bool) { if !f(node) { return } switch x := node.(type) { case *File: walkStmts(x.Stmts, x.Last, f) case *Comment: case *Stmt: for _, c := range x.Comments { if !x.End().After(c.Pos()) { defer Walk(&c, f) break } Walk(&c, f) } if x.Cmd != nil { Walk(x.Cmd, f) } for _, r := range x.Redirs { Walk(r, f) } case *Assign: if x.Name != nil { Walk(x.Name, f) } if x.Value != nil { Walk(x.Value, f) } if x.Index != nil { Walk(x.Index, f) } if x.Array != nil { Walk(x.Array, f) } case *Redirect: if x.N != nil { Walk(x.N, f) } Walk(x.Word, f) if x.Hdoc != nil { Walk(x.Hdoc, f) } case *CallExpr: for _, a := range x.Assigns { Walk(a, f) } walkWords(x.Args, f) case *Subshell: walkStmts(x.Stmts, x.Last, f) case *Block: walkStmts(x.Stmts, x.Last, f) case *IfClause: walkStmts(x.Cond, x.CondLast, f) walkStmts(x.Then, x.ThenLast, f) if x.Else != nil { Walk(x.Else, f) } case *WhileClause: walkStmts(x.Cond, x.CondLast, f) walkStmts(x.Do, x.DoLast, f) case *ForClause: Walk(x.Loop, f) walkStmts(x.Do, x.DoLast, f) case *WordIter: Walk(x.Name, f) walkWords(x.Items, f) case *CStyleLoop: if x.Init != nil { Walk(x.Init, f) } if x.Cond != nil { Walk(x.Cond, f) } if x.Post != nil { Walk(x.Post, f) } case *BinaryCmd: Walk(x.X, f) Walk(x.Y, f) case *FuncDecl: Walk(x.Name, f) Walk(x.Body, f) case *Word: for _, wp := range x.Parts { Walk(wp, f) } case *Lit: case *SglQuoted: case *DblQuoted: for _, wp := range x.Parts { Walk(wp, f) } case *CmdSubst: walkStmts(x.Stmts, x.Last, f) case *ParamExp: Walk(x.Param, f) if x.Index != nil { Walk(x.Index, f) } if x.Repl != nil { if x.Repl.Orig != nil { Walk(x.Repl.Orig, f) } if x.Repl.With != nil { Walk(x.Repl.With, f) } } if x.Exp != nil && x.Exp.Word != nil { Walk(x.Exp.Word, f) } case *ArithmExp: Walk(x.X, f) case *ArithmCmd: Walk(x.X, f) case *BinaryArithm: Walk(x.X, f) Walk(x.Y, f) case *BinaryTest: Walk(x.X, f) Walk(x.Y, f) case *UnaryArithm: Walk(x.X, f) case *UnaryTest: Walk(x.X, f) case *ParenArithm: Walk(x.X, f) case *ParenTest: Walk(x.X, f) case *CaseClause: Walk(x.Word, f) for _, ci := range x.Items { Walk(ci, f) } for _, c := range x.Last { Walk(&c, f) } case *CaseItem: for _, c := range x.Comments { if c.Pos().After(x.Pos()) { defer Walk(&c, f) break } Walk(&c, f) } walkWords(x.Patterns, f) walkStmts(x.Stmts, x.Last, f) case *TestClause: Walk(x.X, f) case *DeclClause: for _, a := range x.Args { Walk(a, f) } case *ArrayExpr: for _, el := range x.Elems { Walk(el, f) } for _, c := range x.Last { Walk(&c, f) } case *ArrayElem: for _, c := range x.Comments { if c.Pos().After(x.Pos()) { defer Walk(&c, f) break } Walk(&c, f) } if x.Index != nil { Walk(x.Index, f) } if x.Value != nil { Walk(x.Value, f) } case *ExtGlob: Walk(x.Pattern, f) case *ProcSubst: walkStmts(x.Stmts, x.Last, f) case *TimeClause: if x.Stmt != nil { Walk(x.Stmt, f) } case *CoprocClause: if x.Name != nil { Walk(x.Name, f) } Walk(x.Stmt, f) case *LetClause: for _, expr := range x.Exprs { Walk(expr, f) } case *TestDecl: Walk(x.Description, f) Walk(x.Body, f) default: panic(fmt.Sprintf("syntax.Walk: unexpected node type %T", x)) } f(nil) } // DebugPrint prints the provided syntax tree, spanning multiple lines and with // indentation. Can be useful to investigate the content of a syntax tree. func DebugPrint(w io.Writer, node Node) error { p := debugPrinter{out: w} p.print(reflect.ValueOf(node)) return p.err } type debugPrinter struct { out io.Writer level int err error } func (p *debugPrinter) printf(format string, args ...interface{}) { _, err := fmt.Fprintf(p.out, format, args...) if err != nil && p.err == nil { p.err = err } } func (p *debugPrinter) newline() { p.printf("\n") for i := 0; i < p.level; i++ { p.printf(". ") } } func (p *debugPrinter) print(x reflect.Value) { switch x.Kind() { case reflect.Interface: if x.IsNil() { p.printf("nil") return } p.print(x.Elem()) case reflect.Ptr: if x.IsNil() { p.printf("nil") return } p.printf("*") p.print(x.Elem()) case reflect.Slice: p.printf("%s (len = %d) {", x.Type(), x.Len()) if x.Len() > 0 { p.level++ p.newline() for i := 0; i < x.Len(); i++ { p.printf("%d: ", i) p.print(x.Index(i)) if i == x.Len()-1 { p.level-- } p.newline() } } p.printf("}") case reflect.Struct: if v, ok := x.Interface().(Pos); ok { p.printf("%v:%v", v.Line(), v.Col()) return } t := x.Type() p.printf("%s {", t) p.level++ p.newline() for i := 0; i < t.NumField(); i++ { p.printf("%s: ", t.Field(i).Name) p.print(x.Field(i)) if i == x.NumField()-1 { p.level-- } p.newline() } p.printf("}") default: p.printf("%#v", x.Interface()) } } sh-3.4.3/syntax/walk_test.go000066400000000000000000000056241420425111700160110ustar00rootroot00000000000000// Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information package syntax import ( "reflect" "strings" "testing" ) func TestWalk(t *testing.T) { t.Parallel() seen := map[string]bool{ "*syntax.File": false, "*syntax.Comment": false, "*syntax.Stmt": false, "*syntax.Assign": false, "*syntax.Redirect": false, "*syntax.CallExpr": false, "*syntax.Subshell": false, "*syntax.Block": false, "*syntax.IfClause": false, "*syntax.WhileClause": false, "*syntax.ForClause": false, "*syntax.WordIter": false, "*syntax.CStyleLoop": false, "*syntax.BinaryCmd": false, "*syntax.FuncDecl": false, "*syntax.Word": false, "*syntax.Lit": false, "*syntax.SglQuoted": false, "*syntax.DblQuoted": false, "*syntax.CmdSubst": false, "*syntax.ParamExp": false, "*syntax.ArithmExp": false, "*syntax.ArithmCmd": false, "*syntax.BinaryArithm": false, "*syntax.UnaryArithm": false, "*syntax.ParenArithm": false, "*syntax.CaseClause": false, "*syntax.CaseItem": false, "*syntax.TestClause": false, "*syntax.BinaryTest": false, "*syntax.UnaryTest": false, "*syntax.ParenTest": false, "*syntax.DeclClause": false, "*syntax.ArrayExpr": false, "*syntax.ArrayElem": false, "*syntax.ExtGlob": false, "*syntax.ProcSubst": false, "*syntax.TimeClause": false, "*syntax.CoprocClause": false, "*syntax.LetClause": false, } parser := NewParser(KeepComments(true)) var allStrs []string for _, c := range fileTests { allStrs = append(allStrs, c.Strs[0]) } for _, c := range printTests { allStrs = append(allStrs, c.in) } for _, in := range allStrs { t.Run("", func(t *testing.T) { prog, err := parser.Parse(strings.NewReader(in), "") if err != nil { // good enough for now, as the bash // parser ignoring errors covers what we // need. return } lastOffs := uint(0) Walk(prog, func(node Node) bool { if node == nil { return false } tstr := reflect.TypeOf(node).String() if _, ok := seen[tstr]; !ok { t.Errorf("unexpected type: %s", tstr) } else { seen[tstr] = true } switch node.(type) { case *Lit: return false case *Comment: default: return true } offs := node.Pos().Offset() if offs >= lastOffs { lastOffs = offs } else { t.Errorf("comment offset goes back") } return true }) }) } for tstr, tseen := range seen { if !tseen { t.Errorf("type not seen: %s", tstr) } } } type newNode struct{} func (newNode) Pos() Pos { return Pos{} } func (newNode) End() Pos { return Pos{} } func TestWalkUnexpectedType(t *testing.T) { t.Parallel() defer func() { if r := recover(); r == nil { t.Errorf("did not panic") } }() Walk(newNode{}, func(node Node) bool { return true }) }