diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 493b465..44d151e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ #### For bugs -- Rule Id (if any, e.g. SC1000): -- My shellcheck version (`shellcheck --version` or "online"): +- Rule Id (if any, e.g. SC1000): +- My shellcheck version (`shellcheck --version` or "online"): - [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086) - [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 81bae9a..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83269c9..a435cf4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: sudo apt-get install cabal-install - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -37,47 +37,24 @@ jobs: mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: source path: source/ - run_tests: - name: Run tests - needs: package_source - runs-on: ubuntu-latest - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - - - name: Install dependencies - run: | - sudo apt-get update && sudo apt-get install ghc cabal-install - cabal update - - - name: Unpack source - run: | - cd source - tar xvf source.tar.gz --strip-components=1 - - - name: Build and run tests - run: | - cd source - cabal test - build_source: - name: Build + name: Build Source Code needs: package_source strategy: matrix: - build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64] + build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, windows.x86_64] runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 - name: Build source run: | @@ -86,9 +63,9 @@ jobs: ( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} ) - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: - name: ${{matrix.build}}.bin + name: bin path: bin/ package_binary: @@ -97,25 +74,25 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 - name: Work around GitHub permissions bug - run: chmod +x *.bin/*/shellcheck* + run: chmod +x bin/*/shellcheck* - name: Package binaries run: | export TAGS="$(cat source/tags)" mkdir -p deploy - cp -r *.bin/* deploy + cp -r bin/* deploy cd deploy ../.prepare_deploy rm -rf */ README* LICENSE* - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: deploy path: deploy/ @@ -126,16 +103,11 @@ jobs: runs-on: ubuntu-latest environment: Deploy steps: - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install hub - - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v3 - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 - name: Upload to GitHub env: diff --git a/.gitignore b/.gitignore index cf373a8..6d5f1ae 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,3 @@ cabal.config /parts/ /prime/ *.snap -/dist-newstyle/ diff --git a/.multi_arch_docker b/.multi_arch_docker index 81048a2..a9f7401 100755 --- a/.multi_arch_docker +++ b/.multi_arch_docker @@ -3,10 +3,28 @@ # binaries previously built and deployed to GitHub. function multi_arch_docker::install_docker_buildx() { + # Install up-to-date version of docker, with buildx support. + local -r docker_apt_repo='https://download.docker.com/linux/ubuntu' + curl -fsSL "${docker_apt_repo}/gpg" | sudo apt-key add - + local -r os="$(lsb_release -cs)" + sudo add-apt-repository "deb [arch=amd64] $docker_apt_repo $os stable" + sudo apt-get update + sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce + + # Enable docker daemon experimental support (for 'pull --platform'). + local -r 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 + # Install QEMU multi-architecture support for docker buildx. docker run --rm --privileged multiarch/qemu-user-static --reset -p yes # Instantiate docker buildx builder with multi-architecture support. + export DOCKER_CLI_EXPERIMENTAL=enabled docker buildx create --name mybuilder docker buildx use mybuilder # Start up buildx and verify that all is OK. @@ -80,7 +98,6 @@ function multi_arch_docker::main() { export DOCKER_PLATFORMS='linux/amd64' DOCKER_PLATFORMS+=' linux/arm64' DOCKER_PLATFORMS+=' linux/arm/v6' - DOCKER_PLATFORMS+=' linux/riscv64' multi_arch_docker::install_docker_buildx multi_arch_docker::login_to_docker_hub diff --git a/.snapsquid.conf b/.snapsquid.conf new file mode 100644 index 0000000..205c1a6 --- /dev/null +++ b/.snapsquid.conf @@ -0,0 +1,14 @@ +# In 2015, cabal-install had a http bug triggered when proxies didn't keep +# the connection open. This version made it into Ubuntu Xenial as used by +# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug. +# +# https://bugs.launchpad.net/launchpad-buildd/+bug/1797809 +# +# Workaround: add more proxy + +visible_hostname localhost +http_port 8888 +cache_peer 10.10.10.1 parent 8222 0 no-query default +cache_peer_domain localhost !.internal +http_access allow all + diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc8a79..57951c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,47 +1,3 @@ -## Git -### Added -- SC2327/SC2328: Warn about capturing the output of redirected commands. -- SC2329: Warn when (non-escaping) functions are never invoked. -- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox. -- SC2331: Suggest using standard -e instead of unary -a in tests. -- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash. -- SC3062: Warn about bashism `[ -o opt ]`. -- Precompiled binaries for Linux riscv64 (linux.riscv64) -### Changed -- SC2002 about Useless Use Of Cat is now disabled by default. It can be - re-enabled with `--enable=useless-use-of-cat` or equivalent directive. -- SC2015 about `A && B || C` no longer triggers when B is a test command. -- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024 -### Fixed -- SC2218 about function use-before-define is now more accurate. -- SC2317 about unreachable commands is now less spammy for nested ones. -- SC2292, optional suggestion for [[ ]], now triggers for Busybox. - -### Removed -- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024 - -## v0.10.0 - 2024-03-07 -### Added -- Precompiled binaries for macOS ARM64 (darwin.aarch64) -- Added support for busybox sh -- Added flag --rcfile to specify an rc file by name. -- Added `extended-analysis=true` directive to enable/disable dataflow analysis - (with a corresponding --extended-analysis flag). -- SC2324: Warn when x+=1 appends instead of increments -- SC2325: Warn about multiple `!`s in dash/sh. -- SC2326: Warn about `foo | ! bar` in bash/dash/sh. -- SC3012: Warn about lexicographic-compare bashism in test like in [ ] -- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ] -- SC3014: Warn bashism `test _ == _` like in [ ] -- SC3015: Warn bashism `test _ =~ _` like in [ ] -- SC3016: Warn bashism `test -v _` like in [ ] -- SC3017: Warn bashism `test -a _` like in [ ] - -### Fixed -- source statements with here docs now work correctly -- "(Array.!): undefined array element" error should no longer occur - - ## v0.9.0 - 2022-12-12 ### Added - SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!) diff --git a/README.md b/README.md index 9b776cf..6f3e4a9 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ You can see ShellCheck suggestions directly in a variety of editors. * Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck). -* Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar). +* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck). * VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck). @@ -110,11 +110,8 @@ Services and platforms that have ShellCheck pre-installed and ready to use: * [Codacy](https://www.codacy.com/) * [Code Climate](https://codeclimate.com/) * [Code Factor](https://www.codefactor.io/) -* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner) * [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck) * [Github](https://github.com/features/actions) (only Linux) -* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml) -* [CodeRabbit](https://coderabbit.ai/) Most other services, including [GitLab](https://about.gitlab.com/), let you install ShellCheck yourself, either through the system's package manager (see [Installing](#installing)), @@ -196,12 +193,6 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)): C:\> choco install shellcheck ``` -Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)): - -```cmd -C:\> winget install --id koalaman.shellcheck -``` - Or Windows (via [scoop](http://scoop.sh)): ```cmd @@ -230,26 +221,17 @@ Using the [nix package manager](https://nixos.org/nix): nix-env -iA nixpkgs.shellcheck ``` -Using the [Flox package manager](https://flox.dev/) -```sh -flox install shellcheck -``` - Alternatively, you can download pre-compiled binaries for the latest release here: * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) -* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz) * [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases (including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds). -There are currently no official binaries for Apple Silicon, but third party builds are available via -[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases). - Distro packages already come with a `man` page. If you are building from source, it can be installed with: ```console @@ -317,6 +299,10 @@ Verify that `cabal` is installed and update its dependency list with $ cabal install +Or if you intend to run the tests: + + $ cabal install --enable-tests + This will compile ShellCheck and install it to your `~/.cabal/bin` directory. Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`): @@ -562,3 +548,4 @@ Happy ShellChecking! * The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221). * ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)! + diff --git a/ShellCheck.cabal b/ShellCheck.cabal index 68c32d9..1226588 100644 --- a/ShellCheck.cabal +++ b/ShellCheck.cabal @@ -1,5 +1,5 @@ Name: ShellCheck -Version: 0.10.0 +Version: 0.9.0 Synopsis: Shell script analysis tool License: GPL-3 License-file: LICENSE @@ -46,21 +46,21 @@ library semigroups build-depends: -- The lower bounds are based on GHC 7.10.3 - -- The upper bounds are based on GHC 9.8.1 - aeson >= 1.4.0 && < 2.3, + -- The upper bounds are based on GHC 9.4.3 + aeson >= 1.4.0 && < 2.2, array >= 0.5.1 && < 0.6, base >= 4.8.0.0 && < 5, - bytestring >= 0.10.6 && < 0.13, - containers >= 0.5.6 && < 0.8, - deepseq >= 1.4.1 && < 1.6, - Diff >= 0.4.0 && < 1.1, - fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), - filepath >= 1.4.0 && < 1.6, - mtl >= 2.2.2 && < 2.4, + bytestring >= 0.10.6 && < 0.12, + containers >= 0.5.6 && < 0.7, + deepseq >= 1.4.1 && < 1.5, + Diff >= 0.4.0 && < 0.5, + fgl >= 5.7.0 && < 5.9, + filepath >= 1.4.0 && < 1.5, + mtl >= 2.2.2 && < 2.3, parsec >= 3.1.14 && < 3.2, - QuickCheck >= 2.14.2 && < 2.16, + QuickCheck >= 2.14.2 && < 2.15, regex-tdfa >= 1.2.0 && < 1.4, - transformers >= 0.4.2 && < 0.7, + transformers >= 0.4.2 && < 0.6, -- getXdgDirectory from 1.2.3.0 directory >= 1.2.3 && < 1.4, diff --git a/build/README.md b/build/README.md index 31e8607..eb745a0 100644 --- a/build/README.md +++ b/build/README.md @@ -11,7 +11,3 @@ This makes it simple to build any release without exotic hardware or software. An image can be built and tagged using `build_builder`, and run on a source tarball using `run_builder`. - -Tip: Are you developing an image that relies on QEmu usermode emulation? - It's easy to accidentally depend on binfmt\_misc on the host OS. - Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing. diff --git a/build/darwin.aarch64/Dockerfile b/build/darwin.aarch64/Dockerfile deleted file mode 100644 index 7839728..0000000 --- a/build/darwin.aarch64/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest - -ENV TARGET aarch64-apple-darwin22 -ENV TARGETNAME darwin.aarch64 - -# Build dependencies -USER root -ENV DEBIAN_FRONTEND noninteractive -ENV LC_ALL C.utf8 - -# Install basic deps -RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static - -# Install a more suitable host compiler -WORKDIR /host-ghc -RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin -RUN curl -L 'https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-x86_64-deb10-linux.tar.xz' | tar xJ --strip-components=1 -RUN ./configure && make install - -# Build GHC. We have to use an old version because cross-compilation across OS has since broken. -WORKDIR /ghc -RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1 -RUN apt-get install -y llvm-12 -RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" -RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" -RUN make install - -# Due to an apparent cabal bug, we specify our options directly to cabal -# It won't reuse caches if ghc-options are specified in ~/.cabal/config -ENV CABALOPTS "--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0" - -# Prebuild the dependencies -RUN cabal update -RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck - -# Copy the build script -COPY build /usr/bin - -WORKDIR /scratch -ENTRYPOINT ["/usr/bin/build"] diff --git a/build/darwin.aarch64/build b/build/darwin.aarch64/build deleted file mode 100755 index ff522ff..0000000 --- a/build/darwin.aarch64/build +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -set -xe -{ - tar xzv --strip-components=1 - chmod +x striptests && ./striptests - mkdir "$TARGETNAME" - ( IFS=';'; cabal build $CABALOPTS ) - find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - # Stripping invalidates the code signature and the build image does - # not appear to have anything similar to the 'codesign' tool. - # "$TARGET-strip" "$TARGETNAME/shellcheck" - ls -l "$TARGETNAME" - file "$TARGETNAME/shellcheck" | grep "Mach-O 64-bit arm64 executable" -} >&2 -tar czv "$TARGETNAME" diff --git a/build/darwin.aarch64/tag b/build/darwin.aarch64/tag deleted file mode 100644 index ae93ef3..0000000 --- a/build/darwin.aarch64/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-darwin-aarch64 diff --git a/build/darwin.x86_64/Dockerfile b/build/darwin.x86_64/Dockerfile index a53245f..9e33a82 100644 --- a/build/darwin.x86_64/Dockerfile +++ b/build/darwin.x86_64/Dockerfile @@ -6,18 +6,15 @@ ENV TARGETNAME darwin.x86_64 # Build dependencies USER root ENV DEBIAN_FRONTEND noninteractive -RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list -RUN apt-get update -RUN apt-get dist-upgrade -y -RUN apt-get install -y ghc automake autoconf llvm curl alex happy +RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl # Build GHC WORKDIR /ghc -RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1 -RUN ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" +RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1 +RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" RUN make install -RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin +RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin # Due to an apparent cabal bug, we specify our options directly to cabal # It won't reuse caches if ghc-options are specified in ~/.cabal/config diff --git a/build/darwin.x86_64/build b/build/darwin.x86_64/build index 058cece..53857e8 100755 --- a/build/darwin.x86_64/build +++ b/build/darwin.x86_64/build @@ -4,6 +4,7 @@ set -xe tar xzv --strip-components=1 chmod +x striptests && ./striptests mkdir "$TARGETNAME" + cabal update ( IFS=';'; cabal build $CABALOPTS ) find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" diff --git a/build/linux.aarch64/Dockerfile b/build/linux.aarch64/Dockerfile index 1ffe1bd..60537b3 100644 --- a/build/linux.aarch64/Dockerfile +++ b/build/linux.aarch64/Dockerfile @@ -6,29 +6,19 @@ ENV TARGETNAME linux.aarch64 # Build dependencies USER root ENV DEBIAN_FRONTEND noninteractive - -# These deps are from 20.04, because GHC's compiler/llvm support moves slowly -RUN apt-get update && apt-get install -y llvm gcc-$TARGET - -# The rest are from 22.10 -RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list -# Kinetic does not receive updates anymore, switch to last available -RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list -RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list - -RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static +RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET # Build GHC WORKDIR /ghc -RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1 +RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1 RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" RUN make install -RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin +RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin # Due to an apparent cabal bug, we specify our options directly to cabal # It won't reuse caches if ghc-options are specified in ~/.cabal/config -ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native" +ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg" # Prebuild the dependencies RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck diff --git a/build/linux.aarch64/build b/build/linux.aarch64/build index 3ce61ce..f8001aa 100755 --- a/build/linux.aarch64/build +++ b/build/linux.aarch64/build @@ -4,6 +4,7 @@ set -xe tar xzv --strip-components=1 chmod +x striptests && ./striptests mkdir "$TARGETNAME" + cabal update ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" diff --git a/build/linux.armv6hf/Dockerfile b/build/linux.armv6hf/Dockerfile index b4d4197..f933dda 100644 --- a/build/linux.armv6hf/Dockerfile +++ b/build/linux.armv6hf/Dockerfile @@ -1,7 +1,25 @@ -# This Docker file uses a custom QEmu fork with patches to follow execve -# to build all of ShellCheck emulated. +# I've again spent days trying to get a working armv6hf compiler going. +# God only knows how many recompilations of GCC, GHC, libraries, and +# ShellCheck itself, has gone into it. +# +# I tried Debian's toolchain. I tried my custom one built according to +# RPi `gcc -v`. I tried GHC9, glibc, musl, registerised vs not, but +# nothing has yielded an armv6hf binary that does not immediately +# segfault on qemu-arm-static or the RPi itself. +# +# I then tried the same but with armv7hf. Same story. +# +# Emulating the entire userspace with balenalib again? Very strange build +# failures where programs would fail to execute with > ~100 arguments. +# +# Finally, creating our own appears to work when using a custom QEmu +# patched to follow execve calls. +# +# PS: $100 bounty for getting a RPi1 compatible static build going +# with cross-compilation, similar to what the aarch64 build does. +# -FROM ubuntu:24.04 +FROM ubuntu:20.04 ENV TARGETNAME linux.armv6hf @@ -9,34 +27,34 @@ ENV TARGETNAME linux.armv6hf USER root ENV DEBIAN_FRONTEND noninteractive RUN apt-get update -RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap -WORKDIR /qemu -RUN git clone --depth 1 https://github.com/koalaman/qemu . -RUN ./configure --static --disable-werror && cd build && ninja qemu-arm +RUN apt-get install -y build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev +WORKDIR /build +RUN git clone --depth 1 https://github.com/koalaman/qemu +RUN cd qemu && ./configure --static && cd build && ninja qemu-arm +RUN cp qemu/build/qemu-arm /build/qemu-arm-static ENV QEMU_EXECVE 1 -# Convenience utility -COPY scutil /bin/scutil -COPY scutil /chroot/bin/scutil -RUN chmod +x /bin/scutil /chroot/bin/scutil - # Set up an armv6 userspace WORKDIR / -RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian -RUN cp /qemu/build/qemu-arm /chroot/bin/qemu -RUN scutil emu /debootstrap/debootstrap --second-stage +RUN apt-get install -y debootstrap qemu-user-static +# We expect this to fail if the host doesn't have binfmt qemu support +RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ] +RUN cp /build/qemu-arm-static /pi/usr/bin/qemu-arm-static +RUN printf > /bin/pirun '%s\n' '#!/bin/sh' 'chroot /pi /usr/bin/qemu-arm-static /usr/bin/env "$@"' && chmod +x /bin/pirun +# If the debootstrap process didn't finish, continue it +RUN [ ! -e /pi/debootstrap ] || pirun '/debootstrap/debootstrap' --second-stage # Install deps in the chroot -RUN scutil emu apt-get update -RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install -RUN scutil emu cabal update +RUN pirun apt-get update +RUN pirun apt-get install -y ghc cabal-install # Finally we can build the current dependencies. This takes hours. ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections" -# Generated with `cabal freeze --constraint 'hashable -arch-native'` -COPY cabal.project.freeze /chroot/etc -RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS +RUN pirun cabal update +RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck +RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl # Copy the build script -COPY build /chroot/bin -ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"] +WORKDIR /pi/scratch +COPY build /pi/usr/bin +ENTRYPOINT ["/bin/pirun", "/usr/bin/build"] diff --git a/build/linux.armv6hf/build b/build/linux.armv6hf/build index 1d496ae..daa94d9 100755 --- a/build/linux.armv6hf/build +++ b/build/linux.armv6hf/build @@ -1,9 +1,8 @@ #!/bin/sh set -xe -mkdir /scratch && cd /scratch +cd /scratch { tar xzv --strip-components=1 - cp /etc/cabal.project.freeze . chmod +x striptests && ./striptests mkdir "$TARGETNAME" # This script does not cabal update because compiling anything new is slow diff --git a/build/linux.armv6hf/cabal.project.freeze b/build/linux.armv6hf/cabal.project.freeze deleted file mode 100644 index 183bcc6..0000000 --- a/build/linux.armv6hf/cabal.project.freeze +++ /dev/null @@ -1,93 +0,0 @@ -active-repositories: hackage.haskell.org:merge -constraints: any.Diff ==0.5, - any.OneTuple ==0.4.2, - any.QuickCheck ==2.14.3, - QuickCheck -old-random +templatehaskell, - any.StateVar ==1.2.2, - any.aeson ==2.2.3.0, - aeson +ordered-keymap, - any.array ==0.5.4.0, - any.assoc ==1.1.1, - assoc -tagged, - any.base ==4.15.1.0, - any.base-orphans ==0.9.2, - any.bifunctors ==5.6.2, - bifunctors +tagged, - any.binary ==0.8.8.0, - any.bytestring ==0.10.12.1, - any.character-ps ==0.1, - any.comonad ==5.0.8, - comonad +containers +distributive +indexed-traversable, - any.containers ==0.6.4.1, - any.contravariant ==1.5.5, - contravariant +semigroups +statevar +tagged, - any.data-array-byte ==0.1.0.1, - any.data-fix ==0.3.3, - any.deepseq ==1.4.5.0, - any.directory ==1.3.6.2, - any.distributive ==0.6.2.1, - distributive +semigroups +tagged, - any.dlist ==1.0, - dlist -werror, - any.exceptions ==0.10.4, - any.fgl ==5.8.2.0, - fgl +containers042, - any.filepath ==1.4.2.1, - any.foldable1-classes-compat ==0.1, - foldable1-classes-compat +tagged, - any.generically ==0.1.1, - any.ghc-bignum ==1.1, - any.ghc-boot-th ==9.0.2, - any.ghc-prim ==0.7.0, - any.hashable ==1.4.6.0, - hashable -arch-native +integer-gmp -random-initial-seed, - any.indexed-traversable ==0.1.4, - any.indexed-traversable-instances ==0.1.2, - any.integer-conversion ==0.1.1, - any.integer-logarithms ==1.0.3.1, - integer-logarithms -check-bounds +integer-gmp, - any.mtl ==2.2.2, - any.network-uri ==2.6.4.2, - any.parsec ==3.1.14.0, - any.pretty ==1.1.3.6, - any.primitive ==0.9.0.0, - any.process ==1.6.13.2, - any.random ==1.2.1.2, - any.regex-base ==0.94.0.2, - any.regex-tdfa ==1.3.2.2, - regex-tdfa +doctest -force-o2, - any.rts ==1.0.2, - any.scientific ==0.3.8.0, - scientific -integer-simple, - any.semialign ==1.3.1, - semialign +semigroupoids, - any.semigroupoids ==6.0.1, - semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers, - any.splitmix ==0.1.0.5, - splitmix -optimised-mixer, - any.stm ==2.5.0.0, - any.strict ==0.5, - any.tagged ==0.8.8, - tagged +deepseq +transformers, - any.template-haskell ==2.17.0.0, - any.text ==1.2.5.0, - any.text-iso8601 ==0.1.1, - any.text-short ==0.1.6, - text-short -asserts, - any.th-abstraction ==0.7.0.0, - any.th-compat ==0.1.5, - any.these ==1.2.1, - any.time ==1.9.3, - any.time-compat ==1.9.7, - any.transformers ==0.5.6.2, - any.transformers-compat ==0.7.2, - transformers-compat -five +five-three -four +generic-deriving +mtl -three -two, - any.unix ==2.7.2.2, - any.unordered-containers ==0.2.20, - unordered-containers -debug, - any.uuid-types ==1.0.6, - any.vector ==0.13.1.0, - vector +boundschecks -internalchecks -unsafechecks -wall, - any.vector-stream ==0.1.0.1, - any.witherable ==0.5 -index-state: hackage.haskell.org 2024-06-18T02:21:19Z diff --git a/build/linux.armv6hf/scutil b/build/linux.armv6hf/scutil deleted file mode 100644 index a85d810..0000000 --- a/build/linux.armv6hf/scutil +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/dash -# Various ShellCheck build utility functions - -# Generally set a ulimit to avoid QEmu using too much memory -ulimit -v "$((10*1024*1024))" -# If we happen to invoke or run under QEmu, make sure to follow execve. -# This requires a patched QEmu. -export QEMU_EXECVE=1 - -# Retry a command until it succeeds -# Usage: scutil retry 3 mycmd -retry() { - n="$1" - ret=1 - shift - while [ "$n" -gt 0 ] - do - "$@" - ret=$? - [ "$ret" = 0 ] && break - n=$((n-1)) - done - return "$ret" -} - -# Install all dependencies from a freeze file -# Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install -install_from_freeze() { - linefeed=$(printf '\nx') - linefeed=${linefeed%x} - flags=$( - sed 's/constraints:/&\n /' "$1" | - grep -vw -e rts -e base | - sed -n -e 's/^ *\([^,]*\).*/\1/p' | - sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e') - shift - # shellcheck disable=SC2086 - ( IFS=$linefeed; set -x; "$@" $flags ) -} - -# Run a command under emulation. -# This assumes the correct emulator is named 'qemu' and the chroot is /chroot -# Usage: scutil emu echo "Hello World" -emu() { - chroot /chroot /bin/qemu /usr/bin/env "$@" -} - -"$@" diff --git a/build/linux.riscv64/Dockerfile b/build/linux.riscv64/Dockerfile deleted file mode 100644 index d138ff7..0000000 --- a/build/linux.riscv64/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -FROM ubuntu:24.04 - -ENV TARGETNAME linux.riscv64 -ENV TARGET riscv64-linux-gnu - -USER root -ENV DEBIAN_FRONTEND noninteractive - -# Init base -RUN apt-get update -y - -# Install qemu -RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools debootstrap -WORKDIR /qemu -RUN git clone --depth 1 https://github.com/koalaman/qemu . -RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror -RUN cd build && ninja qemu-riscv64 -ENV QEMU_EXECVE 1 - -# Convenience utility -COPY scutil /bin/scutil -# We have to copy to /usr/bin because debootstrap will try to symlink /bin and fail if it exists -COPY scutil /chroot/usr/bin/scutil -RUN chmod +x /bin/scutil /chroot/usr/bin/scutil - -# Set up a riscv64 userspace -WORKDIR / -RUN debootstrap --arch=riscv64 --variant=minbase --components=main,universe --foreign noble /chroot http://ports.ubuntu.com/ubuntu-ports -RUN cp /qemu/build/qemu-riscv64 /chroot/bin/qemu -RUN scutil emu /debootstrap/debootstrap --second-stage - -# Install deps in the chroot -RUN scutil emu apt-get update -RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install -RUN scutil emu cabal update - -# Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it. -COPY cabal.project.freeze /chroot/etc - -# Build all dependencies from the freeze file. The emulator segfaults at random, -# so retry a few times. -RUN scutil install_from_freeze /chroot/etc/cabal.project.freeze retry 5 emu cabal install --keep-going - -# Copy the build script -COPY build /chroot/bin/build -ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"] diff --git a/build/linux.riscv64/build b/build/linux.riscv64/build deleted file mode 100755 index ed9dc27..0000000 --- a/build/linux.riscv64/build +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh -set -xe -IFS=';' -{ - mkdir -p /tmp/scratch - cd /tmp/scratch - tar xzv --strip-components=1 - chmod +x striptests && ./striptests - # Use a freeze file to ensure we use the same dependencies we cached during - # the docker image build. We don't want to spend time compiling anything new. - cp /etc/cabal.project.freeze . - mkdir "$TARGETNAME" - # Retry in case of random segfault - scutil retry 3 cabal build --enable-executable-static - find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - "$TARGET-strip" -s "$TARGETNAME/shellcheck" - ls -l "$TARGETNAME" - "$TARGETNAME/shellcheck" --version -} >&2 -tar czv "$TARGETNAME" diff --git a/build/linux.riscv64/cabal.project.freeze b/build/linux.riscv64/cabal.project.freeze deleted file mode 100644 index cbb42e1..0000000 --- a/build/linux.riscv64/cabal.project.freeze +++ /dev/null @@ -1,93 +0,0 @@ -active-repositories: hackage.haskell.org:merge -constraints: any.Diff ==0.5, - any.OneTuple ==0.4.2, - any.QuickCheck ==2.14.3, - QuickCheck -old-random +templatehaskell, - any.StateVar ==1.2.2, - any.aeson ==2.2.3.0, - aeson +ordered-keymap, - any.array ==0.5.4.0, - any.assoc ==1.1.1, - assoc -tagged, - any.base ==4.17.2.0, - any.base-orphans ==0.9.2, - any.bifunctors ==5.6.2, - bifunctors +tagged, - any.binary ==0.8.9.1, - any.bytestring ==0.11.5.2, - any.character-ps ==0.1, - any.comonad ==5.0.8, - comonad +containers +distributive +indexed-traversable, - any.containers ==0.6.7, - any.contravariant ==1.5.5, - contravariant +semigroups +statevar +tagged, - any.data-fix ==0.3.3, - any.deepseq ==1.4.8.0, - any.directory ==1.3.7.1, - any.distributive ==0.6.2.1, - distributive +semigroups +tagged, - any.dlist ==1.0, - dlist -werror, - any.exceptions ==0.10.5, - any.fgl ==5.8.2.0, - fgl +containers042, - any.filepath ==1.4.2.2, - any.foldable1-classes-compat ==0.1, - foldable1-classes-compat +tagged, - any.generically ==0.1.1, - any.ghc-bignum ==1.3, - any.ghc-boot-th ==9.4.7, - any.ghc-prim ==0.9.1, - any.hashable ==1.4.6.0, - hashable -arch-native +integer-gmp -random-initial-seed, - any.indexed-traversable ==0.1.4, - any.indexed-traversable-instances ==0.1.2, - any.integer-conversion ==0.1.1, - any.integer-logarithms ==1.0.3.1, - integer-logarithms -check-bounds +integer-gmp, - any.mtl ==2.2.2, - any.network-uri ==2.6.4.2, - any.os-string ==2.0.3, - any.parsec ==3.1.16.1, - any.pretty ==1.1.3.6, - any.primitive ==0.9.0.0, - any.process ==1.6.17.0, - any.random ==1.2.1.2, - any.regex-base ==0.94.0.2, - any.regex-tdfa ==1.3.2.2, - regex-tdfa +doctest -force-o2, - any.rts ==1.0.2, - any.scientific ==0.3.8.0, - scientific -integer-simple, - any.semialign ==1.3.1, - semialign +semigroupoids, - any.semigroupoids ==6.0.1, - semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers, - any.splitmix ==0.1.0.5, - splitmix -optimised-mixer, - any.stm ==2.5.1.0, - any.strict ==0.5, - any.tagged ==0.8.8, - tagged +deepseq +transformers, - any.template-haskell ==2.19.0.0, - any.text ==2.0.2, - any.text-iso8601 ==0.1.1, - any.text-short ==0.1.6, - text-short -asserts, - any.th-abstraction ==0.7.0.0, - any.th-compat ==0.1.5, - any.these ==1.2.1, - any.time ==1.12.2, - any.time-compat ==1.9.7, - any.transformers ==0.5.6.2, - any.transformers-compat ==0.7.2, - transformers-compat -five +five-three -four +generic-deriving +mtl -three -two, - any.unix ==2.7.3, - any.unordered-containers ==0.2.20, - unordered-containers -debug, - any.uuid-types ==1.0.6, - any.vector ==0.13.1.0, - vector +boundschecks -internalchecks -unsafechecks -wall, - any.vector-stream ==0.1.0.1, - any.witherable ==0.5 -index-state: hackage.haskell.org 2024-06-17T00:48:51Z diff --git a/build/linux.riscv64/tag b/build/linux.riscv64/tag deleted file mode 100644 index 901eaaa..0000000 --- a/build/linux.riscv64/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-linux-riscv64 diff --git a/build/linux.x86_64/Dockerfile b/build/linux.x86_64/Dockerfile index edafb36..3112ac2 100644 --- a/build/linux.x86_64/Dockerfile +++ b/build/linux.x86_64/Dockerfile @@ -1,8 +1,4 @@ -FROM alpine:3.16 -# alpine:3.16 (GHC 9.0.1): 5.8 megabytes -# alpine:3.17 (GHC 9.0.2): 15.0 megabytes -# alpine:3.18 (GHC 9.4.4): 29.0 megabytes -# alpine:3.19 (GHC 9.4.7): 29.0 megabytes +FROM alpine:latest ENV TARGETNAME linux.x86_64 diff --git a/build/windows.x86_64/Dockerfile b/build/windows.x86_64/Dockerfile index 2ae78ac..1e5c5d9 100644 --- a/build/windows.x86_64/Dockerfile +++ b/build/windows.x86_64/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /haskell RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1 WORKDIR /haskell/bin RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip - -RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* . +RUN curl -L "https://curl.se/windows/dl-7.84.0/curl-7.84.0-win64-mingw.zip" | busybox unzip - && mv curl-7.84.0-win64-mingw/bin/* . ENV WINEPATH /haskell/bin # It's unknown whether Cabal on Windows suffers from the same issue diff --git a/build/windows.x86_64/build b/build/windows.x86_64/build index 22e5b42..7bf186e 100755 --- a/build/windows.x86_64/build +++ b/build/windows.x86_64/build @@ -8,6 +8,7 @@ set -xe tar xzv --strip-components=1 chmod +x striptests && ./striptests mkdir "$TARGETNAME" + cabal update ( IFS=';'; cabal build $CABALOPTS ) find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" diff --git a/shellcheck.1.md b/shellcheck.1.md index c768bfe..9675e79 100644 --- a/shellcheck.1.md +++ b/shellcheck.1.md @@ -56,13 +56,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts. options are cumulative, but all the codes can be specified at once, comma-separated as a single argument. -**--extended-analysis=true/false** - -: Enable/disable Dataflow Analysis to identify more issues (default true). If - ShellCheck uses too much CPU/RAM when checking scripts with several - thousand lines of code, extended analysis can be disabled with this flag - or a directive. This flag overrides directives and rc files. - **-f** *FORMAT*, **--format=***FORMAT* : Specify the output format of shellcheck, which prints its results in the @@ -78,11 +71,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts. : Don't try to look for .shellcheckrc configuration files. -**--rcfile** *RCFILE* - -: Prefer the specified configuration file over searching for one - in the default locations. - **-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...] : Enable optional checks. The special name *all* enables all of them. @@ -97,8 +85,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts. **-s**\ *shell*,\ **--shell=***shell* -: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*, - and *busybox*. +: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*. The default is to deduce the shell from the file's `shell` directive, shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to POSIX `sh` (not the system's), and will warn of portability issues. @@ -256,12 +243,6 @@ Valid keys are: : Enable an optional check by name, as listed with **--list-optional**. Only file-wide `enable` directives are considered. -**extended-analysis** -: Set to true/false to enable/disable dataflow analysis. Specifying - `# shellcheck extended-analysis=false` in particularly large (2000+ line) - auto-generated scripts will reduce ShellCheck's resource usage at the - expense of certain checks. Extended analysis is enabled by default. - **external-sources** : Set to `true` in `.shellcheckrc` to always allow ShellCheck to open arbitrary files from 'source' statements (the way most tools do). @@ -317,7 +298,7 @@ Here is an example `.shellcheckrc`: disable=SC2236 If no `.shellcheckrc` is found in any of the parent directories, ShellCheck -will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME` +will look in `~/.shellcheckrc` followed by the XDG config directory (usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on Windows. Only the first file found will be used. @@ -397,10 +378,10 @@ long list of wonderful contributors. # COPYRIGHT -Copyright 2012-2024, Vidar Holen and contributors. +Copyright 2012-2022, Vidar Holen and contributors. Licensed under the GNU General Public License version 3 or later, see https://gnu.org/licenses/gpl.html # SEE ALSO -sh(1) bash(1) dash(1) ksh(1) +sh(1) bash(1) diff --git a/shellcheck.hs b/shellcheck.hs index def3654..4e8a155 100644 --- a/shellcheck.hs +++ b/shellcheck.hs @@ -76,8 +76,7 @@ data Options = Options { externalSources :: Bool, sourcePaths :: [FilePath], formatterOptions :: FormatterOptions, - minSeverity :: Severity, - rcfile :: Maybe FilePath + minSeverity :: Severity } defaultOptions = Options { @@ -87,8 +86,7 @@ defaultOptions = Options { formatterOptions = newFormatterOptions { foColorOption = ColorAuto }, - minSeverity = StyleC, - rcfile = Nothing + minSeverity = StyleC } usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." @@ -102,8 +100,6 @@ options = [ (ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings", Option "e" ["exclude"] (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings", - Option "" ["extended-analysis"] - (ReqArg (Flag "extended-analysis") "bool") "Perform dataflow analysis (default true)", Option "f" ["format"] (ReqArg (Flag "format") "FORMAT") $ "Output format (" ++ formatList ++ ")", @@ -111,9 +107,6 @@ options = [ (NoArg $ Flag "list-optional" "true") "List checks disabled by default", Option "" ["norc"] (NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files", - Option "" ["rcfile"] - (ReqArg (Flag "rcfile") "RCFILE") - "Prefer the specified configuration file over searching for one", Option "o" ["enable"] (ReqArg (Flag "enable") "check1,check2..") "List of optional checks to enable (or 'all')", @@ -122,7 +115,7 @@ options = [ "Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)", Option "s" ["shell"] (ReqArg (Flag "shell") "SHELLNAME") - "Specify dialect (sh, bash, dash, ksh, busybox)", + "Specify dialect (sh, bash, dash, ksh)", Option "S" ["severity"] (ReqArg (Flag "severity") "SEVERITY") "Minimum severity of errors to consider (error, warning, info, style)", @@ -259,9 +252,9 @@ runFormatter sys format options files = do else SomeProblems parseEnum name value list = - case lookup value list of - Just value -> return value - Nothing -> do + case filter ((== value) . fst) list of + [(name, value)] -> return value + [] -> do printErr $ "Unknown value for --" ++ name ++ ". " ++ "Valid options are: " ++ (intercalate ", " $ map fst list) throwError SupportFailure @@ -374,11 +367,6 @@ parseOption flag options = } } - Flag "rcfile" str -> do - return options { - rcfile = Just str - } - Flag "enable" value -> let cs = checkSpec options in return options { checkSpec = cs { @@ -386,14 +374,6 @@ parseOption flag options = } } - Flag "extended-analysis" str -> do - value <- parseBool str - return options { - checkSpec = (checkSpec options) { - csExtendedAnalysis = Just value - } - } - -- This flag is handled specially in 'process' Flag "format" _ -> return options @@ -411,20 +391,12 @@ parseOption flag options = throwError SyntaxFailure return (Prelude.read num :: Integer) - parseBool str = do - case str of - "true" -> return True - "false" -> return False - _ -> do - printErr $ "Invalid boolean, expected true/false: " ++ str - throwError SyntaxFailure - ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO) ioInterface options files = do inputs <- mapM normalize files cache <- newIORef emptyCache configCache <- newIORef ("", Nothing) - return (newSystemInterface :: SystemInterface IO) { + return SystemInterface { siReadFile = get cache inputs, siFindSource = findSourceFile inputs (sourcePaths options), siGetConfig = getConfig configCache @@ -469,33 +441,18 @@ ioInterface options files = do fallback :: FilePath -> IOException -> IO FilePath fallback path _ = return path - -- Returns the name and contents of .shellcheckrc for the given file - getConfig cache filename = - case rcfile options of - Just file -> do - -- We have a specified rcfile. Ignore normal rcfile resolution. - (path, result) <- readIORef cache - if path == "/" - then return result - else do - result <- readConfig file - when (isNothing result) $ - hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file - writeIORef cache ("/", result) - return result - - Nothing -> do - path <- normalize filename - let dir = takeDirectory path - (previousPath, result) <- readIORef cache - if dir == previousPath - then return result - else do - paths <- getConfigPaths dir - result <- findConfig paths - writeIORef cache (dir, result) - return result + getConfig cache filename = do + path <- normalize filename + let dir = takeDirectory path + (previousPath, result) <- readIORef cache + if dir == previousPath + then return result + else do + paths <- getConfigPaths dir + result <- findConfig paths + writeIORef cache (dir, result) + return result findConfig paths = case paths of @@ -533,7 +490,7 @@ ioInterface options files = do where handler :: FilePath -> IOException -> IO (String, Bool) handler file err = do - hPutStrLn stderr $ file ++ ": " ++ show err + putStrLn $ file ++ ": " ++ show err return ("", True) andM a b arg = do diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index f294c4e..e14b854 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -23,7 +23,7 @@ description: | # snap connect shellcheck:removable-media version: git -base: core20 +base: core18 grade: stable confinement: strict @@ -40,16 +40,16 @@ parts: source: . build-packages: - cabal-install - stage-packages: - - libatomic1 + - squid override-build: | - # Give ourselves enough memory to build - dd if=/dev/zero of=/tmp/swap bs=1M count=2000 - mkswap /tmp/swap - swapon /tmp/swap - + # See comments in .snapsquid.conf + [ "$http_proxy" ] && { + squid3 -f .snapsquid.conf + export http_proxy="http://localhost:8888" + sleep 3 + } cabal sandbox init - cabal update + cabal update || cat /var/log/squid/* cabal install -j install -d $SNAPCRAFT_PART_INSTALL/usr/bin diff --git a/src/ShellCheck/AST.hs b/src/ShellCheck/AST.hs index bafe035..5c20416 100644 --- a/src/ShellCheck/AST.hs +++ b/src/ShellCheck/AST.hs @@ -138,7 +138,7 @@ data InnerToken t = | Inner_T_WhileExpression [t] [t] | Inner_T_Annotation [Annotation] t | Inner_T_Pipe String - | Inner_T_CoProc (Maybe Token) t + | Inner_T_CoProc (Maybe String) t | Inner_T_CoProcBody t | Inner_T_Include t | Inner_T_SourceCommand t t @@ -152,7 +152,6 @@ data Annotation = | ShellOverride String | SourcePath String | ExternalSources Bool - | ExtendedAnalysis Bool deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) @@ -206,7 +205,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t) pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c) pattern T_Array id t = OuterToken id (Inner_T_Array t) pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l) -pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t) +pattern TA_Parentesis id t = OuterToken id (Inner_TA_Parenthesis t) pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value) pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3) pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1) @@ -259,7 +258,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l) pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l) pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) -{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parenthesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-} +{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parentesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-} instance Eq Token where OuterToken _ a == OuterToken _ b = a == b diff --git a/src/ShellCheck/ASTLib.hs b/src/ShellCheck/ASTLib.hs index 1e1b9cd..56903ee 100644 --- a/src/ShellCheck/ASTLib.hs +++ b/src/ShellCheck/ASTLib.hs @@ -31,7 +31,6 @@ import Data.Functor import Data.Functor.Identity import Data.List import Data.Maybe -import qualified Data.List.NonEmpty as NE import qualified Data.Map as Map import Numeric (showHex) @@ -158,10 +157,9 @@ isFlag token = _ -> False -- Is this token a flag where the - is unquoted? -isUnquotedFlag token = - case getLeadingUnquotedString token of - Just ('-':_) -> True - _ -> False +isUnquotedFlag token = fromMaybe False $ do + str <- getLeadingUnquotedString token + return $ "-" `isPrefixOf` str -- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read` -- -re -d : -u 3 bar @@ -446,12 +444,6 @@ getLiteralStringExt more = g -- Is this token a string literal? isLiteral t = isJust $ getLiteralString t --- Is this token a string literal number? -isLiteralNumber t = fromMaybe False $ do - s <- getLiteralString t - guard $ all isDigit s - return True - -- Escape user data for messages. -- Messages generally avoid repeating user data, but sometimes it's helpful. e4m = escapeForMessage @@ -766,8 +758,8 @@ prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash" prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash" prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash" -prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh" -prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash" +prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash" +prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash" -- Get the shell executable from a string like '/usr/bin/env bash' executableFromShebang :: String -> String @@ -784,8 +776,7 @@ executableFromShebang = shellFor [x] -> basename x (first:second:args) | basename first == "busybox" -> case basename second of - "sh" -> "busybox sh" - "ash" -> "busybox ash" + "sh" -> "ash" -- busybox sh is ash x -> x (first:args) | basename first == "env" -> fromEnvArgs args @@ -865,7 +856,8 @@ getBracedModifier s = headOrDefault "" $ do -- Get the variables from indices like ["x", "y"] in ${var[x+y+1]} prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"] getIndexReferences s = fromMaybe [] $ do - index:_ <- matchRegex re s + match <- matchRegex re s + index <- match !!! 0 return $ matchAllStrings variableNameRegex index where re = mkRegex "(\\[.*\\])" @@ -876,7 +868,8 @@ prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"] prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"] getOffsetReferences mods = fromMaybe [] $ do -- if mods start with [, then drop until ] - _:offsets:_ <- matchRegex re mods + match <- matchRegex re mods + offsets <- match !!! 1 return $ matchAllStrings variableNameRegex offsets where re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)" @@ -893,17 +886,11 @@ isUnmodifiedParameterExpansion t = in getBracedReference str == str _ -> False --- Return the referenced variable if (and only if) it's an unmodified parameter expansion. -getUnmodifiedParameterExpansion t = - case t of - T_DollarBraced _ _ list -> do - let str = concat $ oversimplify list - guard $ getBracedReference str == str - return str - _ -> Nothing - --- A list of the element and all its parents up to the root node. -getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree) +getPath tree t = t : + case Map.lookup (getId t) tree of + Nothing -> [] + Just parent -> getPath tree parent isClosingFileOp op = case op of @@ -916,11 +903,5 @@ getEnableDirectives root = T_Annotation _ list _ -> [s | EnableComment s <- list] _ -> [] -getExtendedAnalysisDirective :: Token -> Maybe Bool -getExtendedAnalysisDirective root = - case root of - T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list] - _ -> Nothing - return [] runTests = $quickCheckAll diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 2e9a3bd..1f6d96d 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -1,5 +1,5 @@ {- - Copyright 2012-2024 Vidar Holen + Copyright 2012-2022 Vidar Holen This file is part of ShellCheck. https://www.shellcheck.net @@ -19,7 +19,6 @@ -} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE PatternGuards #-} module ShellCheck.Analytics (checker, optionalChecks, ShellCheck.Analytics.runTests) where import ShellCheck.AST @@ -47,7 +46,6 @@ import Data.Maybe import Data.Ord import Data.Semigroup import Debug.Trace -- STRIP -import qualified Data.List.NonEmpty as NE import qualified Data.Map.Strict as Map import qualified Data.Set as S import Test.QuickCheck.All (forAllProperties) @@ -103,7 +101,8 @@ nodeChecksToTreeCheck checkList = nodeChecks :: [Parameters -> Token -> Writer [TokenComment] ()] nodeChecks = [ - checkPipePitfalls + checkUuoc + ,checkPipePitfalls ,checkForInQuoted ,checkForInLs ,checkShorthandIf @@ -123,7 +122,6 @@ nodeChecks = [ ,checkCaseAgainstGlob ,checkCommarrays ,checkOrNeq - ,checkAndEq ,checkEchoWc ,checkConstantIfs ,checkPipedAssignment @@ -203,9 +201,6 @@ nodeChecks = [ ,checkOverwrittenExitCode ,checkUnnecessaryArithmeticExpansionIndex ,checkUnnecessaryParens - ,checkPlusEqualsNumber - ,checkExpansionWithRedirection - ,checkUnaryTestA ] optionalChecks = map fst optionalTreeChecks @@ -274,13 +269,6 @@ optionalTreeChecks = [ cdPositive = "rm -r \"$(get_chroot_dir)/home\"", cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\"" }, checkExtraMaskedReturns) - - ,(newCheckDescription { - cdName = "useless-use-of-cat", - cdDescription = "Check for Useless Use Of Cat (UUOC)", - cdPositive = "cat foo | grep bar", - cdNegative = "grep bar foo" - }, nodeChecksToTreeCheck [checkUuoc]) ] optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment]) @@ -355,11 +343,13 @@ dist a b hasFloatingPoint params = shellType params == Ksh -- Checks whether the current parent path is part of a condition -isCondition (x NE.:| xs) = foldr go (const False) xs x +isCondition [] = False +isCondition [_] = False +isCondition (child:parent:rest) = + case child of + T_BatsTest {} -> True -- count anything in a @test as conditional + _ -> getId child `elem` map getId (getConditionChildren parent) || isCondition (parent:rest) where - go _ _ T_BatsTest{} = True -- count anything in a @test as conditional - go parent go_rest child = - getId child `elem` map getId (getConditionChildren parent) || go_rest parent getConditionChildren t = case t of T_AndIf _ left right -> [left] @@ -476,8 +466,9 @@ checkAssignAteCommand _ (T_SimpleCommand id [T_Assignment _ _ _ _ assignmentTerm where isCommonCommand (Just s) = s `elem` commonCommands isCommonCommand _ = False - firstWordIsArg (head:_) = isGlob head || isUnquotedFlag head - firstWordIsArg [] = False + firstWordIsArg list = fromMaybe False $ do + head <- list !!! 0 + return $ isGlob head || isUnquotedFlag head checkAssignAteCommand _ _ = return () @@ -498,13 +489,18 @@ prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2" checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _ val] []) = sequence_ $ do str <- getNormalString val - var:op:_ <- matchRegex regex str - guard $ S.member var references + match <- matchRegex regex str + var <- match !!! 0 + op <- match !!! 1 + Map.lookup var references return . warn (getId val) 2100 $ "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))" where regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$" - references = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params] + references = foldl (flip ($)) Map.empty (map insertRef $ variableFlow params) + insertRef (Assignment (_, _, name, _)) = + Map.insert name () + insertRef _ = Prelude.id getNormalString (T_NormalWord _ words) = do parts <- mapM getLiterals words @@ -566,7 +562,7 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do hasParameter "print0", hasParameter "printf" ]) $ warn (getId find) 2038 - "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames." + "Use -print0/-0 or -exec + to allow for non-alphanumeric filenames." for ["ps", "grep"] $ \(ps:grep:_) -> @@ -649,10 +645,10 @@ prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue" prop_checkShebang10 = verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue" prop_checkShebang11 = verifyTree checkShebang "#!/bin/sh/\ntrue" prop_checkShebang12 = verifyTree checkShebang "#!/bin/sh/ -xe\ntrue" -prop_checkShebang13 = verifyNotTree checkShebang "#!/bin/busybox sh" +prop_checkShebang13 = verifyTree checkShebang "#!/bin/busybox sh" prop_checkShebang14 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=sh\n" prop_checkShebang15 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=dash\n" -prop_checkShebang16 = verifyNotTree checkShebang "#!/bin/busybox ash" +prop_checkShebang16 = verifyTree checkShebang "#!/bin/busybox ash" prop_checkShebang17 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=dash\n" prop_checkShebang18 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=sh\n" checkShebang params (T_Annotation _ list t) = @@ -849,14 +845,14 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) = getRedirs _ = [] special x = "/dev/" `isPrefixOf` concat (oversimplify x) isInput t = - case NE.tail $ getPath (parentMap params) t of + case drop 1 $ getPath (parentMap params) t of T_IoFile _ op _:_ -> case op of T_Less _ -> True _ -> False _ -> False isOutput t = - case NE.tail $ getPath (parentMap params) t of + case drop 1 $ getPath (parentMap params) t of T_IoFile _ op _:_ -> case op of T_Greater _ -> True @@ -882,16 +878,13 @@ prop_checkShorthandIf5 = verifyNot checkShorthandIf "foo && rm || printf b" prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi" prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done" prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi" -prop_checkShorthandIf9 = verifyNot checkShorthandIf "foo && [ -x /file ] || bar" -prop_checkShorthandIf10 = verifyNot checkShorthandIf "foo && bar || true" -prop_checkShorthandIf11 = verify checkShorthandIf "foo && bar || false" -checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ b) (T_Pipeline _ _ t)) - | not (isOk t || inCondition) && not (isTestCommand b) = +checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ _) (T_Pipeline _ _ t)) + | not (isOk t || inCondition) = info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true." where isOk [t] = isAssignment t || fromMaybe False (do name <- getCommandBasename t - return $ name `elem` ["echo", "exit", "return", "printf", "true", ":"]) + return $ name `elem` ["echo", "exit", "return", "printf"]) isOk _ = False inCondition = isCondition $ getPath (parentMap params) x checkShorthandIf _ _ = return () @@ -981,32 +974,32 @@ prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr prop_checkArrayWithoutIndex10 = verifyTree checkArrayWithoutIndex "read -ra arr <<< 'foo bar'; echo \"$arr\"" prop_checkArrayWithoutIndex11 = verifyNotTree checkArrayWithoutIndex "read -rpfoobar r; r=42" checkArrayWithoutIndex params _ = - doVariableFlowAnalysis readF writeF defaultSet (variableFlow params) + doVariableFlowAnalysis readF writeF defaultMap (variableFlow params) where - defaultSet = S.fromList arrayVariables + defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables readF _ (T_DollarBraced id _ token) _ = do - s <- get + map <- get return . maybeToList $ do name <- getLiteralString token - guard $ S.member name s + assigned <- Map.lookup name map return $ makeComment WarningC id 2128 "Expanding an array without an index only gives the first element." readF _ _ _ = return [] writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do - isArray <- gets (S.member name) + isArray <- gets (Map.member name) return $ if not isArray then [] else case mode of Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."] Append -> [makeComment WarningC id 2179 "Use array+=(\"item\") to append items to an array."] writeF _ t name (DataArray _) = do - modify (S.insert name) + modify (Map.insert name ()) return [] writeF _ expr name _ = do if isIndexed expr - then modify (S.insert name) - else modify (S.delete name) + then modify (Map.insert name ()) + else modify (Map.delete name) return [] isIndexed expr = @@ -1093,7 +1086,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) = return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else if name == "mumps" then getMumpsCommand cmd else name isProbablyOk = - any isOkAssignment (NE.take 3 $ getPath parents t) + any isOkAssignment (take 3 $ getPath parents t) || commandName `elem` [ "trap" ,"sh" @@ -1107,7 +1100,6 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) = ,"sudo" -- covering "sudo sh" and such ,"docker" -- like above ,"podman" - ,"oc" ,"dpkg-query" ,"jq" -- could also check that user provides --arg ,"rename" @@ -1211,7 +1203,6 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do case shellType params of Sh -> return () -- These are unsupported and will be caught by bashism checks. Dash -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting." - BusyboxSh -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting." _ -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting (or switch to [[ .. ]])." when (op `elem` arithmeticBinaryTestOps) $ do @@ -1272,8 +1263,7 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do str = concat $ oversimplify c var = getBracedReference str in fromMaybe False $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id + state <- CF.getIncomingState (cfgAnalysis params) id value <- Map.lookup var $ CF.variablesInScope state return $ CF.numericalStatus (CF.variableValue value) >= CF.NumericalStatusMaybe _ -> @@ -1451,14 +1441,14 @@ prop_checkConstantNullary5 = verify checkConstantNullary "[[ true ]]" prop_checkConstantNullary6 = verify checkConstantNullary "[ 1 ]" prop_checkConstantNullary7 = verify checkConstantNullary "[ false ]" checkConstantNullary _ (TC_Nullary _ _ t) | isConstant t = - case onlyLiteralString t of + case fromMaybe "" $ getLiteralString t of "false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets." "0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead." "true" -> style (getId t) 2160 "Instead of '[ true ]', just use 'true'." "1" -> style (getId t) 2161 "Instead of '[ 1 ]', use 'true'." _ -> err (getId t) 2078 "This expression is constant. Did you forget a $ somewhere?" where - string = onlyLiteralString t + string = fromMaybe "" $ getLiteralString t checkConstantNullary _ _ = return () @@ -1467,8 +1457,9 @@ prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" checkForDecimals params t@(TA_Expansion id _) = sequence_ $ do guard $ not (hasFloatingPoint params) - first:rest <- getLiteralString t - guard $ isDigit first && '.' `elem` rest + str <- getLiteralString t + first <- str !!! 0 + guard $ isDigit first && '.' `elem` str return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." checkForDecimals _ _ = return () @@ -1502,7 +1493,7 @@ checkArithmeticDeref params t@(TA_Expansion _ [T_DollarBraced id _ l]) = where isException [] = True isException s@(h:_) = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit h - getWarning = fromMaybe noWarning . msum . NE.map warningFor $ parents params t + getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t warningFor t = case t of T_Arithmetic {} -> return normalWarning @@ -1533,7 +1524,6 @@ prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = * prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]" prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]" prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]" -prop_checkComparisonAgainstGlob7 = verify checkComparisonAgainstGlob "#!/bin/busybox sh\n[[ $f == *foo* ]]" checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _ _])) | op `elem` ["=", "==", "!="] = warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching." @@ -1541,14 +1531,10 @@ checkComparisonAgainstGlob params (TC_Binary _ SingleBracket op _ word) | op `elem` ["=", "==", "!="] && isGlob word = err (getId word) 2081 msg where - msg = if (shellType params) `elem` [Bash, Ksh] -- Busybox does not support glob matching + msg = if isBashLike params then "[ .. ] can't match globs. Use [[ .. ]] or case statement." else "[ .. ] can't match globs. Use a case statement." -checkComparisonAgainstGlob params (TC_Binary _ DoubleBracket op _ word) - | shellType params == BusyboxSh && op `elem` ["=", "==", "!="] && isGlob word = - err (getId word) 2330 "BusyBox [[ .. ]] does not support glob matching. Use a case statement." - checkComparisonAgainstGlob _ _ = return () prop_checkCaseAgainstGlob1 = verify checkCaseAgainstGlob "case foo in lol$n) foo;; esac" @@ -1635,64 +1621,6 @@ checkOrNeq _ (T_OrIf id lhs rhs) = sequence_ $ do checkOrNeq _ _ = return () -prop_checkAndEq1 = verifyNot checkAndEq "cow=0; foo=0; if [[ $lol -eq cow && $lol -eq foo ]]; then echo foo; fi" -prop_checkAndEq2 = verifyNot checkAndEq "lol=0 foo=0; (( a==lol && a==foo ))" -prop_checkAndEq3 = verify checkAndEq "[ \"$a\" = lol && \"$a\" = foo ]" -prop_checkAndEq4 = verifyNot checkAndEq "[ a = $cow && b = $foo ]" -prop_checkAndEq5 = verifyNot checkAndEq "[[ $a = /home && $a = */public_html/* ]]" -prop_checkAndEq6 = verify checkAndEq "[ $a = a ] && [ $a = b ]" -prop_checkAndEq7 = verify checkAndEq "[ $a = a ] && [ $a = b ] || true" -prop_checkAndEq8 = verifyNot checkAndEq "[[ $a == x && $a == x ]]" -prop_checkAndEq9 = verifyNot checkAndEq "[ 0 -eq $FOO ] && [ 0 -eq $BAR ]" -prop_checkAndEq10 = verify checkAndEq "(( a == 1 && a == 2 ))" -prop_checkAndEq11 = verify checkAndEq "[ $x -eq 1 ] && [ $x -eq 2 ]" -prop_checkAndEq12 = verify checkAndEq "[ 1 -eq $x ] && [ $x -eq 2 ]" -prop_checkAndEq13 = verifyNot checkAndEq "[ 1 -eq $x ] && [ $x -eq 1 ]" -prop_checkAndEq14 = verifyNot checkAndEq "[ $a = $b ] && [ $a = $c ]" - -checkAndEqOperands "-eq" rhs1 rhs2 = isLiteralNumber rhs1 && isLiteralNumber rhs2 -checkAndEqOperands op rhs1 rhs2 | op == "=" || op == "==" = isLiteral rhs1 && isLiteral rhs2 -checkAndEqOperands _ _ _ = False - --- For test-level "and": [ x = y -a x = z ] -checkAndEq _ (TC_And id typ op (TC_Binary _ _ op1 lhs1 rhs1 ) (TC_Binary _ _ op2 lhs2 rhs2)) - | op1 == op2 && lhs1 == lhs2 && rhs1 /= rhs2 && checkAndEqOperands op1 rhs1 rhs2 = - warn id 2333 $ "You probably wanted " ++ (if typ == SingleBracket then "-o" else "||") ++ " here, otherwise it's always false." - --- For arithmetic context "and" -checkAndEq _ (TA_Binary id "&&" (TA_Binary _ "==" lhs1 rhs1) (TA_Binary _ "==" lhs2 rhs2)) - | lhs1 == lhs2 && isLiteralNumber rhs1 && isLiteralNumber rhs2 = - warn id 2334 "You probably wanted || here, otherwise it's always false." - --- For command level "and": [ x = y ] && [ x = z ] -checkAndEq _ (T_AndIf id lhs rhs) = sequence_ $ do - (lhs1, op1, rhs1) <- getExpr lhs - (lhs2, op2, rhs2) <- getExpr rhs - guard $ op1 == op2 - guard $ lhs1 == lhs2 && rhs1 /= rhs2 - guard $ checkAndEqOperands op1 rhs1 rhs2 - return $ warn id 2333 "You probably wanted || here, otherwise it's always false." - where - getExpr x = - case x of - T_AndIf _ lhs _ -> getExpr lhs -- Fetches x and y in `T_AndIf x (T_AndIf y z)` - T_Pipeline _ _ [x] -> getExpr x - T_Redirecting _ _ c -> getExpr c - T_Condition _ _ c -> getExpr c - TC_Binary _ _ op lhs rhs -> orient (lhs, op, rhs) - _ -> Nothing - - -- Swap items so that the constant side is rhs (or Nothing if both/neither is constant) - orient (lhs, op, rhs) = - case (isConstant lhs, isConstant rhs) of - (True, False) -> return (rhs, op, lhs) - (False, True) -> return (lhs, op, rhs) - _ -> Nothing - - -checkAndEq _ _ = return () - - prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]" prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]" prop_checkValidCondOps2a = verifyNot checkValidCondOps "[ 3 \\> 2 ]" @@ -1893,7 +1821,7 @@ checkInexplicablyUnquoted params (T_NormalWord id tokens) = mapM_ check (tails t T_Literal id s | not (quotesSingleThing a && quotesSingleThing b || s `elem` ["=", ":", "/"] - || isSpecial (NE.toList $ getPath (parentMap params) trapped) + || isSpecial (getPath (parentMap params) trapped) ) -> warnAboutLiteral id _ -> return () @@ -1958,9 +1886,7 @@ prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.l prop_checkSpuriousExec9 = verify checkSpuriousExec "for file in rc.d/*; do exec \"$file\"; done" prop_checkSpuriousExec10 = verifyNot checkSpuriousExec "exec file; r=$?; printf >&2 'failed\n'; return $r" prop_checkSpuriousExec11 = verifyNot checkSpuriousExec "exec file; :" -prop_checkSpuriousExec12 = verifyNot checkSpuriousExec "#!/bin/bash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;" -prop_checkSpuriousExec13 = verify checkSpuriousExec "#!/bin/dash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;" -checkSpuriousExec params t = when (not $ hasExecfail params) $ doLists t +checkSpuriousExec _ = doLists where doLists (T_Script _ _ cmds) = doList cmds False doLists (T_BraceGroup _ cmds) = doList cmds False @@ -2113,7 +2039,7 @@ doVariableFlowAnalysis readFunc writeFunc empty flow = evalState ( -- from $foo=bar to foo=bar. This is not pretty but ok. quotesMayConflictWithSC2281 params t = case getPath (parentMap params) t of - _ NE.:| T_NormalWord parentId (me:T_Literal _ ('=':_):_) : T_SimpleCommand _ _ (cmd:_) : _ -> + _ : T_NormalWord parentId (me:T_Literal _ ('=':_):_) : T_SimpleCommand _ _ (cmd:_) : _ -> (getId t) == (getId me) && (parentId == getId cmd) _ -> False @@ -2208,8 +2134,7 @@ checkSpacefulnessCfg' dirtyPass params token@(T_DollarBraced id _ list) = addDoubleQuotesAround params token where - bracedString = concat $ oversimplify list - name = getBracedReference bracedString + name = getBracedReference $ concat $ oversimplify list parents = parentMap params needsQuoting = not (isArrayExpansion token) -- There's another warning for this @@ -2219,8 +2144,7 @@ checkSpacefulnessCfg' dirtyPass params token@(T_DollarBraced id _ list) = && not (usedAsCommandName parents token) isClean = fromMaybe False $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id + state <- CF.getIncomingState (cfgAnalysis params) id value <- Map.lookup name $ CF.variablesInScope state return $ isCleanState value @@ -2229,10 +2153,14 @@ checkSpacefulnessCfg' dirtyPass params token@(T_DollarBraced id _ list) = || CF.spaceStatus (CF.variableValue state) == CF.SpaceStatusClean isDefaultAssignment parents token = - let modifier = getBracedModifier bracedString in + let modifier = getBracedModifier $ bracedString token in any (`isPrefixOf` modifier) ["=", ":="] && isParamTo parents ":" token + -- Given a T_DollarBraced, return a simplified version of the string contents. + bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l + bracedString _ = error $ pleaseReport "bracedString on non-variable" + checkSpacefulnessCfg' _ _ _ = return () @@ -2349,7 +2277,7 @@ checkFunctionsUsedExternally params t = (Just str, t) -> do let name = basename str let args = skipOver t argv - let argStrings = map (\x -> (onlyLiteralString x, x)) args + let argStrings = map (\x -> (fromMaybe "" $ getLiteralString x, x)) args let candidates = getPotentialCommands name argStrings mapM_ (checkArg name (getId t)) candidates _ -> return () @@ -2448,9 +2376,15 @@ prop_checkUnused51 = verifyTree checkUnusedAssignments "x[y[z=1]]=1; echo ${x[@] checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) where flow = variableFlow params - references = Map.union (Map.fromList [(stripSuffix name, ()) | Reference (base, token, name) <- flow]) defaultMap + references = foldl (flip ($)) defaultMap (map insertRef flow) + insertRef (Reference (base, token, name)) = + Map.insert (stripSuffix name) () + insertRef _ = id - assignments = Map.fromList [(name, token) | Assignment (_, token, name, _) <- flow, isVariableName name] + assignments = foldl (flip ($)) Map.empty (map insertAssignment flow) + insertAssignment (Assignment (_, token, name, _)) | isVariableName name = + Map.insert name token + insertAssignment _ = id unused = Map.assocs $ Map.difference assignments references @@ -2514,7 +2448,6 @@ prop_checkUnassignedReferences_minusZDefault = verifyNotTree checkUnassignedRefe prop_checkUnassignedReferences50 = verifyNotTree checkUnassignedReferences "echo ${foo:+bar}" prop_checkUnassignedReferences51 = verifyNotTree checkUnassignedReferences "echo ${foo:+$foo}" prop_checkUnassignedReferences52 = verifyNotTree checkUnassignedReferences "wait -p pid; echo $pid" -prop_checkUnassignedReferences53 = verifyTree checkUnassignedReferences "x=($foo)" checkUnassignedReferences = checkUnassignedReferences' False checkUnassignedReferences' includeGlobals params t = warnings @@ -2570,12 +2503,14 @@ checkUnassignedReferences' includeGlobals params t = warnings warnings = execWriter . sequence $ mapMaybe warningFor unassigned - -- ${foo[bar baz]} may not be referencing bar/baz. Just skip these. + -- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays. + -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these. -- We can also have ${foo:+$foo} should be treated like [[ -n $foo ]] && echo $foo isException var t = any shouldExclude $ getPath (parentMap params) t where shouldExclude t = case t of + T_Array {} -> True (T_DollarBraced _ _ l) -> let str = concat $ oversimplify l ref = getBracedReference str @@ -2718,7 +2653,7 @@ checkPrefixAssignmentReference params t@(T_DollarBraced id _ value) = check path where name = getBracedReference $ concat $ oversimplify value - path = NE.toList $ getPath (parentMap params) t + path = getPath (parentMap params) t idPath = map getId path check [] = return () @@ -2767,7 +2702,7 @@ checkCharRangeGlob p t@(T_Glob id str) | return $ isCommandMatch cmd (`elem` ["tr", "read"]) -- Check if this is a dereferencing context like [[ -v array[operandhere] ]] - isDereferenced = fromMaybe False . msum . NE.map isDereferencingOp . getPath (parentMap p) + isDereferenced = fromMaybe False . msum . map isDereferencingOp . getPath (parentMap p) isDereferencingOp t = case t of TC_Binary _ DoubleBracket str _ _ -> return $ isDereferencingBinaryOp str @@ -2818,18 +2753,19 @@ prop_checkLoopKeywordScope5 = verify checkLoopKeywordScope "if true; then break; prop_checkLoopKeywordScope6 = verify checkLoopKeywordScope "while true; do true | { break; }; done" prop_checkLoopKeywordScope7 = verifyNot checkLoopKeywordScope "#!/bin/ksh\nwhile true; do true | { break; }; done" checkLoopKeywordScope params t | - Just name <- getCommandName t, name `elem` ["continue", "break"] = - if any isLoop path - then case map subshellType $ filter (not . isFunction) path of + name `elem` map Just ["continue", "break"] = + if not $ any isLoop path + then if any isFunction $ take 1 path + -- breaking at a source/function invocation is an abomination. Let's ignore it. + then err (getId t) 2104 $ "In functions, use return instead of " ++ fromJust name ++ "." + else err (getId t) 2105 $ fromJust name ++ " is only valid in loops." + else case map subshellType $ filter (not . isFunction) path of Just str:_ -> warn (getId t) 2106 $ "This only exits the subshell caused by the " ++ str ++ "." _ -> return () - else case path of - -- breaking at a source/function invocation is an abomination. Let's ignore it. - h:_ | isFunction h -> err (getId t) 2104 $ "In functions, use return instead of " ++ name ++ "." - _ -> err (getId t) 2105 $ name ++ " is only valid in loops." where - path = let p = getPath (parentMap params) t in NE.filter relevant p + name = getCommandName t + path = let p = getPath (parentMap params) t in filter relevant p subshellType t = case leadType params t of NoneScope -> Nothing SubshellScope str -> return str @@ -2848,7 +2784,6 @@ checkFunctionDeclarations params when (hasKeyword && hasParens) $ err id 2111 "ksh does not allow 'function' keyword and '()' at the same time." Dash -> forSh - BusyboxSh -> forSh Sh -> forSh where @@ -2886,19 +2821,17 @@ prop_checkUnpassedInFunctions11 = verifyNotTree checkUnpassedInFunctions "foo() prop_checkUnpassedInFunctions12 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;" prop_checkUnpassedInFunctions13 = verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n" prop_checkUnpassedInFunctions14 = verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo" -prop_checkUnpassedInFunctions15 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1-x}; }; foo" -prop_checkUnpassedInFunctions16 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1:-x}; }; foo" -prop_checkUnpassedInFunctions17 = verifyNotTree checkUnpassedInFunctions "foo() { mycommand ${1+--verbose}; }; foo" -prop_checkUnpassedInFunctions18 = verifyNotTree checkUnpassedInFunctions "foo() { if mycheck; then foo ${1?Missing}; fi; }; foo" checkUnpassedInFunctions params root = execWriter $ mapM_ warnForGroup referenceGroups where functionMap :: Map.Map String Token - functionMap = Map.fromList $ execWriter $ doAnalysis (tell . maybeToList . findFunction) root + functionMap = Map.fromList $ + map (\t@(T_Function _ _ _ name _) -> (name,t)) functions + functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root findFunction t@(T_Function id _ _ name body) | any (isPositionalReference t) flow && not (any isPositionalAssignment flow) - = return (name,t) + = return t where flow = getVariableFlow params body findFunction _ = Nothing @@ -2906,10 +2839,9 @@ checkUnpassedInFunctions params root = case x of Assignment (_, _, str, _) -> isPositional str _ -> False - isPositionalReference function x = case x of - Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function && not (hasDefaultValue t) + Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function _ -> False isDirectChildOf child parent = fromMaybe False $ do @@ -2923,7 +2855,6 @@ checkUnpassedInFunctions params root = referenceList :: [(String, Bool, Token)] referenceList = execWriter $ doAnalysis (sequence_ . checkCommand) root - checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ()) checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do str <- getLiteralString cmd @@ -2934,22 +2865,6 @@ checkUnpassedInFunctions params root = isPositional str = str == "*" || str == "@" || str == "#" || (all isDigit str && str /= "0" && str /= "") - -- True if t is a variable that specifies a default value, - -- such as ${1-x} or ${1:-x}. - hasDefaultValue t = - case t of - T_DollarBraced _ True l -> - let str = concat $ oversimplify l - in isDefaultValueModifier $ getBracedModifier str - _ -> False - - isDefaultValueModifier str = - case str of - ':':c:_ -> c `elem` handlesDefault - c:_ -> c `elem` handlesDefault - _ -> False - where handlesDefault = "-+?" - isArgumentless (_, b, _) = b referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList updateWith x@(name, _, _) = Map.insertWith (++) name [x] @@ -3304,7 +3219,7 @@ checkLoopVariableReassignment params token = return $ do warn (getId token) 2165 "This nested loop overrides the index variable of its parent." warn (getId next) 2167 "This parent loop has its index variable overridden." - path = NE.tail $ getPath (parentMap params) token + path = drop 1 $ getPath (parentMap params) token loopVariable :: Token -> Maybe String loopVariable t = case t of @@ -3377,17 +3292,17 @@ checkReturnAgainstZero params token = -- We don't want to warn about composite expressions like -- [[ $? -eq 0 || $? -eq 4 ]] since these can be annoying to rewrite. isOnlyTestInCommand t = - case NE.tail $ getPath (parentMap params) t of - (T_Condition {}):_ -> True - (T_Arithmetic {}):_ -> True - (TA_Sequence _ [_]):(T_Arithmetic {}):_ -> True + case getPath (parentMap params) t of + _:(T_Condition {}):_ -> True + _:(T_Arithmetic {}):_ -> True + _:(TA_Sequence _ [_]):(T_Arithmetic {}):_ -> True -- Some negations and groupings are also fine - next@(TC_Unary _ _ "!" _):_ -> isOnlyTestInCommand next - next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next - next@(TC_Group {}):_ -> isOnlyTestInCommand next - next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next - next@(TA_Parenthesis _ _):_ -> isOnlyTestInCommand next + _:next@(TC_Unary _ _ "!" _):_ -> isOnlyTestInCommand next + _:next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next + _:next@(TC_Group {}):_ -> isOnlyTestInCommand next + _:next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next + _:next@(TA_Parentesis _ _):_ -> isOnlyTestInCommand next _ -> False -- TODO: Do better $? tracking and filter on whether @@ -3407,7 +3322,7 @@ checkReturnAgainstZero params token = isFirstCommandInFunction = fromMaybe False $ do let path = getPath (parentMap params) token - func <- find isFunction path + func <- listToMaybe $ filter isFunction path cmd <- getClosestCommand (parentMap params) token return $ getId cmd == getId (getFirstCommandInFunction func) @@ -3452,7 +3367,7 @@ checkRedirectedNowhere params token = _ -> return () where isInExpansion t = - case NE.tail $ getPath (parentMap params) t of + case drop 1 $ getPath (parentMap params) t of T_DollarExpansion _ [_] : _ -> True T_Backticked _ [_] : _ -> True t@T_Annotation {} : _ -> isInExpansion t @@ -3682,8 +3597,6 @@ prop_checkPipeToNowhere17 = verify checkPipeToNowhere "echo World | cat << 'EOF' prop_checkPipeToNowhere18 = verifyNot checkPipeToNowhere "ls 1>&3 3>&1 3>&- | wc -l" prop_checkPipeToNowhere19 = verifyNot checkPipeToNowhere "find . -print0 | du --files0-from=/dev/stdin" prop_checkPipeToNowhere20 = verifyNot checkPipeToNowhere "find . | du --exclude-from=/dev/fd/0" -prop_checkPipeToNowhere21 = verifyNot checkPipeToNowhere "yes | cp -ri foo/* bar" -prop_checkPipeToNowhere22 = verifyNot checkPipeToNowhere "yes | rm --interactive *" data PipeType = StdoutPipe | StdoutStderrPipe | NoPipe deriving (Eq) checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity () @@ -3749,7 +3662,6 @@ checkPipeToNowhere params t = commandSpecificException name cmd = case name of "du" -> any ((`elem` ["exclude-from", "files0-from"]) . snd) $ getAllFlags cmd - _ | name `elem` interactiveFlagCmds -> hasInteractiveFlag cmd _ -> False warnAboutDupes (n, list@(_:_:_)) = @@ -3773,7 +3685,7 @@ checkPipeToNowhere params t = name <- getCommandBasename cmd guard $ name `elem` nonReadingCommands guard . not $ hasAdditionalConsumers cmd - guard . not $ name `elem` interactiveFlagCmds && hasInteractiveFlag cmd + guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i" let suggestion = if name == "echo" then "Did you want 'cat' instead?" @@ -3788,9 +3700,6 @@ checkPipeToNowhere params t = treeContains pred t = isNothing $ doAnalysis (guard . not . pred) t - interactiveFlagCmds = [ "cp", "mv", "rm" ] - hasInteractiveFlag cmd = cmd `hasFlag` "i" || cmd `hasFlag` "interactive" - mayConsume t = case t of T_ProcSub _ "<" _ -> True @@ -3857,32 +3766,32 @@ prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { t prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f" prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi" prop_checkUseBeforeDefinition4 = verifyNotTree checkUseBeforeDefinition "mycmd || mycmd() { f; }" -prop_checkUseBeforeDefinition5 = verifyTree checkUseBeforeDefinition "false || mycmd; mycmd() { f; }" -prop_checkUseBeforeDefinition6 = verifyNotTree checkUseBeforeDefinition "f() { one; }; f; f() { two; }; f" -checkUseBeforeDefinition :: Parameters -> Token -> [TokenComment] -checkUseBeforeDefinition params t = fromMaybe [] $ do - cfga <- cfgAnalysis params - let funcs = execState (doAnalysis findFunction t) Map.empty - -- Green cut: no point enumerating commands if there are no functions. - guard . not $ Map.null funcs - return $ execWriter $ doAnalysis (findInvocation cfga funcs) t +checkUseBeforeDefinition _ t = + execWriter $ evalStateT (mapM_ examine $ revCommands) Map.empty where - findFunction t = - case t of - T_Function id _ _ name _ -> modify (Map.insertWith (++) name [id]) - _ -> return () + examine t = case t of + T_Pipeline _ _ [T_Redirecting _ _ (T_Function _ _ _ name _)] -> + modify $ Map.insert name t + T_Annotation _ _ w -> examine w + T_Pipeline _ _ cmds -> do + m <- get + unless (Map.null m) $ + mapM_ (checkUsage m) $ concatMap recursiveSequences cmds + _ -> return () - findInvocation cfga funcs t = - case t of - T_SimpleCommand id _ (cmd:_) -> sequence_ $ do - name <- getLiteralString cmd - invocations <- Map.lookup name funcs - -- Is the function definitely being defined later? - guard $ any (\c -> CF.doesPostDominate cfga c id) invocations - -- Was one already defined, so it's actually a re-definition? - guard . not $ any (\c -> CF.doesPostDominate cfga id c) invocations - return $ err id 2218 "This function is only defined later. Move the definition up." - _ -> return () + checkUsage map cmd = sequence_ $ do + name <- getCommandName cmd + def <- Map.lookup name map + return $ + err (getId cmd) 2218 + "This function is only defined later. Move the definition up." + + revCommands = reverse $ concat $ getCommandSequences t + recursiveSequences x = + let list = concat $ getCommandSequences x in + if null list + then [x] + else concatMap recursiveSequences list prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done" prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done" @@ -3932,7 +3841,7 @@ checkSubshelledTests params t = isFunctionBody path = case path of - (_ NE.:| f:_) -> isFunction f + (_:f:_) -> isFunction f _ -> False isTestStructure t = @@ -3959,7 +3868,7 @@ checkSubshelledTests params t = -- This technically also triggers for `if true; then ( test ); fi` -- but it's still a valid suggestion. isCompoundCondition chain = - case dropWhile skippable (NE.tail chain) of + case dropWhile skippable (drop 1 chain) of T_IfExpression {} : _ -> True T_WhileExpression {} : _ -> True T_UntilExpression {} : _ -> True @@ -4058,10 +3967,13 @@ prop_checkTranslatedStringVariable4 = verifyNot checkTranslatedStringVariable "v prop_checkTranslatedStringVariable5 = verifyNot checkTranslatedStringVariable "foo=var; bar=val2; $\"foo bar\"" checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s]) | all isVariableChar s - && S.member s assignments + && Map.member s assignments = warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id) where - assignments = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params, isVariableName name] + assignments = foldl (flip ($)) Map.empty (map insertAssignment $ variableFlow params) + insertAssignment (Assignment (_, token, name, _)) | isVariableName name = + Map.insert name token + insertAssignment _ = Prelude.id fix id = fixWith [replaceStart id params 2 "\"$"] checkTranslatedStringVariable _ _ = return () @@ -4091,7 +4003,6 @@ prop_checkUselessBang6 = verify checkUselessBang "set -e; { ! true; }" prop_checkUselessBang7 = verifyNot checkUselessBang "set -e; x() { ! [ x ]; }" prop_checkUselessBang8 = verifyNot checkUselessBang "set -e; if { ! true; }; then true; fi" prop_checkUselessBang9 = verifyNot checkUselessBang "set -e; while ! true; do true; done" -prop_checkUselessBang10 = verify checkUselessBang "set -e\nshellcheck disable=SC0000\n! true\nrest" checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturningCommands t) where check t = @@ -4100,7 +4011,6 @@ checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturning addComment $ makeCommentWithFix InfoC id 2251 "This ! is not on a condition and skips errexit. Use `&& exit 1` instead, or make sure $? is checked." (fixWith [replaceStart id params 1 "", replaceEnd (getId cmd) params 0 " && exit 1"]) - T_Annotation _ _ t -> check t _ -> return () -- Get all the subcommands that aren't likely to be the return value @@ -4121,7 +4031,7 @@ checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturning isFunctionBody t = case getPath (parentMap params) t of - _ NE.:| T_Function {}:_-> True + _:T_Function {}:_-> True _ -> False dropLast t = @@ -4136,8 +4046,7 @@ prop_checkModifiedArithmeticInRedirection3 = verifyNot checkModifiedArithmeticIn prop_checkModifiedArithmeticInRedirection4 = verify checkModifiedArithmeticInRedirection "cat <<< $((i++))" prop_checkModifiedArithmeticInRedirection5 = verify checkModifiedArithmeticInRedirection "cat << foo\n$((i++))\nfoo\n" prop_checkModifiedArithmeticInRedirection6 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/dash\nls > $((i=i+1))" -prop_checkModifiedArithmeticInRedirection7 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/busybox sh\ncat << foo\n$((i++))\nfoo\n" -checkModifiedArithmeticInRedirection params t = unless (shellType params == Dash || shellType params == BusyboxSh) $ +checkModifiedArithmeticInRedirection params t = unless (shellType params == Dash) $ case t of T_Redirecting _ redirs (T_SimpleCommand _ _ (_:_)) -> mapM_ checkRedirs redirs _ -> return () @@ -4291,7 +4200,7 @@ checkBadTestAndOr params t = in mapM_ checkTest commandWithSeps checkTest (before, cmd, after) = - when (isTestCommand cmd) $ do + when (isTest cmd) $ do checkPipe before checkPipe after @@ -4307,10 +4216,17 @@ checkBadTestAndOr params t = T_AndIf _ _ rhs -> checkAnds id rhs T_OrIf _ _ rhs -> checkAnds id rhs T_Pipeline _ _ list | not (null list) -> checkAnds id (last list) - cmd -> when (isTestCommand cmd) $ + cmd -> when (isTest cmd) $ errWithFix id 2265 "Use && for logical AND. Single & will background and return true." $ (fixWith [replaceEnd id params 0 "&"]) + isTest t = + case t of + T_Condition {} -> True + T_SimpleCommand {} -> t `isCommand` "test" + T_Redirecting _ _ t -> isTest t + T_Annotation _ _ t -> isTest t + _ -> False prop_checkComparisonWithLeadingX1 = verify checkComparisonWithLeadingX "[ x$foo = xlol ]" prop_checkComparisonWithLeadingX2 = verify checkComparisonWithLeadingX "test x$foo = xlol" @@ -4442,7 +4358,6 @@ checkEqualsInCommand params originalToken = Bash -> errWithFix id 2277 "Use BASH_ARGV0 to assign to $0 in bash (or use [ ] to compare)." bashfix Ksh -> err id 2278 "$0 can't be assigned in Ksh (but it does reflect the current function)." Dash -> err id 2279 "$0 can't be assigned in Dash. This becomes a command name." - BusyboxSh -> err id 2279 "$0 can't be assigned in Busybox Ash. This becomes a command name." _ -> err id 2280 "$0 can't be assigned this way, and there is no portable alternative." leadingNumberMsg id = err id 2282 "Variable names can't start with numbers, so this is interpreted as a command." @@ -4465,9 +4380,9 @@ checkEqualsInCommand params originalToken = return $ isVariableName str isLeadingNumberVar s = - case takeWhile (/= '=') s of - lead@(x:_) -> isDigit x && all isVariableChar lead && not (all isDigit lead) - [] -> False + let lead = takeWhile (/= '=') s + in not (null lead) && isDigit (head lead) + && all isVariableChar lead && not (all isDigit lead) msg cmd leading (T_Literal litId s) = do -- There are many different cases, and the order of the branches matter. @@ -4597,7 +4512,7 @@ prop_checkCommandWithTrailingSymbol9 = verifyNot checkCommandWithTrailingSymbol checkCommandWithTrailingSymbol _ t = case t of T_SimpleCommand _ _ (cmd:_) -> - let str = getLiteralStringDef "x" cmd + let str = fromJust $ getLiteralStringExt (\_ -> Just "x") cmd last = lastOrDefault 'x' str in case str of @@ -4626,13 +4541,13 @@ prop_checkRequireDoubleBracket2 = verifyTree checkRequireDoubleBracket "[ foo -o prop_checkRequireDoubleBracket3 = verifyNotTree checkRequireDoubleBracket "#!/bin/sh\n[ -x foo ]" prop_checkRequireDoubleBracket4 = verifyNotTree checkRequireDoubleBracket "[[ -x foo ]]" checkRequireDoubleBracket params = - if (shellType params) `elem` [Bash, Ksh, BusyboxSh] + if isBashLike params then nodeChecksToTreeCheck [check] params else const [] where check _ t = case t of T_Condition id SingleBracket _ -> - styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh/Busybox." (fixFor t) + styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh." (fixFor t) _ -> return () fixFor t = fixWith $ @@ -4712,8 +4627,7 @@ checkArrayValueUsedAsIndex params _ = -- Is this one of the 'for' arrays? (loopWord, _) <- find ((==arrayName) . snd) arrays -- Are we still in this loop? - let loopId = getId loop - guard $ any (\t -> loopId == getId t) (getPath parents t) + guard $ getId loop `elem` map getId (getPath parents t) return [ makeComment WarningC (getId loopWord) 2302 "This loops over values. To loop over keys, use \"${!array[@]}\".", makeComment WarningC (getId arrayRef) 2303 $ (e4m name) ++ " is an array value, not a key. Use directly or loop over keys instead." @@ -4795,7 +4709,7 @@ checkSetESuppressed params t = literalArg <- getUnquotedLiteral cmd Map.lookup literalArg functions_ - checkCmd cmd = go $ NE.toList $ getPath (parentMap params) cmd + checkCmd cmd = go $ getPath (parentMap params) cmd where go (child:parent:rest) = do case parent of @@ -4909,15 +4823,15 @@ checkExtraMaskedReturns params t = ++ "separately to avoid masking its return value (or use '|| true' " ++ "to ignore).") - isMaskDeliberate t = any isOrIf $ NE.init $ parents params t + isMaskDeliberate t = hasParent isOrIf t where - isOrIf (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd])) + isOrIf _ (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd])) = getCommandBasename cmd `elem` [Just "true", Just ":"] - isOrIf _ = False + isOrIf _ _ = False - isCheckedElsewhere t = any isDeclaringCommand $ NE.tail $ parents params t + isCheckedElsewhere t = hasParent isDeclaringCommand t where - isDeclaringCommand t = fromMaybe False $ do + isDeclaringCommand t _ = fromMaybe False $ do cmd <- getCommand t basename <- getCommandBasename cmd return $ @@ -4937,7 +4851,16 @@ checkExtraMaskedReturns params t = ,"shopt" ] - isTransparentCommand t = getCommandBasename t == Just "time" + isTransparentCommand t = fromMaybe False $ do + basename <- getCommandBasename t + return $ basename == "time" + + parentChildPairs t = go $ parents params t + where + go (child:parent:rest) = (parent, child):go (parent:rest) + go _ = [] + + hasParent pred t = any (uncurry pred) (parentChildPairs t) -- hard error on negated command that is not last @@ -4983,33 +4906,15 @@ checkBatsTestDoesNotUseNegation params t = prop_checkCommandIsUnreachable1 = verify checkCommandIsUnreachable "foo; bar; exit; baz" prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit; }; foo; bar; die; baz" prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz" -prop_checkCommandIsUnreachable4 = verifyNot checkCommandIsUnreachable "f() { foo; }; # Maybe sourced" -prop_checkCommandIsUnreachable5 = verify checkCommandIsUnreachable "f() { foo; }; exit # Not sourced" checkCommandIsUnreachable params t = case t of T_Pipeline {} -> sequence_ $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga (getId t) + state <- CF.getIncomingState (cfgAnalysis params) id guard . not $ CF.stateIsReachable state guard . not $ isSourced params t - guard . not $ any (\t -> isUnreachable t || isUnreachableFunction t) $ NE.drop 1 $ getPath (parentMap params) t - return $ info (getId t) 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." - T_Function id _ _ _ _ -> - when (isUnreachableFunction t - && (not . any isUnreachableFunction . NE.drop 1 $ getPath (parentMap params) t) - && (not $ isSourced params t)) $ - info id 2329 "This function is never invoked. Check usage (or ignored if invoked indirectly)." + return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." _ -> return () - where - isUnreachableFunction :: Token -> Bool - isUnreachableFunction f = - case f of - T_Function id _ _ _ t -> isUnreachable t - _ -> False - isUnreachable t = fromMaybe False $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga (getId t) - return . not $ CF.stateIsReachable state + where id = getId t prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]" @@ -5026,15 +4931,14 @@ checkOverwrittenExitCode params t = _ -> return () where check id = sequence_ $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id + state <- CF.getIncomingState (cfgAnalysis params) id let exitCodeIds = CF.exitCodes state guard . not $ S.null exitCodeIds let idToToken = idMap params - exitCodeTokens <- traverse (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds + exitCodeTokens <- sequence $ map (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds return $ do - when (all isCondition exitCodeTokens && not (usedUnconditionally cfga t exitCodeIds)) $ + when (all isCondition exitCodeTokens && not (usedUnconditionally t exitCodeIds)) $ warn id 2319 "This $? refers to a condition, not a command. Assign to a variable to avoid it being overwritten." when (all isPrinting exitCodeTokens) $ warn id 2320 "This $? refers to echo/printf, not a previous command. Assign to variable to avoid it being overwritten." @@ -5047,8 +4951,8 @@ checkOverwrittenExitCode params t = -- If we don't do anything based on the condition, assume we wanted the condition itself -- This helps differentiate `x; [ $? -gt 0 ] && exit $?` vs `[ cond ]; exit $?` - usedUnconditionally cfga t testIds = - all (\c -> CF.doesPostDominate cfga (getId t) c) testIds + usedUnconditionally t testIds = + all (\c -> CF.doesPostDominate (cfgAnalysis params) (getId t) c) testIds isPrinting t = case getCommandBasename t of @@ -5089,14 +4993,14 @@ checkUnnecessaryParens params t = T_ForArithmetic _ x y z _ -> mapM_ (checkLeading "for (((x); (y); (z))) is the same as for ((x; y; z))") [x,y,z] T_Assignment _ _ _ [t] _ -> checkLeading "a[(x)] is the same as a[x]" t T_Arithmetic _ t -> checkLeading "(( (x) )) is the same as (( x ))" t - TA_Parenthesis _ (TA_Sequence _ [ TA_Parenthesis id _ ]) -> + TA_Parentesis _ (TA_Sequence _ [ TA_Parentesis id _ ]) -> styleWithFix id 2322 "In arithmetic contexts, ((x)) is the same as (x). Prefer only one layer of parentheses." $ fix id _ -> return () where checkLeading str t = case t of - TA_Sequence _ [TA_Parenthesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id + TA_Sequence _ [TA_Parentesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id _ -> return () fix id = @@ -5106,91 +5010,5 @@ checkUnnecessaryParens params t = ] -prop_checkPlusEqualsNumber1 = verify checkPlusEqualsNumber "x+=1" -prop_checkPlusEqualsNumber2 = verify checkPlusEqualsNumber "x+=42" -prop_checkPlusEqualsNumber3 = verifyNot checkPlusEqualsNumber "(( x += 1 ))" -prop_checkPlusEqualsNumber4 = verifyNot checkPlusEqualsNumber "declare -i x=0; x+=1" -prop_checkPlusEqualsNumber5 = verifyNot checkPlusEqualsNumber "x+='1'" -prop_checkPlusEqualsNumber6 = verifyNot checkPlusEqualsNumber "n=foo; x+=n" -prop_checkPlusEqualsNumber7 = verify checkPlusEqualsNumber "n=4; x+=n" -prop_checkPlusEqualsNumber8 = verify checkPlusEqualsNumber "n=4; x+=$n" -prop_checkPlusEqualsNumber9 = verifyNot checkPlusEqualsNumber "declare -ia var; var[x]+=1" -checkPlusEqualsNumber params t = - case t of - T_Assignment id Append var _ word -> sequence_ $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id - guard $ isNumber state word - guard . not $ fromMaybe False $ CF.variableMayBeDeclaredInteger state var - -- Recommend "typeset" because ksh does not have "declare". - return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), typeset -i var, or quote number to silence." - _ -> return () - - where - isNumber state word = - let - unquotedLiteral = getUnquotedLiteral word - isEmpty = unquotedLiteral == Just "" - isUnquotedNumber = not isEmpty && maybe False (all isDigit) unquotedLiteral - isNumericalVariableName = fromMaybe False $ do - str <- unquotedLiteral - CF.variableMayBeAssignedInteger state str - isNumericalVariableExpansion = - case word of - T_NormalWord _ [part] -> fromMaybe False $ do - str <- getUnmodifiedParameterExpansion part - CF.variableMayBeAssignedInteger state str - _ -> False - in - isUnquotedNumber || isNumericalVariableName || isNumericalVariableExpansion - - - -prop_checkExpansionWithRedirection1 = verify checkExpansionWithRedirection "var=$(foo > bar)" -prop_checkExpansionWithRedirection2 = verify checkExpansionWithRedirection "var=`foo 1> bar`" -prop_checkExpansionWithRedirection3 = verify checkExpansionWithRedirection "var=${ foo >> bar; }" -prop_checkExpansionWithRedirection4 = verify checkExpansionWithRedirection "var=$(foo | bar > baz)" -prop_checkExpansionWithRedirection5 = verifyNot checkExpansionWithRedirection "stderr=$(foo 2>&1 > /dev/null)" -prop_checkExpansionWithRedirection6 = verifyNot checkExpansionWithRedirection "var=$(foo; bar > baz)" -prop_checkExpansionWithRedirection7 = verifyNot checkExpansionWithRedirection "var=$(foo > bar; baz)" -prop_checkExpansionWithRedirection8 = verifyNot checkExpansionWithRedirection "var=$(cat <&3)" -checkExpansionWithRedirection params t = - case t of - T_DollarExpansion id [cmd] -> check id cmd - T_Backticked id [cmd] -> check id cmd - T_DollarBraceCommandExpansion id [cmd] -> check id cmd - _ -> return () - where - check id pipe = - case pipe of - (T_Pipeline _ _ t@(_:_)) -> checkCmd id (last t) - _ -> return () - - checkCmd captureId (T_Redirecting _ redirs _) = foldr (walk captureId) (return ()) redirs - - walk captureId t acc = - case t of - T_FdRedirect _ _ (T_IoDuplicate _ _ "1") -> return () - T_FdRedirect id "1" (T_IoDuplicate _ _ _) -> return () - T_FdRedirect id "" (T_IoDuplicate _ op _) | op `elem` [T_GREATAND (Id 0), T_Greater (Id 0)] -> emit id captureId True - T_FdRedirect id str (T_IoFile _ op file) | str `elem` ["", "1"] && op `elem` [ T_DGREAT (Id 0), T_Greater (Id 0) ] -> - emit id captureId $ getLiteralString file /= Just "/dev/null" - _ -> acc - - emit redirectId captureId suggestTee = do - warn captureId 2327 "This command substitution will be empty because the command's output gets redirected away." - err redirectId 2328 $ "This redirection takes output away from the command substitution" ++ if suggestTee then " (use tee to duplicate)." else "." - - -prop_checkUnaryTestA1 = verify checkUnaryTestA "[ -a foo ]" -prop_checkUnaryTestA2 = verify checkUnaryTestA "[ ! -a foo ]" -prop_checkUnaryTestA3 = verifyNot checkUnaryTestA "[ foo -a bar ]" -checkUnaryTestA params t = - case t of - TC_Unary id _ "-a" _ -> - styleWithFix id 2331 "For file existence, prefer standard -e over legacy -a." $ - fixWith [replaceStart id params 2 "-e"] - _ -> return () - return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs index da528a4..ca928fd 100644 --- a/src/ShellCheck/AnalyzerLib.hs +++ b/src/ShellCheck/AnalyzerLib.hs @@ -41,7 +41,6 @@ import Data.Char import Data.List import Data.Maybe import Data.Semigroup -import qualified Data.List.NonEmpty as NE import qualified Data.Map as Map import Test.QuickCheck.All (forAllProperties) @@ -89,8 +88,6 @@ data Parameters = Parameters { hasSetE :: Bool, -- Whether this script has 'set -o pipefail' anywhere. hasPipefail :: Bool, - -- Whether this script has 'shopt -s execfail' anywhere. - hasExecfail :: Bool, -- A linear (bad) analysis of data flow variableFlow :: [StackData], -- A map from Id to Token @@ -106,7 +103,7 @@ data Parameters = Parameters { -- map from token id to start and end position tokenPositions :: Map.Map Id (Position, Position), -- Result from Control Flow Graph analysis (including data flow analysis) - cfgAnalysis :: Maybe CF.CFGAnalysis + cfgAnalysis :: CF.CFGAnalysis } deriving (Show) -- TODO: Cache results of common AST ops here @@ -199,10 +196,8 @@ makeCommentWithFix severity id code str fix = } in force withFix --- makeParameters :: CheckSpec -> Parameters makeParameters spec = params where - extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root] params = Parameters { rootNode = root, shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec, @@ -211,35 +206,26 @@ makeParameters spec = params case shellType params of Bash -> isOptionSet "lastpipe" root Dash -> False - BusyboxSh -> False Sh -> False Ksh -> True, hasInheritErrexit = case shellType params of Bash -> isOptionSet "inherit_errexit" root Dash -> True - BusyboxSh -> True Sh -> True Ksh -> False, hasPipefail = case shellType params of Bash -> isOptionSet "pipefail" root Dash -> True - BusyboxSh -> isOptionSet "pipefail" root Sh -> True Ksh -> isOptionSet "pipefail" root, - hasExecfail = - case shellType params of - Bash -> isOptionSet "execfail" root - _ -> False, shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), idMap = getTokenMap root, parentMap = getParentTree root, variableFlow = getVariableFlow params root, tokenPositions = asTokenPositions spec, - cfgAnalysis = do - guard extendedAnalysis - return $ CF.analyzeControlFlow cfParams root + cfgAnalysis = CF.analyzeControlFlow cfParams root } cfParams = CF.CFGParameters { CF.cfLastpipe = hasLastpipe params, @@ -298,8 +284,8 @@ prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash -prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh -prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh +prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh +prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash determineShellTest = determineShellTest' Nothing determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript @@ -347,14 +333,14 @@ isQuoteFree = isQuoteFreeNode False isQuoteFreeNode strict shell tree t = isQuoteFreeElement t || - (fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t) + (fromMaybe False $ msum $ map isQuoteFreeContext $ drop 1 $ getPath tree t) where -- Is this node self-quoting in itself? isQuoteFreeElement t = case t of - T_Assignment id _ _ _ _ -> assignmentIsQuoting id - T_FdRedirect {} -> True - _ -> False + T_Assignment {} -> assignmentIsQuoting t + T_FdRedirect {} -> True + _ -> False -- Are any subnodes inherently self-quoting? isQuoteFreeContext t = @@ -364,7 +350,7 @@ isQuoteFreeNode strict shell tree t = TC_Binary _ DoubleBracket _ _ _ -> return True TA_Sequence {} -> return True T_Arithmetic {} -> return True - T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id + T_Assignment {} -> return $ assignmentIsQuoting t T_Redirecting {} -> return False T_DoubleQuoted _ _ -> return True T_DollarDoubleQuoted _ _ -> return True @@ -379,11 +365,11 @@ isQuoteFreeNode strict shell tree t = -- Check whether this assignment is self-quoting due to being a recognized -- assignment passed to a Declaration Utility. This will soon be required -- by POSIX: https://austingroupbugs.net/view.php?id=351 - assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id) + assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t) shellParsesParamsAsAssignments = shell /= Sh -- Is this assignment a parameter to a command like export/typeset/etc? - isAssignmentParamToCommand id = + isAssignmentParamToCommand (T_Assignment id _ _ _ _) = case Map.lookup id tree of Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args) _ -> False @@ -409,7 +395,7 @@ isParamTo tree cmd = -- Get the parent command (T_Redirecting) of a Token, if any. getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token getClosestCommand tree t = - findFirst findCommand $ NE.toList $ getPath tree t + findFirst findCommand $ getPath tree t where findCommand t = case t of @@ -423,7 +409,7 @@ getClosestCommandM t = do return $ getClosestCommand (parentMap params) t -- Is the token used as a command name (the first word in a T_SimpleCommand)? -usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token) +usedAsCommandName tree token = go (getId token) (tail $ getPath tree token) where go currentId (T_NormalWord id [word]:rest) | currentId == getId word = go id rest @@ -440,9 +426,7 @@ getPathM t = do return $ getPath (parentMap params) t isParentOf tree parent child = - any (\t -> parentId == getId t) (getPath tree child) - where - parentId = getId parent + elem (getId parent) . map getId $ getPath tree child parents params = getPath (parentMap params) @@ -541,9 +525,7 @@ getModifiedVariables t = T_BatsTest {} -> [ (t, t, "lines", DataArray SourceExternal), (t, t, "status", DataString SourceInteger), - (t, t, "output", DataString SourceExternal), - (t, t, "stderr", DataString SourceExternal), - (t, t, "stderr_lines", DataArray SourceExternal) + (t, t, "output", DataString SourceExternal) ] -- Count [[ -v foo ]] as an "assignment". @@ -565,12 +547,8 @@ getModifiedVariables t = T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] - T_CoProc _ Nothing _ -> - [(t, t, "COPROC", DataArray SourceInteger)] - - T_CoProc _ (Just token) _ -> do - name <- maybeToList $ getLiteralString token - [(t, t, name, DataArray SourceInteger)] + T_CoProc _ name _ -> + [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)] --Points to 'for' rather than variable T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)] @@ -832,7 +810,7 @@ getReferencedVariables parents t = return (context, token, getBracedReference str) isArithmeticAssignment t = case getPath parents t of - this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t + this: TA_Assignment _ "=" lhs _ :_ -> lhs == t _ -> False isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]) @@ -914,6 +892,15 @@ supportsArrays Bash = True supportsArrays Ksh = True supportsArrays _ = False +-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh) +isBashLike :: Parameters -> Bool +isBashLike params = + case shellType params of + Bash -> True + Ksh -> True + Dash -> False + Sh -> False + isTrueAssignmentSource c = case c of DataString SourceChecked -> False @@ -931,14 +918,6 @@ modifiesVariable params token name = Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name _ -> False -isTestCommand t = - case t of - T_Condition {} -> True - T_SimpleCommand {} -> t `isCommand` "test" - T_Redirecting _ _ t -> isTestCommand t - T_Annotation _ _ t -> isTestCommand t - T_Pipeline _ _ [t] -> isTestCommand t - _ -> False return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs index 57aaf4b..e0c6267 100644 --- a/src/ShellCheck/CFG.hs +++ b/src/ShellCheck/CFG.hs @@ -51,7 +51,6 @@ import Control.Monad.Identity import Data.Array.Unboxed import Data.Array.ST import Data.List hiding (map) -import qualified Data.List.NonEmpty as NE import Data.Maybe import qualified Data.Map as M import qualified Data.Set as S @@ -112,8 +111,8 @@ data CFEdge = -- Actions we track data CFEffect = - CFSetProps (Maybe Scope) String (S.Set CFVariableProp) - | CFUnsetProps (Maybe Scope) String (S.Set CFVariableProp) + CFSetProps Scope String (S.Set CFVariableProp) + | CFUnsetProps Scope String (S.Set CFVariableProp) | CFReadVariable String | CFWriteVariable String CFValue | CFWriteGlobal String CFValue @@ -193,7 +192,7 @@ buildGraph params root = base idToRange = M.fromList mapping - isRealEdge (from, to, edge) = case edge of CFEFlow -> True; CFEExit -> True; _ -> False + isRealEdge (from, to, edge) = case edge of CFEFlow -> True; _ -> False onlyRealEdges = filter isRealEdge edges (_, mainExit) = fromJust $ M.lookup (getId root) idToRange @@ -295,19 +294,19 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) = regularEdges = filter isRegularEdge edges inDegree = counter $ map (\(from,to,_) -> from) regularEdges outDegree = counter $ map (\(from,to,_) -> to) regularEdges - structuralNodes = S.fromList [node | (node, CFStructuralNode) <- nodes] + structuralNodes = S.fromList $ map fst $ filter isStructural nodes candidateNodes = S.filter isLinear structuralNodes edgesToCollapse = S.fromList $ filter filterEdges regularEdges remapping :: M.Map Node Node - remapping = M.fromList $ map orderEdge $ S.toList edgesToCollapse - recursiveRemapping = M.mapWithKey (\c _ -> recursiveLookup remapping c) remapping + remapping = foldl' (\m (new, old) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse + recursiveRemapping = M.fromList $ map (\c -> (c, recursiveLookup remapping c)) $ M.keys remapping filterEdges (a,b,_) = a `S.member` candidateNodes && b `S.member` candidateNodes - orderEdge (a,b,_) = if a < b then (b,a) else (a,b) - counter = M.fromListWith (+) . map (\key -> (key, 1)) + orderEdge (a,b,_) = if a < b then (a,b) else (b,a) + counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty isRegularEdge (_, _, CFEFlow) = True isRegularEdge _ = False @@ -317,6 +316,11 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) = Nothing -> node Just x -> recursiveLookup map x + isStructural (node, label) = + case label of + CFStructuralNode -> True + _ -> False + isLinear node = M.findWithDefault 0 node inDegree == 1 && M.findWithDefault 0 node outDegree == 1 @@ -490,7 +494,7 @@ build t = do TA_Binary _ _ a b -> sequentially [a,b] TA_Expansion _ list -> sequentially list TA_Sequence _ list -> sequentially list - TA_Parenthesis _ t -> build t + TA_Parentesis _ t -> build t TA_Trinary _ cond a b -> do condition <- build cond @@ -574,7 +578,7 @@ build t = do T_Array _ list -> sequentially list - T_Assignment {} -> buildAssignment Nothing t + T_Assignment {} -> buildAssignment DefaultScope t T_Backgrounded id body -> do start <- newStructuralNode @@ -610,15 +614,15 @@ build t = do T_CaseExpression id t [] -> build t - T_CaseExpression id t list@(hd:tl) -> do + T_CaseExpression id t list -> do start <- newStructuralNode token <- build t - branches <- mapM buildBranch (hd NE.:| tl) + branches <- mapM buildBranch list end <- newStructuralNode - let neighbors = zip (NE.toList branches) $ NE.tail branches - let (_, firstCond, _) = NE.head branches - let (_, lastCond, lastBody) = NE.last branches + let neighbors = zip branches $ tail branches + let (_, firstCond, _) = head branches + let (_, lastCond, lastBody) = last branches linkRange start token linkRange token firstCond @@ -668,18 +672,10 @@ build t = do status <- newNodeRange $ CFSetExitCode id linkRange cond status - T_CoProc id maybeNameToken t -> do - -- If unspecified, "COPROC". If not a constant string, Nothing. - let maybeName = case maybeNameToken of - Just x -> getLiteralString x - Nothing -> Just "COPROC" - - let parentNode = case maybeName of - Just str -> applySingle $ IdTagged id $ CFWriteVariable str CFValueArray - Nothing -> CFStructuralNode - + T_CoProc id maybeName t -> do + let name = fromMaybe "COPROC" maybeName start <- newStructuralNode - parent <- newNodeRange parentNode + parent <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueArray child <- subshell id "coproc" $ build t end <- newNodeRange $ CFSetExitCode id @@ -861,8 +857,8 @@ build t = do status <- newNodeRange (CFSetExitCode id) linkRange assignments status - T_SimpleCommand id vars (cmd:args) -> - handleCommand t vars (cmd NE.:| args) $ getUnquotedLiteral cmd + T_SimpleCommand id vars list@(cmd:_) -> + handleCommand t vars list $ getUnquotedLiteral cmd T_SingleQuoted _ _ -> none @@ -891,9 +887,7 @@ build t = do T_Less _ -> none T_ParamSubSpecialChar _ _ -> none - x -> do - error ("Unimplemented: " ++ show x) -- STRIP - none + x -> error ("Unimplemented: " ++ show x) -- Still in `where` clause forInHelper id name words body = do @@ -929,8 +923,8 @@ handleCommand cmd vars args literalCmd = do -- TODO: Handle assignments in declaring commands case literalCmd of - Just "exit" -> regularExpansion vars (NE.toList args) $ handleExit - Just "return" -> regularExpansion vars (NE.toList args) $ handleReturn + Just "exit" -> regularExpansion vars args $ handleExit + Just "return" -> regularExpansion vars args $ handleReturn Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args Just "declare" -> handleDeclare args @@ -953,14 +947,14 @@ handleCommand cmd vars args literalCmd = do -- This will mostly behave like 'command' but ok Just "builtin" -> case args of - _ NE.:| [] -> regular - (_ NE.:| newcmd:newargs) -> - handleCommand newcmd vars (newcmd NE.:| newargs) $ getLiteralString newcmd + [_] -> regular + (_:newargs@(newcmd:_)) -> + handleCommand newcmd vars newargs $ getLiteralString newcmd Just "command" -> case args of - _ NE.:| [] -> regular - (_ NE.:| newcmd:newargs) -> - handleOthers (getId newcmd) vars (newcmd NE.:| newargs) $ getLiteralString newcmd + [_] -> regular + (_:newargs@(newcmd:_)) -> + handleOthers (getId newcmd) vars newargs $ getLiteralString newcmd _ -> regular where @@ -988,7 +982,7 @@ handleCommand cmd vars args literalCmd = do unreachable <- newNode CFUnreachable return $ Range ret unreachable - handleUnset (cmd NE.:| args) = do + handleUnset (cmd:args) = do case () of _ | "n" `elem` flagNames -> unsetWith CFUndefineNameref _ | "v" `elem` flagNames -> unsetWith CFUndefineVariable @@ -1000,14 +994,14 @@ handleCommand cmd vars args literalCmd = do (names, flags) = partition (null . fst) pairs flagNames = map fst flags literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")] - literalNames = mapMaybe (\(_, t) -> (,) t <$> getLiteralString t) names + literalNames = mapMaybe (\(_, t) -> getLiteralString t >>= (return . (,) t)) names -- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)=" - handleDeclare (cmd NE.:| args) = do + handleDeclare (cmd:args) = do isFunc <- asks cfIsFunction -- This is a bit of a kludge: we don't have great support for things like -- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x @@ -1034,9 +1028,9 @@ handleCommand cmd vars args literalCmd = do scope isFunc = case () of - _ | global -> Just GlobalScope - _ | isFunc -> Just LocalScope - _ -> Nothing + _ | global -> GlobalScope + _ | isFunc -> LocalScope + _ -> DefaultScope addedProps = S.fromList $ concat $ [ [ CFVPArray | array ], @@ -1064,7 +1058,7 @@ handleCommand cmd vars args literalCmd = do let id = getId t pre = [t] - literal = getLiteralStringDef "\0" t + literal = fromJust $ getLiteralStringExt (const $ Just "\0") t isKnown = '\0' `notElem` literal match = fmap head $ variableAssignRegex `matchRegex` literal name = fromMaybe literal match @@ -1096,7 +1090,7 @@ handleCommand cmd vars args literalCmd = do in concatMap (drop 1) plusses - handlePrintf (cmd NE.:| args) = + handlePrintf (cmd:args) = newNodeRange $ CFApplyEffects $ maybeToList findVar where findVar = do @@ -1105,7 +1099,7 @@ handleCommand cmd vars args literalCmd = do name <- getLiteralString arg return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString - handleWait (cmd NE.:| args) = + handleWait (cmd:args) = newNodeRange $ CFApplyEffects $ maybeToList findVar where findVar = do @@ -1114,7 +1108,7 @@ handleCommand cmd vars args literalCmd = do name <- getLiteralString arg return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger - handleMapfile (cmd NE.:| args) = + handleMapfile (cmd:args) = newNodeRange $ CFApplyEffects [findVar] where findVar = @@ -1134,7 +1128,7 @@ handleCommand cmd vars args literalCmd = do guard $ isVariableName name return (getId c, name) - handleRead (cmd NE.:| args) = newNodeRange $ CFApplyEffects main + handleRead (cmd:args) = newNodeRange $ CFApplyEffects main where main = fromMaybe fallback $ do flags <- getGnuOpts flagsForRead args @@ -1164,7 +1158,7 @@ handleCommand cmd vars args literalCmd = do in map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault - handleDEFINE (cmd NE.:| args) = + handleDEFINE (cmd:args) = newNodeRange $ CFApplyEffects $ maybeToList findVar where findVar = do @@ -1174,14 +1168,14 @@ handleCommand cmd vars args literalCmd = do return $ IdTagged (getId name) $ CFWriteVariable str CFValueString handleOthers id vars args cmd = - regularExpansion vars (NE.toList args) $ do + regularExpansion vars args $ do exe <- newNodeRange $ CFExecuteCommand cmd status <- newNodeRange $ CFSetExitCode id linkRange exe status regularExpansion vars args p = do args <- sequentially args - assignments <- mapM (buildAssignment (Just PrefixScope)) vars + assignments <- mapM (buildAssignment PrefixScope) vars exe <- p dropAssignments <- if null vars @@ -1193,15 +1187,15 @@ handleCommand cmd vars args literalCmd = do linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments - regularExpansionWithStatus vars args@(cmd NE.:| _) p = do - initial <- regularExpansion vars (NE.toList args) p + regularExpansionWithStatus vars args@(cmd:_) p = do + initial <- regularExpansion vars args p status <- newNodeRange $ CFSetExitCode (getId cmd) linkRange initial status none = newStructuralNode -data Scope = GlobalScope | LocalScope | PrefixScope +data Scope = DefaultScope | GlobalScope | LocalScope | PrefixScope deriving (Eq, Ord, Show, Generic, NFData) buildAssignment scope t = do @@ -1215,10 +1209,10 @@ buildAssignment scope t = do let valueType = if null indices then f id value else CFValueArray let scoper = case scope of - Just PrefixScope -> CFWritePrefix - Just LocalScope -> CFWriteLocal - Just GlobalScope -> CFWriteGlobal - Nothing -> CFWriteVariable + PrefixScope -> CFWritePrefix + LocalScope -> CFWriteLocal + GlobalScope -> CFWriteGlobal + DefaultScope -> CFWriteVariable write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType linkRanges [expand, index, read, write] where @@ -1307,10 +1301,7 @@ findPostDominators mainexit graph = asArray reversed = grev withExitEdges postDoms = dom reversed mainexit (_, maxNode) = nodeRange graph - -- Holes in the array cause "Exception: (Array.!): undefined array element" while - -- inspecting/debugging, so fill the array first and then update. - initializedArray = listArray (0, maxNode) $ repeat [] - asArray = initializedArray // postDoms + asArray = array (0, maxNode) postDoms return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/CFGAnalysis.hs b/src/ShellCheck/CFGAnalysis.hs index cf982e0..4e36cf5 100644 --- a/src/ShellCheck/CFGAnalysis.hs +++ b/src/ShellCheck/CFGAnalysis.hs @@ -59,8 +59,6 @@ module ShellCheck.CFGAnalysis ( ,getIncomingState ,getOutgoingState ,doesPostDominate - ,variableMayBeDeclaredInteger - ,variableMayBeAssignedInteger ,ShellCheck.CFGAnalysis.runTests -- STRIP ) where @@ -133,7 +131,7 @@ internalToExternal s = literalValue = Nothing } } - flatVars = M.unions $ map mapStorage [sPrefixValues s, sLocalValues s, sGlobalValues s] + flatVars = M.unionsWith (\_ last -> last) $ map mapStorage [sGlobalValues s, sLocalValues s, sPrefixValues s] -- Conveniently get the state before a token id getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState @@ -155,20 +153,6 @@ doesPostDominate analysis target base = fromMaybe False $ do (targetStart, _) <- M.lookup target $ tokenToRange analysis return $ targetStart `elem` (postDominators analysis ! baseEnd) --- See if any execution path results in the variable containing a state -variableMayHaveState :: ProgramState -> String -> CFVariableProp -> Maybe Bool -variableMayHaveState state var property = do - value <- M.lookup var $ variablesInScope state - return $ any (S.member property) $ variableProperties value - --- See if any execution path declares the variable an integer (declare -i). -variableMayBeDeclaredInteger state var = variableMayHaveState state var CFVPInteger - --- See if any execution path suggests the variable may contain an integer value -variableMayBeAssignedInteger state var = do - value <- M.lookup var $ variablesInScope state - return $ (numericalStatus $ variableValue value) >= NumericalStatusMaybe - getDataForNode analysis node = M.lookup node $ nodeToData analysis -- The current state of data flow at a point in the program, potentially as a diff @@ -299,6 +283,7 @@ depsToState set = foldl insert newInternalState $ S.toList set PrefixScope -> (sPrefixValues, insertPrefix) LocalScope -> (sLocalValues, insertLocal) GlobalScope -> (sGlobalValues, insertGlobal) + DefaultScope -> error $ pleaseReport "Unresolved scope in dependency" alreadyExists = isJust $ vmLookup name $ mapToCheck state in @@ -672,7 +657,7 @@ vmPatch base diff = _ | vmIsQuickEqual base diff -> diff _ -> VersionedMap { mapVersion = -1, - mapStorage = M.union (mapStorage diff) (mapStorage base) + mapStorage = M.unionWith (flip const) (mapStorage base) (mapStorage diff) } -- Set a variable. This includes properties. Applies it to the appropriate scope. @@ -829,7 +814,7 @@ lookupStack' functionOnly get dep def ctx key = do f (s:rest) = do -- Go up the stack until we find the value, and add -- a dependency on each state (including where it was found) - res <- maybe (f rest) return (get (stackState s) key) + res <- fromMaybe (f rest) (return <$> get (stackState s) key) modifySTRef (dependencies s) $ S.insert $ dep key res return res @@ -1119,34 +1104,34 @@ transferEffect ctx effect = CFSetProps scope name props -> case scope of - Nothing -> do + DefaultScope -> do state <- readVariable ctx name writeVariable ctx name $ addProperties props state - Just GlobalScope -> do + GlobalScope -> do state <- readGlobal ctx name writeGlobal ctx name $ addProperties props state - Just LocalScope -> do + LocalScope -> do out <- readSTRef (cOutput ctx) state <- readLocal ctx name writeLocal ctx name $ addProperties props state - Just PrefixScope -> do + PrefixScope -> do -- Prefix values become local state <- readLocal ctx name writeLocal ctx name $ addProperties props state CFUnsetProps scope name props -> case scope of - Nothing -> do + DefaultScope -> do state <- readVariable ctx name writeVariable ctx name $ removeProperties props state - Just GlobalScope -> do + GlobalScope -> do state <- readGlobal ctx name writeGlobal ctx name $ removeProperties props state - Just LocalScope -> do + LocalScope -> do out <- readSTRef (cOutput ctx) state <- readLocal ctx name writeLocal ctx name $ removeProperties props state - Just PrefixScope -> do + PrefixScope -> do -- Prefix values become local state <- readLocal ctx name writeLocal ctx name $ removeProperties props state @@ -1286,7 +1271,7 @@ dataflow ctx entry = do else do let (next, rest) = S.deleteFindMin ps nexts <- process states next - writeSTRef pending $ S.union (S.fromList nexts) rest + writeSTRef pending $ foldl (flip S.insert) rest nexts f (n-1) pending states process states node = do @@ -1350,7 +1335,7 @@ analyzeControlFlow params t = -- All nodes we've touched invocations <- readSTRef $ cInvocations ctx - let invokedNodes = M.fromSet (const ()) $ S.unions $ map (M.keysSet . snd) $ M.elems invocations + let invokedNodes = M.fromDistinctAscList $ map (\c -> (c, ())) $ S.toList $ M.keysSet $ groupByNode $ M.map snd invocations -- Invoke all functions that were declared but not invoked -- This is so that we still get warnings for dead code @@ -1373,7 +1358,7 @@ analyzeControlFlow params t = -- Fill in the map with unreachable states for anything we didn't get to let baseStates = M.fromDistinctAscList $ map (\c -> (c, (unreachableState, unreachableState))) $ uncurry enumFromTo $ nodeRange $ cfGraph cfg - let allStates = M.union invokedStates baseStates + let allStates = M.unionWith (flip const) baseStates invokedStates -- Convert to external states let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates diff --git a/src/ShellCheck/Checker.hs b/src/ShellCheck/Checker.hs index 0cfc3ab..b56be68 100644 --- a/src/ShellCheck/Checker.hs +++ b/src/ShellCheck/Checker.hs @@ -25,7 +25,6 @@ import ShellCheck.ASTLib import ShellCheck.Interface import ShellCheck.Parser -import Debug.Trace -- DO NOT SUBMIT import Data.Either import Data.Functor import Data.List @@ -87,7 +86,6 @@ checkScript sys spec = do asCheckSourced = csCheckSourced spec, asExecutionMode = Executed, asTokenPositions = tokenPositions, - asExtendedAnalysis = csExtendedAnalysis spec, asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec } where as = newAnalysisSpec root let analysisMessages = @@ -510,55 +508,5 @@ prop_rcCanSuppressEarlyProblems2 = null result csScript = "!/bin/bash\necho 'hello world'" } -prop_sourceWithHereDocWorks = null result - where - result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof" - -prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result - where - result = check "cat << eof" - -prop_hereDocsWillHaveParsedIndices = null result - where - result = check "#!/bin/bash\nmy_array=(a b)\ncat <> ./test\n $(( 1 + my_array[1] ))\nEOF" - -prop_rcCanSuppressDfa = null result - where - result = checkWithRc "extended-analysis=false" emptyCheckSpec { - csScript = "#!/bin/sh\nexit; foo;" - } - -prop_fileCanSuppressDfa = null $ traceShowId result - where - result = checkWithRc "" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;" - } - -prop_fileWinsWhenSuppressingDfa1 = null result - where - result = checkWithRc "extended-analysis=true" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;" - } - -prop_fileWinsWhenSuppressingDfa2 = result == [2317] - where - result = checkWithRc "extended-analysis=false" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;" - } - -prop_flagWinsWhenSuppressingDfa1 = result == [2317] - where - result = checkWithRc "extended-analysis=false" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;", - csExtendedAnalysis = Just True - } - -prop_flagWinsWhenSuppressingDfa2 = null result - where - result = checkWithRc "extended-analysis=true" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;", - csExtendedAnalysis = Just False - } - return [] runTests = $quickCheckAll diff --git a/src/ShellCheck/Checks/Commands.hs b/src/ShellCheck/Checks/Commands.hs index c37a67d..691836f 100644 --- a/src/ShellCheck/Checks/Commands.hs +++ b/src/ShellCheck/Checks/Commands.hs @@ -20,7 +20,6 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE PatternGuards #-} -- This module contains checks that examine specific commands by name. module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where @@ -43,7 +42,6 @@ import Data.Functor.Identity import qualified Data.Graph.Inductive.Graph as G import Data.List import Data.Maybe -import qualified Data.List.NonEmpty as NE import qualified Data.Map.Strict as M import qualified Data.Set as S import Test.QuickCheck.All (forAllProperties) @@ -183,15 +181,16 @@ checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do name <- getLiteralString cmd return $ - if | '/' `elem` name -> - M.findWithDefault nullCheck (Basename $ basename name) map t - | name == "builtin", (h:_) <- rest -> - let t' = T_SimpleCommand id cmdPrefix rest - selectedBuiltin = onlyLiteralString h - in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t' - | otherwise -> do - M.findWithDefault nullCheck (Exactly name) map t - M.findWithDefault nullCheck (Basename name) map t + if '/' `elem` name + then + M.findWithDefault nullCheck (Basename $ basename name) map t + else if name == "builtin" && not (null rest) then + let t' = T_SimpleCommand id cmdPrefix rest + selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest + in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t' + else do + M.findWithDefault nullCheck (Exactly name) map t + M.findWithDefault nullCheck (Basename name) map t where basename = reverse . takeWhile (/= '/') . reverse @@ -300,7 +299,7 @@ checkExpr = CommandCheck (Basename "expr") f where "'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|." [first, second] | - onlyLiteralString first /= "length" + (fromMaybe "" $ getLiteralString first) /= "length" && not (willSplit first || willSplit second) -> do checkOp first warn (getId t) 2307 @@ -931,7 +930,7 @@ prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1" checkTimedCommand = CommandCheck (Exactly "time") f where f (T_SimpleCommand _ _ (c:args@(_:_))) = - whenShell [Sh, Dash, BusyboxSh] $ do + whenShell [Sh, Dash] $ do let cmd = last args -- "time" is parsed with a command as argument when (isPiped cmd) $ warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead." @@ -955,7 +954,7 @@ checkTimedCommand = CommandCheck (Exactly "time") f where prop_checkLocalScope1 = verify checkLocalScope "local foo=3" prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }" checkLocalScope = CommandCheck (Exactly "local") $ \t -> - whenShell [Bash, Dash, BusyboxSh] $ do -- Ksh allows it, Sh doesn't support local + whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local path <- getPathM t unless (any isFunctionLike path) $ err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions." @@ -1006,8 +1005,8 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f sequence_ $ do options <- getLiteralString arg1 getoptsVar <- getLiteralString name - (T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path) - T_CaseExpression id var list <- mapMaybe findCase body !!! 0 + (T_WhileExpression _ _ body) <- findFirst whileLoop path + caseCmd@(T_CaseExpression _ var _) <- mapMaybe findCase body !!! 0 -- Make sure getopts name and case variable matches [T_DollarBraced _ _ bracedWord] <- return $ getWordParts var @@ -1017,11 +1016,11 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f -- Make sure the variable isn't modified guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar - return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list + return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd f _ = return () - check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis - check optId opts id list = do + check :: Id -> [String] -> Token -> Analysis + check optId opts (T_CaseExpression id _ list) = do unless (Nothing `M.member` handledMap) $ do mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled @@ -1237,7 +1236,8 @@ checkSudoArgs = CommandCheck (Basename "sudo") f where f t = sequence_ $ do opts <- parseOpts $ arguments t - (_,(commandArg, _)) <- find (null . fst) opts + let nonFlags = [x | ("",(x, _)) <- opts] + commandArg <- nonFlags !!! 0 command <- getLiteralString commandArg guard $ command `elem` builtins return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?" @@ -1430,27 +1430,26 @@ prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x" checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check where - check t = do - maybeCfga <- asks cfgAnalysis - mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga + check t = foldM_ perArg M.empty $ arguments t - perArg cfga leftArgs t = + perArg leftArgs t = case t of T_Assignment id _ name idx t -> do - warnIfBackreferencing cfga leftArgs $ t:idx + warnIfBackreferencing leftArgs $ t:idx return $ M.insert name id leftArgs t -> do - warnIfBackreferencing cfga leftArgs [t] + warnIfBackreferencing leftArgs [t] return leftArgs - warnIfBackreferencing cfga backrefs l = do - references <- findReferences cfga l + warnIfBackreferencing backrefs l = do + references <- findReferences l let reused = M.intersection backrefs references mapM msg $ M.toList reused msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s." - findReferences cfga list = do + findReferences list = do + cfga <- asks cfgAnalysis let graph = CF.graph cfga let nodesMap = CF.tokenToNodes cfga let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list diff --git a/src/ShellCheck/Checks/ControlFlow.hs b/src/ShellCheck/Checks/ControlFlow.hs index 9f63141..9b7635e 100644 --- a/src/ShellCheck/Checks/ControlFlow.hs +++ b/src/ShellCheck/Checks/ControlFlow.hs @@ -78,7 +78,7 @@ controlFlowEffectChecks = [ runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck runNodeChecks perNode = do cfg <- asks cfgAnalysis - mapM_ runOnAll cfg + runOnAll cfg where getData datas n@(node, label) = do (pre, post) <- M.lookup node datas diff --git a/src/ShellCheck/Checks/Custom.hs b/src/ShellCheck/Checks/Custom.hs index 17e9c9e..76ac83c 100644 --- a/src/ShellCheck/Checks/Custom.hs +++ b/src/ShellCheck/Checks/Custom.hs @@ -1,7 +1,7 @@ {- This empty file is provided for ease of patching in site specific checks. However, there are no guarantees regarding compatibility between versions. --} +-} {-# LANGUAGE TemplateHaskell #-} module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index b664879..30a19b9 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -19,7 +19,6 @@ -} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE ViewPatterns #-} module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where import ShellCheck.AST @@ -61,9 +60,6 @@ checks = [ ,checkBraceExpansionVars ,checkMultiDimensionalArrays ,checkPS1Assignments - ,checkMultipleBangs - ,checkBangAfterPipe - ,checkNegatedUnaryOps ] testChecker (ForShell _ t) = @@ -77,24 +73,22 @@ verifyNot c s = producesComments (testChecker c) s == Just False prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" -checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f +checkForDecimals = ForShell [Sh, Dash, Bash] f where f t@(TA_Expansion id _) = sequence_ $ do - first:rest <- getLiteralString t - guard $ isDigit first && '.' `elem` rest + str <- getLiteralString t + first <- str !!! 0 + guard $ isDigit first && '.' `elem` str return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." f _ = return () prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" -prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]" +prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]" prop_checkBashisms3 = verify checkBashisms "echo $((i++))" prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" prop_checkBashisms5 = verify checkBashisms "source file" prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]" -prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42" -prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]" -prop_checkBashisms6d = verify checkBashisms "test foo =~ bar" prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" prop_checkBashisms9 = verify checkBashisms "echo ${!var*}" @@ -110,7 +104,6 @@ prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null" prop_checkBashisms19 = verify checkBashisms "foo > file*.txt" prop_checkBashisms20 = verify checkBashisms "read -ra foo" prop_checkBashisms21 = verify checkBashisms "[ -a foo ]" -prop_checkBashisms21b = verify checkBashisms "test -a foo" prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]" prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT" prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM" @@ -191,82 +184,49 @@ prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_" prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}" prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}" prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo" -prop_checkBashisms100 = verify checkBashisms "read -r" -prop_checkBashisms101 = verify checkBashisms "read" -prop_checkBashisms102 = verifyNot checkBashisms "read -r foo" -prop_checkBashisms103 = verifyNot checkBashisms "read foo" -prop_checkBashisms104 = verifyNot checkBashisms "read ''" -prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail" -prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]" -prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]" -prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null" -prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM" -prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null" -prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057 -prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057 -prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060 -prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060 -prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053 -prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054 -prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055 -prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056 -prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059 -prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]" -prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]" -prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'" -prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'" -prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test" -prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test" -prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar" -prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo" -prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test" -prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]" -prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp" -prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]" -checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do +checkBashisms = ForShell [Sh, Dash] $ \t -> do params <- ask kludge params t where -- This code was copy-pasted from Analytics where params was a variable kludge params = bashism where - isBusyboxSh = shellType params == BusyboxSh - isDash = shellType params == Dash || isBusyboxSh + isDash = shellType params == Dash warnMsg id code s = if isDash then err id code $ "In dash, " ++ s ++ " not supported." else warn id code $ "In POSIX sh, " ++ s ++ " undefined." - asStr = getLiteralString bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is" bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is" - bashism (T_DollarSingleQuoted id _) = - unless isBusyboxSh $ warnMsg id 3003 "$'..' is" + bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is" bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is" bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are" bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is" bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is" bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are" bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is" - bashism (T_Condition id DoubleBracket _) = - unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is" + bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is" bashism (T_HereString id _) = warnMsg id 3011 "here-strings are" - - bashism (TC_Binary id _ op _ _) = - checkTestOp bashismBinaryTestFlags op id - bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) = - checkTestOp bashismBinaryTestFlags op id - bashism (TC_Unary id _ op _) = - checkTestOp bashismUnaryTestFlags op id - bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) = - checkTestOp bashismUnaryTestFlags op id - + bashism (TC_Binary id SingleBracket op _ _) + | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = + unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" + bashism (TC_Binary id SingleBracket op _ _) + | op `elem` [ "-ot", "-nt", "-ef" ] = + unless isDash $ warnMsg id 3013 $ op ++ " is" + bashism (TC_Binary id SingleBracket "==" _ _) = + warnMsg id 3014 "== in place of = is" + bashism (TC_Binary id SingleBracket "=~" _ _) = + warnMsg id 3015 "=~ regex matching is" + bashism (TC_Unary id SingleBracket "-v" _) = + warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is" + bashism (TC_Unary id _ "-a" _) = + warnMsg id 3017 "unary -a in place of -e is" bashism (TA_Unary id op _) | op `elem` [ "|++", "|--", "++|", "--|"] = warnMsg id 3018 $ filter (/= '|') op ++ " is" bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are" - bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = - unless isBusyboxSh $ warnMsg id 3020 "&> is" + bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is" bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) = unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is" bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are" @@ -286,8 +246,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do warnMsg id 3028 $ str ++ " is" bashism t@(T_DollarBraced id _ token) = do - unless isBusyboxSh $ mapM_ check simpleExpansions - mapM_ check advancedExpansions + mapM_ check expansion when (isBashVariable var) $ warnMsg id 3028 $ var ++ " is" where @@ -315,11 +274,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) | t `isCommand` "echo" && argString `matches` flagRegex = - if isBusyboxSh - then - unless (argString `matches` busyboxFlagRegex) $ - warnMsg (getId arg) 3036 "echo flags besides -n and -e" - else if isDash + if isDash then when (argString /= "-n") $ warnMsg (getId arg) 3036 "echo flags besides -n" @@ -328,7 +283,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do where argString = concat $ oversimplify arg flagRegex = mkRegex "^-[eEsn]+$" - busyboxFlagRegex = mkRegex "^-[en]+$" bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) | getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) = @@ -402,8 +356,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is" - when (name == "source" && not isBusyboxSh) $ - warnMsg id 3046 "'source' in place of '.' is" + when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is" when (name == "trap") $ let check token = sequence_ $ do @@ -412,7 +365,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do return $ do when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $ warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is" - when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $ + when ("SIG" `isPrefixOf` upper) $ warnMsg (getId token) 3048 "prefixing signal names with 'SIG' is" when (not isDash && upper /= str) $ @@ -426,9 +379,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do let literal = onlyLiteralString format guard $ "%q" `isInfixOf` literal return $ warnMsg (getId format) 3050 "printf %q is" - - when (name == "read" && all isFlag rest) $ - warnMsg (getId cmd) 3061 "read without a variable is" where unsupportedCommands = [ "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", @@ -442,19 +392,17 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do ("hash", Just $ if isDash then ["r", "v"] else ["r"]), ("jobs", Just ["l", "p"]), ("printf", Just []), - ("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]), + ("read", Just $ if isDash then ["r", "p"] else ["r"]), ("readonly", Just ["p"]), ("trap", Just []), - ("type", Just $ if isBusyboxSh then ["p"] else []), + ("type", Just []), ("ulimit", if isDash then Nothing else Just ["f"]), ("umask", Just ["S"]), ("unset", Just ["f", "v"]), ("wait", Just []) ] bashism t@(T_SourceCommand id src _) - | getCommandName src == Just "source" = - unless isBusyboxSh $ - warnMsg id 3051 "'source' in place of '.' is" + | getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is" bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix = warnMsg id 3052 "arithmetic base conversion is" where @@ -462,16 +410,14 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do bashism _ = return () varChars="_0-9a-zA-Z" - advancedExpansions = let re = mkRegex in [ + expansion = let re = mkRegex in [ (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"), (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"), (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"), (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"), - (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is") - ] - simpleExpansions = let re = mkRegex in [ (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"), (re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"), + (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"), (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is") ] bashVars = [ @@ -497,50 +443,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do Assignment (_, _, name, _) -> name == var _ -> False - checkTestOp table op id = sequence_ $ do - (code, shells, msg) <- Map.lookup op table - guard . not $ shellType params `elem` shells - return $ warnMsg id code (msg op) - - -buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list -bashismBinaryTestFlags = buildTestFlagMap [ - -- ([list of applicable flags], - -- (error code, exempt shells, message builder :: String -> String)), - -- - -- Distinct error codes allow the wiki to give more helpful, targeted - -- information. - (["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="], - (3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")), - (["=="], - (3014, [BusyboxSh], \op -> op ++ " in place of = is")), - (["=~"], - (3015, [], \op -> op ++ " regex matching is")), - - ([], (0,[],const "")) - ] -bashismUnaryTestFlags = buildTestFlagMap [ - (["-v"], - (3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")), - (["-a"], - (3017, [], \op -> "unary " ++ op ++ " in place of -e is")), - (["-o"], - (3062, [], \op -> "test " ++ op ++ " to check options is")), - (["-R"], - (3063, [], \op -> "test " ++ op ++ " and namerefs in general are")), - (["-N"], - (3064, [], \op -> "test " ++ op ++ " is")), - (["-k"], - (3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), - (["-G"], - (3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), - (["-O"], - (3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), - - ([], (0,[],const "")) - ] - - prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")" prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')" @@ -656,46 +558,5 @@ checkPS1Assignments = ForShell [Bash] f escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" -prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true" -prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true" -checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f - where - f token = case token of - T_Banged id (T_Banged _ _) -> - err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1." - _ -> return () - - -prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true" -prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )" -prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true" -checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f - where - f token = case token of - T_Pipeline _ _ cmds -> mapM_ check cmds - _ -> return () - - check token = case token of - T_Banged id _ -> - err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary." - _ -> return () - - -prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]" -prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]" -prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]" -prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]" -prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]" -checkNegatedUnaryOps = ForShell [Bash] f - where - f token = case token of - TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] -> - err id 2332 $ msg op - _ -> return () - - msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]." - msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead." - msg _ = pleaseReport "unhandled negated unary message" - return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs index 688d0d7..550ff87 100644 --- a/src/ShellCheck/Data.hs +++ b/src/ShellCheck/Data.hs @@ -49,7 +49,6 @@ internalVariables = [ "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR", - "BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT", "auto_resume", "histchars", -- Other @@ -63,9 +62,6 @@ internalVariables = [ , "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", "FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", "flags_error", "flags_return" - - -- Bats - ,"stderr", "stderr_lines" ] specialIntegerVariables = [ @@ -79,7 +75,7 @@ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [ "EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM", "READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE", - "HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG" + "HISTSIZE", "LINES" -- shflags , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE" @@ -160,15 +156,11 @@ shellForExecutable name = "sh" -> return Sh "bash" -> return Bash "bats" -> return Bash - "busybox" -> return BusyboxSh -- Used for directives and --shell=busybox - "busybox sh" -> return BusyboxSh - "busybox ash" -> return BusyboxSh "dash" -> return Dash "ash" -> return Dash -- There's also a warning for this. "ksh" -> return Ksh "ksh88" -> return Ksh "ksh93" -> return Ksh - "oksh" -> return Ksh _ -> Nothing flagsForRead = "sreu:n:N:i:p:a:t:" diff --git a/src/ShellCheck/Formatter/CheckStyle.hs b/src/ShellCheck/Formatter/CheckStyle.hs index 3f898c3..6ad6c9c 100644 --- a/src/ShellCheck/Formatter/CheckStyle.hs +++ b/src/ShellCheck/Formatter/CheckStyle.hs @@ -24,8 +24,8 @@ import ShellCheck.Formatter.Format import Data.Char import Data.List +import GHC.Exts import System.IO -import qualified Data.List.NonEmpty as NE format :: IO Formatter format = return Formatter { @@ -45,12 +45,12 @@ outputResults cr sys = else mapM_ outputGroup fileGroups where comments = crComments cr - fileGroups = NE.groupWith sourceFile comments + fileGroups = groupWith sourceFile comments outputGroup group = do - let filename = sourceFile (NE.head group) + let filename = sourceFile (head group) result <- siReadFile sys (Just True) filename let contents = either (const "") id result - outputFile filename contents (NE.toList group) + outputFile filename contents group outputFile filename contents warnings = do let comments = makeNonVirtual warnings contents diff --git a/src/ShellCheck/Formatter/GCC.hs b/src/ShellCheck/Formatter/GCC.hs index b921753..5106e4c 100644 --- a/src/ShellCheck/Formatter/GCC.hs +++ b/src/ShellCheck/Formatter/GCC.hs @@ -23,8 +23,8 @@ import ShellCheck.Interface import ShellCheck.Formatter.Format import Data.List +import GHC.Exts import System.IO -import qualified Data.List.NonEmpty as NE format :: IO Formatter format = return Formatter { @@ -39,13 +39,13 @@ outputError file error = hPutStrLn stderr $ file ++ ": " ++ error outputAll cr sys = mapM_ f groups where comments = crComments cr - groups = NE.groupWith sourceFile comments - f :: NE.NonEmpty PositionedComment -> IO () + groups = groupWith sourceFile comments + f :: [PositionedComment] -> IO () f group = do - let filename = sourceFile (NE.head group) + let filename = sourceFile (head group) result <- siReadFile sys (Just True) filename let contents = either (const "") id result - outputResult filename contents (NE.toList group) + outputResult filename contents group outputResult filename contents warnings = do let comments = makeNonVirtual warnings contents diff --git a/src/ShellCheck/Formatter/JSON1.hs b/src/ShellCheck/Formatter/JSON1.hs index b4dbe35..2169bf6 100644 --- a/src/ShellCheck/Formatter/JSON1.hs +++ b/src/ShellCheck/Formatter/JSON1.hs @@ -27,9 +27,9 @@ import Control.DeepSeq import Data.Aeson import Data.IORef import Data.Monoid +import GHC.Exts import System.IO import qualified Data.ByteString.Lazy.Char8 as BL -import qualified Data.List.NonEmpty as NE format :: IO Formatter format = do @@ -114,10 +114,10 @@ outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg collectResult ref cr sys = mapM_ f groups where comments = crComments cr - groups = NE.groupWith sourceFile comments - f :: NE.NonEmpty PositionedComment -> IO () + groups = groupWith sourceFile comments + f :: [PositionedComment] -> IO () f group = do - let filename = sourceFile (NE.head group) + let filename = sourceFile (head group) result <- siReadFile sys (Just True) filename let contents = either (const "") id result let comments' = makeNonVirtual comments contents diff --git a/src/ShellCheck/Formatter/TTY.hs b/src/ShellCheck/Formatter/TTY.hs index 117da6e..e28696c 100644 --- a/src/ShellCheck/Formatter/TTY.hs +++ b/src/ShellCheck/Formatter/TTY.hs @@ -31,9 +31,9 @@ import Data.Ord import Data.IORef import Data.List import Data.Maybe +import GHC.Exts import System.IO import System.Info -import qualified Data.List.NonEmpty as NE wikiLink = "https://www.shellcheck.net/wiki/" @@ -117,19 +117,19 @@ outputResult options ref result sys = do color <- getColorFunc $ foColorOption options let comments = crComments result appendComments ref comments (fromIntegral $ foWikiLinkCount options) - let fileGroups = NE.groupWith sourceFile comments + let fileGroups = groupWith sourceFile comments mapM_ (outputForFile color sys) fileGroups outputForFile color sys comments = do - let fileName = sourceFile (NE.head comments) + let fileName = sourceFile (head comments) result <- siReadFile sys (Just True) fileName let contents = either (const "") id result let fileLinesList = lines contents let lineCount = length fileLinesList let fileLines = listArray (1, lineCount) fileLinesList - let groups = NE.groupWith lineNo comments + let groups = groupWith lineNo comments forM_ groups $ \commentsForLine -> do - let lineNum = fromIntegral $ lineNo (NE.head commentsForLine) + let lineNum = fromIntegral $ lineNo (head commentsForLine) let line = if lineNum < 1 || lineNum > lineCount then "" else fileLines ! fromIntegral lineNum @@ -139,7 +139,7 @@ outputForFile color sys comments = do putStrLn (color "source" line) forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c putStrLn "" - showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines + showFixedString color commentsForLine (fromIntegral lineNum) fileLines -- Pick out only the lines necessary to show a fix in action sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) @@ -169,7 +169,7 @@ showFixedString color comments lineNum fileLines = -- and/or other unrelated lines. let (excerptFix, excerpt) = sliceFile mergedFix fileLines -- in the spirit of error prone - putStrLn $ color "message" "Did you mean:" + putStrLn $ color "message" "Did you mean: " putStrLn $ unlines $ applyFix excerptFix excerpt cuteIndent :: PositionedComment -> String diff --git a/src/ShellCheck/Interface.hs b/src/ShellCheck/Interface.hs index 16a7e36..7528559 100644 --- a/src/ShellCheck/Interface.hs +++ b/src/ShellCheck/Interface.hs @@ -1,5 +1,5 @@ {- - Copyright 2012-2024 Vidar Holen + Copyright 2012-2019 Vidar Holen This file is part of ShellCheck. https://www.shellcheck.net @@ -21,14 +21,14 @@ module ShellCheck.Interface ( SystemInterface(..) - , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks) + , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks) , CheckResult(crFilename, crComments) , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) , ParseResult(prComments, prTokenPositions, prRoot) - , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks) + , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks) , AnalysisResult(arComments) , FormatterOptions(foColorOption, foWikiLinkCount) - , Shell(Ksh, Sh, Bash, Dash, BusyboxSh) + , Shell(Ksh, Sh, Bash, Dash) , ExecutionMode(Executed, Sourced) , ErrorMessage , Code @@ -39,12 +39,11 @@ module ShellCheck.Interface , ColorOption(ColorAuto, ColorAlways, ColorNever) , TokenComment(tcId, tcComment, tcFix) , emptyCheckResult - , newAnalysisResult - , newAnalysisSpec - , newFormatterOptions , newParseResult + , newAnalysisSpec + , newAnalysisResult + , newFormatterOptions , newPosition - , newSystemInterface , newTokenComment , mockedSystemInterface , mockRcFile @@ -100,7 +99,6 @@ data CheckSpec = CheckSpec { csIncludedWarnings :: Maybe [Integer], csShellTypeOverride :: Maybe Shell, csMinSeverity :: Severity, - csExtendedAnalysis :: Maybe Bool, csOptionalChecks :: [String] } deriving (Show, Eq) @@ -125,7 +123,6 @@ emptyCheckSpec = CheckSpec { csIncludedWarnings = Nothing, csShellTypeOverride = Nothing, csMinSeverity = StyleC, - csExtendedAnalysis = Nothing, csOptionalChecks = [] } @@ -138,14 +135,6 @@ newParseSpec = ParseSpec { psShellTypeOverride = Nothing } -newSystemInterface :: Monad m => SystemInterface m -newSystemInterface = - SystemInterface { - siReadFile = \_ _ -> return $ Left "Not implemented", - siFindSource = \_ _ _ name -> return name, - siGetConfig = \_ -> return Nothing - } - -- Parser input and output data ParseSpec = ParseSpec { psFilename :: String, @@ -176,7 +165,6 @@ data AnalysisSpec = AnalysisSpec { asExecutionMode :: ExecutionMode, asCheckSourced :: Bool, asOptionalChecks :: [String], - asExtendedAnalysis :: Maybe Bool, asTokenPositions :: Map.Map Id (Position, Position) } @@ -187,7 +175,6 @@ newAnalysisSpec token = AnalysisSpec { asExecutionMode = Executed, asCheckSourced = False, asOptionalChecks = [], - asExtendedAnalysis = Nothing, asTokenPositions = Map.empty } @@ -225,7 +212,7 @@ newCheckDescription = CheckDescription { } -- Supporting data types -data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq) +data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq) data ExecutionMode = Executed | Sourced deriving (Show, Eq) type ErrorMessage = String @@ -324,7 +311,7 @@ data ColorOption = -- For testing mockedSystemInterface :: [(String, String)] -> SystemInterface Identity -mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) { +mockedSystemInterface files = SystemInterface { siReadFile = rf, siFindSource = fs, siGetConfig = const $ return Nothing @@ -339,3 +326,4 @@ mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) { mockRcFile rcfile mock = mock { siGetConfig = const . return $ Just (".shellcheckrc", rcfile) } + diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs index 84c3ce4..7a50967 100644 --- a/src/ShellCheck/Parser.hs +++ b/src/ShellCheck/Parser.hs @@ -46,7 +46,6 @@ import Text.Parsec.Error import Text.Parsec.Pos import qualified Control.Monad.Reader as Mr import qualified Control.Monad.State as Ms -import qualified Data.List.NonEmpty as NE import qualified Data.Map.Strict as Map import Test.QuickCheck.All (quickCheckAll) @@ -141,9 +140,15 @@ carriageReturn = do parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ." return '\r' -almostSpace = do - parseNote ErrorC 1018 $ "This is a unicode space. Delete and retype it." - oneOf "\xA0\x2002\x2003\x2004\x2005\x2006\x2007\x2008\x2009\x200B\x202F" +almostSpace = + choice [ + check '\xA0' "unicode non-breaking space", + check '\x200B' "unicode zerowidth space" + ] + where + check c name = do + parseNote ErrorC 1018 $ "This is a " ++ name ++ ". Delete and retype it." + char c return ' ' --------- Message/position annotation on top of user state @@ -155,7 +160,7 @@ data Context = deriving (Show) data HereDocContext = - HereDocPending Id Dashed Quoted String [Context] -- on linefeed, read this T_HereDoc + HereDocPending Token [Context] -- on linefeed, read this T_HereDoc deriving (Show) data UserState = UserState { @@ -233,12 +238,12 @@ addToHereDocMap id list = do hereDocMap = Map.insert id list map } -addPendingHereDoc id d q str = do +addPendingHereDoc t = do state <- getState context <- getCurrentContexts let docs = pendingHereDocs state putState $ state { - pendingHereDocs = HereDocPending id d q str context : docs + pendingHereDocs = HereDocPending t context : docs } popPendingHereDocs = do @@ -821,7 +826,7 @@ readArithmeticContents = char ')' id <- endSpan start spacing - return $ TA_Parenthesis id s + return $ TA_Parentesis id s readArithTerm = readGroup <|> readVariable <|> readExpansion @@ -1052,16 +1057,6 @@ readAnnotationWithoutPrefix sandboxed = do "This shell type is unknown. Use e.g. sh or bash." return [ShellOverride shell] - "extended-analysis" -> do - pos <- getPosition - value <- plainOrQuoted $ many1 letter - case value of - "true" -> return [ExtendedAnalysis True] - "false" -> return [ExtendedAnalysis False] - _ -> do - parseNoteAt pos ErrorC 1146 "Unknown extended-analysis value. Expected true/false." - return [] - "external-sources" -> do pos <- getPosition value <- plainOrQuoted $ many1 letter @@ -1199,7 +1194,7 @@ readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readDollarBracedLiteral = do start <- startSpan - vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable + vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable id <- endSpan start return $ T_Literal id $ concat vars @@ -1561,7 +1556,7 @@ readGenericLiteral endChars = do return $ concat strings readGenericLiteral1 endExp = do - strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp + strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp return $ concat strings readGenericEscaped = do @@ -1840,7 +1835,7 @@ readHereDoc = called "here document" $ do -- add empty tokens for now, read the rest in readPendingHereDocs let doc = T_HereDoc hid dashed quoted endToken [] - addPendingHereDoc hid dashed quoted endToken + addPendingHereDoc doc return doc where unquote :: String -> (Quoted, String) @@ -1861,7 +1856,7 @@ readPendingHereDocs = do docs <- popPendingHereDocs mapM_ readDoc docs where - readDoc (HereDocPending id dashed quoted endToken ctx) = + readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) = swapContext ctx $ do docStartPos <- getPosition @@ -2288,31 +2283,22 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d subRead name script = withContext (ContextSource name) $ - inSeparateContext $ do - oldState <- getState - setState $ oldState { pendingHereDocs = [] } - result <- subParse (initialPos name) (readScriptFile True) script - newState <- getState - setState $ newState { pendingHereDocs = pendingHereDocs oldState } - return result + inSeparateContext $ + subParse (initialPos name) (readScriptFile True) script readSource t = return t prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu" prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu" prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat" -prop_readPipeline4 = isOk readPipeline "! ! true" -prop_readPipeline5 = isOk readPipeline "true | ! true" readPipeline = do unexpecting "keyword/token" readKeyword - readBanged readPipeSequence - -readBanged parser = do - pos <- getPosition - (T_Bang id) <- g_Bang - next <- readBanged parser - return $ T_Banged id next - <|> parser + do + (T_Bang id) <- g_Bang + pipe <- readPipeSequence + return $ T_Banged id pipe + <|> + readPipeSequence prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1" prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo" @@ -2368,14 +2354,14 @@ readTerm = do readPipeSequence = do start <- startSpan - (cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand) + (cmds, pipes) <- sepBy1WithSeparators readCommand (readPipe `thenSkip` (spacing >> readLineBreak)) id <- endSpan start spacing return $ T_Pipeline id pipes cmds where sepBy1WithSeparators p s = do - let elems = (\x -> ([x], [])) <$> p + let elems = p >>= \x -> return ([x], []) let seps = do separator <- s return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator]) @@ -2398,10 +2384,6 @@ readCommand = choice [ ] readCmdName = do - -- If the command name is `!` then - optional . lookAhead . try $ do - char '!' - whitespace -- Ignore alias suppression optional . try $ do char '\\' @@ -2795,29 +2777,17 @@ readFunctionDefinition = called "function" $ do prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }" prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }" prop_readCoProc3 = isOk readCoProc "coproc echo bar" -prop_readCoProc4 = isOk readCoProc "coproc a=b echo bar" -prop_readCoProc5 = isOk readCoProc "coproc 'foo' { echo bar; }" -prop_readCoProc6 = isOk readCoProc "coproc \"foo$$\" { echo bar; }" -prop_readCoProc7 = isOk readCoProc "coproc 'foo' ( echo bar )" -prop_readCoProc8 = isOk readCoProc "coproc \"foo$$\" while true; do true; done" readCoProc = called "coproc" $ do start <- startSpan try $ do string "coproc" - spacing1 + whitespace choice [ try $ readCompoundCoProc start, readSimpleCoProc start ] where readCompoundCoProc start = do - notFollowedBy2 readAssignmentWord - (var, body) <- choice [ - try $ do - body <- readBody readCompoundCommand - return (Nothing, body), - try $ do - var <- readNormalWord `thenSkip` spacing - body <- readBody readCompoundCommand - return (Just var, body) - ] + var <- optionMaybe $ + readVariableName `thenSkip` whitespace + body <- readBody readCompoundCommand id <- endSpan start return $ T_CoProc id var body readSimpleCoProc start = do @@ -2921,8 +2891,8 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord) kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos) kludgeAwayQuotes s p = case s of - first:second:rest -> - let (last NE.:| backwards) = NE.reverse (second NE.:| rest) + first:rest@(_:_) -> + let (last:backwards) = reverse rest middle = reverse backwards in if first `elem` "'\"" && first == last @@ -3352,12 +3322,10 @@ readScriptFile sourced = do then do commands <- readCompoundListOrEmpty id <- endSpan start - readPendingHereDocs verifyEof let script = T_Annotation annotationId annotations $ T_Script id shebang commands - userstate <- getState - reparseIndices $ reattachHereDocs script (hereDocMap userstate) + reparseIndices script else do many anyChar id <- endSpan start @@ -3367,8 +3335,8 @@ readScriptFile sourced = do verifyShebang pos s = do case isValidShell s of Just True -> return () - Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh/'busybox sh' scripts. Sorry!" - Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh/'busybox sh'. Add a 'shell' directive to specify." + Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!" + Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify." isValidShell s = let good = null s || any (`isPrefixOf` s) goodShells @@ -3384,20 +3352,16 @@ readScriptFile sourced = do "sh", "ash", "dash", - "busybox sh", "bash", "bats", - "ksh", - "oksh" + "ksh" ] badShells = [ "awk", "csh", "expect", - "fish", "perl", "python", - "python3", "ruby", "tcsh", "zsh" @@ -3450,22 +3414,13 @@ isOk p s = parsesCleanly p s == Just True -- The string parses with no wa isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse --- If the parser matches the string, return Right [ParseNotes+ParseProblems] --- If it does not match the string, return Left [ParseProblems] -getParseOutput parser string = runIdentity $ do - (res, systemState) <- runParser testEnvironment - (parser >> eof >> getState) "-" string - return $ case res of - Right userState -> - Right $ parseNotes userState ++ parseProblems systemState - Left _ -> Left $ parseProblems systemState - --- If the parser matches the string, return Just whether it was clean (without emitting suggestions) --- Otherwise, Nothing -parsesCleanly parser string = - case getParseOutput parser string of - Right list -> Just $ null list - Left _ -> Nothing +parsesCleanly parser string = runIdentity $ do + (res, sys) <- runParser testEnvironment + (parser >> eof >> getState) "-" string + case (res, sys) of + (Right userState, systemState) -> + return $ Just . null $ parseNotes userState ++ parseProblems systemState + (Left _, _) -> return Nothing parseWithNotes parser = do item <- parser @@ -3483,8 +3438,9 @@ makeErrorFor parsecError = pos = errorPos parsecError getStringFromParsec errors = - headOrDefault "" (mapMaybe f $ reverse errors) ++ - " Fix any mentioned problems and try again." + case map f errors of + r -> unwords (take 1 $ catMaybes $ reverse r) ++ + " Fix any mentioned problems and try again." where f err = case err of @@ -3515,7 +3471,8 @@ parseShell env name contents = do return newParseResult { prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state, prTokenPositions = Map.map startEndPosToPos (positionMap userstate), - prRoot = Just script + prRoot = Just $ + reattachHereDocs script (hereDocMap userstate) } Left err -> do let context = contextStack state @@ -3533,11 +3490,13 @@ parseShell env name contents = do -- A final pass for ignoring parse errors after failed parsing isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack -notesForContext list = zipWith ($) [first, second] [(pos, str) | ContextName pos str <- list] +notesForContext list = zipWith ($) [first, second] $ filter isName list where - first (pos, str) = ParseNote pos pos ErrorC 1073 $ + isName (ContextName _ _) = True + isName _ = False + first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $ "Couldn't parse this " ++ str ++ ". Fix to allow more checks." - second (pos, str) = ParseNote pos pos InfoC 1009 $ + second (ContextName pos str) = ParseNote pos pos InfoC 1009 $ "The mentioned syntax error was in this " ++ str ++ "." -- Go over all T_UnparsedIndex and reparse them as either arithmetic or text diff --git a/test/check_release b/test/check_release index f3ea9df..fd1dbca 100755 --- a/test/check_release +++ b/test/check_release @@ -12,17 +12,6 @@ then fail "There are uncommitted changes" fi -version=${current#v} -if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version" -then - fail "The cabal file does not match tag version $version" -fi - -if ! grep -qF "## $current" CHANGELOG.md -then - fail "CHANGELOG.md does not contain '## $current'" -fi - current=$(git tag --points-at) if [[ -z "$current" ]] then @@ -45,30 +34,33 @@ then fail "You are not on master" fi +version=${current#v} +if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version" +then + fail "The cabal file does not match tag version $version" +fi + +if ! grep -qF "## $current" CHANGELOG.md +then + fail "CHANGELOG.md does not contain '## $current'" +fi + if [[ $(git log -1 --pretty=%B) != "Stable version "* ]] then fail "Expected git log message to be 'Stable version ...'" fi -if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]] -then - fail "Expected git log message to contain CHANGELOG" -fi - i=1 j=1 cat << EOF Manual Checklist $((i++)). Make sure none of the automated checks above failed -$((i++)). Run \`build/build_builder build/*/\` to update all builder images. -$((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` to verify that they work. -$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them. -$((i++)). Run test/distrotest to ensure that most distros can build OOTB. $((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions $((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman +$((i++)). Run test/distrotest to ensure that most distros can build OOTB. $((i++)). Format and read over the manual for bad formatting and outdated info. -$((i++)). Make sure the Hackage package builds locally. +$((i++)). Make sure the Hackage package builds. Release Steps diff --git a/test/distrotest b/test/distrotest index 128ee44..4ad66f8 100755 --- a/test/distrotest +++ b/test/distrotest @@ -17,13 +17,13 @@ and is still highly experimental. Make sure you're plugged in and have screen/tmux in place, then re-run with $0 --run to continue. -Also note that dist*/ and .stack-work/ will be deleted. +Also note that dist* will be deleted. EOF exit 0 } -echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..." -rm -rf dist dist-newstyle .stack-work +echo "Deleting 'dist' and 'dist-newstyle'..." +rm -rf dist dist-newstyle execs=$(find . -name shellcheck) @@ -74,12 +74,13 @@ fedora:latest dnf install -y cabal-install ghc-template-haskell-devel fi archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel # Ubuntu LTS -ubuntu:24.04 apt-get update && apt-get install -y cabal-install ubuntu:22.04 apt-get update && apt-get install -y cabal-install ubuntu:20.04 apt-get update && apt-get install -y cabal-install +ubuntu:18.04 apt-get update && apt-get install -y cabal-install +ubuntu:16.04 apt-get update && apt-get install -y cabal-install # Stack on Ubuntu LTS -ubuntu:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest +ubuntu:22.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest EOF exit "$final" diff --git a/test/shellcheck.hs b/test/shellcheck.hs index d5e056d..1a272af 100644 --- a/test/shellcheck.hs +++ b/test/shellcheck.hs @@ -18,24 +18,21 @@ import qualified ShellCheck.Parser main = do putStrLn "Running ShellCheck tests..." - failures <- filter (not . snd) <$> mapM sequenceA tests - if null failures then exitSuccess else do - putStrLn "Tests failed for the following module(s):" - mapM (putStrLn . ("- ShellCheck." ++) . fst) failures - exitFailure - where - tests = - [ ("Analytics" , ShellCheck.Analytics.runTests) - , ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests) - , ("ASTLib" , ShellCheck.ASTLib.runTests) - , ("CFG" , ShellCheck.CFG.runTests) - , ("CFGAnalysis" , ShellCheck.CFGAnalysis.runTests) - , ("Checker" , ShellCheck.Checker.runTests) - , ("Checks.Commands" , ShellCheck.Checks.Commands.runTests) - , ("Checks.ControlFlow" , ShellCheck.Checks.ControlFlow.runTests) - , ("Checks.Custom" , ShellCheck.Checks.Custom.runTests) - , ("Checks.ShellSupport", ShellCheck.Checks.ShellSupport.runTests) - , ("Fixer" , ShellCheck.Fixer.runTests) - , ("Formatter.Diff" , ShellCheck.Formatter.Diff.runTests) - , ("Parser" , ShellCheck.Parser.runTests) + results <- sequence [ + ShellCheck.Analytics.runTests + ,ShellCheck.AnalyzerLib.runTests + ,ShellCheck.ASTLib.runTests + ,ShellCheck.CFG.runTests + ,ShellCheck.CFGAnalysis.runTests + ,ShellCheck.Checker.runTests + ,ShellCheck.Checks.Commands.runTests + ,ShellCheck.Checks.ControlFlow.runTests + ,ShellCheck.Checks.Custom.runTests + ,ShellCheck.Checks.ShellSupport.runTests + ,ShellCheck.Fixer.runTests + ,ShellCheck.Formatter.Diff.runTests + ,ShellCheck.Parser.runTests ] + if and results + then exitSuccess + else exitFailure diff --git a/test/stacktest b/test/stacktest index b486c31..9eb8d1e 100755 --- a/test/stacktest +++ b/test/stacktest @@ -15,7 +15,7 @@ die() { echo "$*" >&2; exit 1; } command -v stack || die "stack is missing" -stack setup --allow-different-user || die "Failed to setup with default resolver" +stack setup || die "Failed to setup with default resolver" stack build --test || die "Failed to build/test with default resolver" # Nice to haves, but not necessary