├── .dockerignore ├── .ghci ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ └── build.yml ├── .github_deploy ├── .gitignore ├── .multi_arch_docker ├── .prepare_deploy ├── CHANGELOG.md ├── Dockerfile.multi-arch ├── LICENSE ├── README.md ├── ShellCheck.cabal ├── build ├── README.md ├── build_builder ├── darwin.aarch64 │ ├── Dockerfile │ ├── build │ └── tag ├── darwin.x86_64 │ ├── Dockerfile │ ├── build │ └── tag ├── linux.aarch64 │ ├── Dockerfile │ ├── build │ └── tag ├── linux.armv6hf │ ├── Dockerfile │ ├── build │ ├── cabal.project.freeze │ ├── scutil │ └── tag ├── linux.riscv64 │ ├── Dockerfile │ ├── build │ ├── cabal.project.freeze │ └── tag ├── linux.x86_64 │ ├── Dockerfile │ ├── build │ └── tag ├── run_builder └── windows.x86_64 │ ├── Dockerfile │ ├── build │ └── tag ├── doc ├── emacs-flycheck.png ├── shellcheck_logo.svg ├── terminal.png └── vim-syntastic.png ├── manpage ├── nextnumber ├── quickrun ├── quicktest ├── setgitversion ├── shellcheck.1.md ├── shellcheck.hs ├── snap └── snapcraft.yaml ├── src └── ShellCheck │ ├── AST.hs │ ├── ASTLib.hs │ ├── Analytics.hs │ ├── Analyzer.hs │ ├── AnalyzerLib.hs │ ├── CFG.hs │ ├── CFGAnalysis.hs │ ├── Checker.hs │ ├── Checks │ ├── Commands.hs │ ├── ControlFlow.hs │ ├── Custom.hs │ └── ShellSupport.hs │ ├── Data.hs │ ├── Debug.hs │ ├── Fixer.hs │ ├── Formatter │ ├── CheckStyle.hs │ ├── Diff.hs │ ├── Format.hs │ ├── GCC.hs │ ├── JSON.hs │ ├── JSON1.hs │ ├── Quiet.hs │ └── TTY.hs │ ├── Interface.hs │ ├── Parser.hs │ ├── Prelude.hs │ └── Regex.hs ├── stack.yaml ├── striptests └── test ├── buildtest ├── check_release ├── distrotest ├── shellcheck.hs └── stacktest /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !LICENSE 3 | !Setup.hs 4 | !ShellCheck.cabal 5 | !shellcheck.hs 6 | !src 7 | -------------------------------------------------------------------------------- /.ghci: -------------------------------------------------------------------------------- 1 | :set -idist/build/autogen -isrc 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### For bugs 2 | - Rule Id (if any, e.g. SC1000): 3 | - My shellcheck version (`shellcheck --version` or "online"): 4 | - [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086) 5 | - [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit 6 | 7 | #### For new checks and feature suggestions 8 | - [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this 9 | - [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related 10 | 11 | 12 | #### Here's a snippet or screenshot that shows the problem: 13 | 14 | ```sh 15 | 16 | #!/your/interpreter 17 | your script here 18 | 19 | ``` 20 | 21 | #### Here's what shellcheck currently says: 22 | 23 | 24 | 25 | #### Here's what I wanted or expected to see: 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build ShellCheck 2 | 3 | # Run this workflow every time a new commit pushed to your repository 4 | on: push 5 | 6 | jobs: 7 | package_source: 8 | name: Package Source Code 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Dependencies 12 | run: | 13 | sudo apt-get update 14 | sudo apt-mark manual ghc # Don't bother installing ghc just to tar up source 15 | sudo apt-get install cabal-install 16 | 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Deduce tags 23 | run: | 24 | mkdir source 25 | echo "latest" > source/tags 26 | if tag=$(git describe --exact-match --tags) 27 | then 28 | echo "stable" >> source/tags 29 | echo "$tag" >> source/tags 30 | fi 31 | cat source/tags 32 | 33 | - name: Package Source 34 | run: | 35 | grep "stable" source/tags || ./setgitversion 36 | cabal sdist 37 | mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz 38 | 39 | - name: Upload artifact 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: source 43 | path: source/ 44 | 45 | run_tests: 46 | name: Run tests 47 | needs: package_source 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Download artifacts 51 | uses: actions/download-artifact@v4 52 | 53 | - name: Install dependencies 54 | run: | 55 | sudo apt-get update && sudo apt-get install ghc cabal-install 56 | cabal update 57 | 58 | - name: Unpack source 59 | run: | 60 | cd source 61 | tar xvf source.tar.gz --strip-components=1 62 | 63 | - name: Build and run tests 64 | run: | 65 | cd source 66 | cabal test 67 | 68 | build_source: 69 | name: Build 70 | needs: package_source 71 | strategy: 72 | matrix: 73 | build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64] 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Checkout repository 77 | uses: actions/checkout@v4 78 | 79 | - name: Download artifacts 80 | uses: actions/download-artifact@v4 81 | 82 | - name: Build source 83 | run: | 84 | mkdir -p bin 85 | mkdir -p bin/${{matrix.build}} 86 | ( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} ) 87 | 88 | - name: Upload artifact 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: ${{matrix.build}}.bin 92 | path: bin/ 93 | 94 | package_binary: 95 | name: Package Binaries 96 | needs: build_source 97 | runs-on: ubuntu-latest 98 | steps: 99 | - name: Checkout repository 100 | uses: actions/checkout@v4 101 | 102 | - name: Download artifacts 103 | uses: actions/download-artifact@v4 104 | 105 | - name: Work around GitHub permissions bug 106 | run: chmod +x *.bin/*/shellcheck* 107 | 108 | - name: Package binaries 109 | run: | 110 | export TAGS="$(cat source/tags)" 111 | mkdir -p deploy 112 | cp -r *.bin/* deploy 113 | cd deploy 114 | ../.prepare_deploy 115 | rm -rf */ README* LICENSE* 116 | 117 | - name: Upload artifact 118 | uses: actions/upload-artifact@v4 119 | with: 120 | name: deploy 121 | path: deploy/ 122 | 123 | deploy: 124 | name: Deploy binaries 125 | needs: package_binary 126 | runs-on: ubuntu-latest 127 | environment: Deploy 128 | steps: 129 | - name: Install Dependencies 130 | run: | 131 | sudo apt-get update 132 | sudo apt-get install hub 133 | 134 | - name: Checkout repository 135 | uses: actions/checkout@v4 136 | 137 | - name: Download artifacts 138 | uses: actions/download-artifact@v4 139 | 140 | - name: Upload to GitHub 141 | env: 142 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 143 | run: | 144 | export TAGS="$(cat source/tags)" 145 | ./.github_deploy 146 | 147 | - name: Waiting for GitHub to replicate uploaded releases 148 | run: | 149 | sleep 300 150 | 151 | - name: Upload to Docker Hub 152 | env: 153 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 154 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 155 | DOCKER_EMAIL: ${{ secrets.DOCKER_EMAIL }} 156 | DOCKER_BASE: ${{ secrets.DOCKER_USERNAME }}/shellcheck 157 | run: | 158 | export TAGS="$(cat source/tags)" 159 | ( source ./.multi_arch_docker && set -eux && multi_arch_docker::main ) 160 | -------------------------------------------------------------------------------- /.github_deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | shopt -s extglob 4 | 5 | export EDITOR="touch" 6 | 7 | # Sanity check 8 | gh --version || exit 1 9 | hub release show latest || exit 1 10 | 11 | for tag in $TAGS 12 | do 13 | if ! hub release show "$tag" 14 | then 15 | echo "Creating new release $tag" 16 | git show --no-patch --format='format:%B' > description 17 | hub release create -F description "$tag" 18 | fi 19 | 20 | files=() 21 | for file in deploy/* 22 | do 23 | [[ $file == *.@(xz|gz|zip) ]] || continue 24 | [[ $file == *"$tag"* ]] || continue 25 | files+=("$file") 26 | done 27 | gh release upload "$tag" "${files[@]}" --clobber || exit 1 28 | done 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Haskell ### 4 | dist 5 | cabal-dev 6 | *.o 7 | *.hi 8 | *.chi 9 | *.chs.h 10 | .virtualenv 11 | .hsenv 12 | .cabal-sandbox/ 13 | cabal.sandbox.config 14 | cabal.config 15 | .stack-work 16 | 17 | ### Snap ### 18 | /snap/.snapcraft/ 19 | /stage/ 20 | /parts/ 21 | /prime/ 22 | *.snap 23 | /dist-newstyle/ 24 | -------------------------------------------------------------------------------- /.multi_arch_docker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script builds and deploys multi-architecture docker images from the 3 | # binaries previously built and deployed to GitHub. 4 | 5 | function multi_arch_docker::install_docker_buildx() { 6 | # Install QEMU multi-architecture support for docker buildx. 7 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 8 | 9 | # Instantiate docker buildx builder with multi-architecture support. 10 | docker buildx create --name mybuilder 11 | docker buildx use mybuilder 12 | # Start up buildx and verify that all is OK. 13 | docker buildx inspect --bootstrap 14 | } 15 | 16 | # Log in to Docker Hub for deployment. 17 | function multi_arch_docker::login_to_docker_hub() { 18 | echo "$DOCKER_PASSWORD" | docker login -u="$DOCKER_USERNAME" --password-stdin 19 | } 20 | 21 | # Run buildx build and push. Passed in arguments augment the command line. 22 | function multi_arch_docker::buildx() { 23 | mkdir -p /tmp/empty 24 | docker buildx build \ 25 | --platform "${DOCKER_PLATFORMS// /,}" \ 26 | --push \ 27 | --progress plain \ 28 | -f Dockerfile.multi-arch \ 29 | "$@" \ 30 | /tmp/empty 31 | rmdir /tmp/empty 32 | } 33 | 34 | # Build and push plain and alpine docker images for all tags. 35 | function multi_arch_docker::build_and_push_all() { 36 | for tag in $TAGS; do 37 | multi_arch_docker::buildx -t "$DOCKER_BASE:$tag" --build-arg "tag=$tag" 38 | multi_arch_docker::buildx -t "$DOCKER_BASE-alpine:$tag" \ 39 | --build-arg "tag=$tag" --target alpine 40 | done 41 | } 42 | 43 | # Test all pushed docker images. 44 | function multi_arch_docker::test_all() { 45 | printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript 46 | 47 | for platform in $DOCKER_PLATFORMS; do 48 | for tag in $TAGS; do 49 | for ext in '-alpine' ''; do 50 | image="${DOCKER_BASE}${ext}:${tag}" 51 | msg="Testing docker image $image on platform $platform" 52 | line="${msg//?/=}" 53 | printf '\n%s\n%s\n%s\n' "${line}" "${msg}" "${line}" 54 | docker pull -q --platform "$platform" "$image" 55 | if [ -n "$ext" ]; then 56 | echo -n "Image architecture: " 57 | docker run --rm --entrypoint /bin/sh "$image" -c 'uname -m' 58 | version=$(docker run --rm "$image" shellcheck --version \ 59 | | grep 'version:') 60 | else 61 | version=$(docker run --rm "$image" --version | grep 'version:') 62 | fi 63 | version=${version/#version: /v} 64 | echo "shellcheck version: $version" 65 | if [[ ! ("$tag" =~ ^(latest|stable)$) && "$tag" != "$version" ]]; then 66 | echo "Version mismatch: shellcheck $version tagged as $tag" 67 | exit 1 68 | fi 69 | if [ -n "$ext" ]; then 70 | docker run --rm -v "$PWD:/mnt" -w /mnt "$image" shellcheck myscript 71 | else 72 | docker run --rm -v "$PWD:/mnt" "$image" myscript 73 | fi 74 | done 75 | done 76 | done 77 | } 78 | 79 | function multi_arch_docker::main() { 80 | export DOCKER_PLATFORMS='linux/amd64' 81 | DOCKER_PLATFORMS+=' linux/arm64' 82 | DOCKER_PLATFORMS+=' linux/arm/v6' 83 | DOCKER_PLATFORMS+=' linux/riscv64' 84 | 85 | multi_arch_docker::install_docker_buildx 86 | multi_arch_docker::login_to_docker_hub 87 | multi_arch_docker::build_and_push_all 88 | multi_arch_docker::test_all 89 | } 90 | -------------------------------------------------------------------------------- /.prepare_deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script packages up compiled binaries 3 | set -ex 4 | shopt -s nullglob extglob 5 | 6 | ls -l 7 | 8 | cp ../LICENSE LICENSE.txt 9 | sed -e $'s/$/\r/' > README.txt << END 10 | This is a precompiled ShellCheck binary. 11 | https://www.shellcheck.net/ 12 | 13 | ShellCheck is a static analysis tool for shell scripts. 14 | It's licensed under the GNU General Public License v3.0. 15 | Information and source code is available on the website. 16 | 17 | This binary was compiled on $(date -u). 18 | 19 | 20 | 21 | ====== Latest commits ====== 22 | 23 | $(git log -n 3) 24 | END 25 | 26 | for dir in */ 27 | do 28 | cp LICENSE.txt README.txt "$dir" 29 | done 30 | 31 | echo "Tags are $TAGS" 32 | 33 | for tag in $TAGS 34 | do 35 | 36 | for dir in windows.*/ 37 | do 38 | ( cd "$dir" && zip "../shellcheck-$tag.zip" * ) 39 | done 40 | 41 | for dir in {linux,darwin}.*/ 42 | do 43 | base="${dir%/}" 44 | ( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * ) 45 | done 46 | done 47 | 48 | for file in ./* 49 | do 50 | [[ -f "$file" ]] || continue 51 | sha512sum "$file" > "$file.sha512sum" 52 | done 53 | 54 | ls -l 55 | -------------------------------------------------------------------------------- /Dockerfile.multi-arch: -------------------------------------------------------------------------------- 1 | # Alpine image 2 | FROM alpine:latest AS alpine 3 | LABEL maintainer="Vidar Holen " 4 | ARG tag 5 | 6 | # Put the right binary for each architecture into place for the 7 | # multi-architecture docker image. 8 | RUN set -x; \ 9 | arch="$(uname -m)"; \ 10 | echo "arch is $arch"; \ 11 | if [ "${arch}" = 'armv7l' ]; then \ 12 | arch='armv6hf'; \ 13 | fi; \ 14 | url_base='https://github.com/koalaman/shellcheck/releases/download/'; \ 15 | tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \ 16 | wget "${url_base}${tar_file}" -O - | tar xJf -; \ 17 | mv "shellcheck-${tag}/shellcheck" /bin/; \ 18 | rm -rf "shellcheck-${tag}"; \ 19 | ls -laF /bin/shellcheck 20 | 21 | # ShellCheck image 22 | FROM scratch 23 | LABEL maintainer="Vidar Holen " 24 | WORKDIR /mnt 25 | COPY --from=alpine /bin/shellcheck /bin/ 26 | ENTRYPOINT ["/bin/shellcheck"] 27 | -------------------------------------------------------------------------------- /ShellCheck.cabal: -------------------------------------------------------------------------------- 1 | Name: ShellCheck 2 | Version: 0.10.0 3 | Synopsis: Shell script analysis tool 4 | License: GPL-3 5 | License-file: LICENSE 6 | Category: Static Analysis 7 | Author: Vidar Holen 8 | Maintainer: vidar@vidarholen.net 9 | Homepage: https://www.shellcheck.net/ 10 | Build-Type: Simple 11 | Cabal-Version: 1.18 12 | Bug-reports: https://github.com/koalaman/shellcheck/issues 13 | Description: 14 | The goals of ShellCheck are: 15 | . 16 | * To point out and clarify typical beginner's syntax issues, 17 | that causes a shell to give cryptic error messages. 18 | . 19 | * To point out and clarify typical intermediate level semantic problems, 20 | that causes a shell to behave strangely and counter-intuitively. 21 | . 22 | * To point out subtle caveats, corner cases and pitfalls, that may cause an 23 | advanced user's otherwise working script to fail under future circumstances. 24 | 25 | Extra-Doc-Files: 26 | README.md 27 | CHANGELOG.md 28 | Extra-Source-Files: 29 | -- documentation 30 | shellcheck.1.md 31 | -- A script to build the man page using pandoc 32 | manpage 33 | -- convenience script for stripping tests 34 | striptests 35 | -- tests 36 | test/shellcheck.hs 37 | 38 | source-repository head 39 | type: git 40 | location: git://github.com/koalaman/shellcheck.git 41 | 42 | library 43 | hs-source-dirs: src 44 | if impl(ghc < 8.0) 45 | build-depends: 46 | semigroups 47 | build-depends: 48 | -- The lower bounds are based on GHC 7.10.3 49 | -- The upper bounds are based on GHC 9.8.1 50 | aeson >= 1.4.0 && < 2.3, 51 | array >= 0.5.1 && < 0.6, 52 | base >= 4.8.0.0 && < 5, 53 | bytestring >= 0.10.6 && < 0.13, 54 | containers >= 0.5.6 && < 0.8, 55 | deepseq >= 1.4.1 && < 1.6, 56 | Diff >= 0.4.0 && < 1.1, 57 | fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), 58 | filepath >= 1.4.0 && < 1.6, 59 | mtl >= 2.2.2 && < 2.4, 60 | parsec >= 3.1.14 && < 3.2, 61 | QuickCheck >= 2.14.2 && < 2.16, 62 | regex-tdfa >= 1.2.0 && < 1.4, 63 | transformers >= 0.4.2 && < 0.7, 64 | 65 | -- getXdgDirectory from 1.2.3.0 66 | directory >= 1.2.3 && < 1.4, 67 | 68 | -- When cabal supports it, move this to setup-depends: 69 | process 70 | exposed-modules: 71 | ShellCheck.AST 72 | ShellCheck.ASTLib 73 | ShellCheck.Analytics 74 | ShellCheck.Analyzer 75 | ShellCheck.AnalyzerLib 76 | ShellCheck.CFG 77 | ShellCheck.CFGAnalysis 78 | ShellCheck.Checker 79 | ShellCheck.Checks.Commands 80 | ShellCheck.Checks.ControlFlow 81 | ShellCheck.Checks.Custom 82 | ShellCheck.Checks.ShellSupport 83 | ShellCheck.Data 84 | ShellCheck.Debug 85 | ShellCheck.Fixer 86 | ShellCheck.Formatter.Format 87 | ShellCheck.Formatter.CheckStyle 88 | ShellCheck.Formatter.Diff 89 | ShellCheck.Formatter.GCC 90 | ShellCheck.Formatter.JSON 91 | ShellCheck.Formatter.JSON1 92 | ShellCheck.Formatter.TTY 93 | ShellCheck.Formatter.Quiet 94 | ShellCheck.Interface 95 | ShellCheck.Parser 96 | ShellCheck.Prelude 97 | ShellCheck.Regex 98 | other-modules: 99 | Paths_ShellCheck 100 | default-language: Haskell98 101 | 102 | executable shellcheck 103 | if impl(ghc < 8.0) 104 | build-depends: 105 | semigroups 106 | build-depends: 107 | aeson, 108 | array, 109 | base, 110 | bytestring, 111 | containers, 112 | deepseq, 113 | Diff, 114 | directory, 115 | fgl, 116 | mtl, 117 | filepath, 118 | parsec, 119 | QuickCheck, 120 | regex-tdfa, 121 | transformers, 122 | ShellCheck 123 | default-language: Haskell98 124 | main-is: shellcheck.hs 125 | 126 | test-suite test-shellcheck 127 | type: exitcode-stdio-1.0 128 | build-depends: 129 | aeson, 130 | array, 131 | base, 132 | bytestring, 133 | containers, 134 | deepseq, 135 | Diff, 136 | directory, 137 | fgl, 138 | filepath, 139 | mtl, 140 | parsec, 141 | QuickCheck, 142 | regex-tdfa, 143 | transformers, 144 | ShellCheck 145 | default-language: Haskell98 146 | main-is: test/shellcheck.hs 147 | -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | This directory contains Dockerfiles for all builds. 2 | 3 | A build image will: 4 | 5 | * Run on Linux x86\_64 with vanilla Docker (no exceptions) 6 | * Not contain any software that would restrict easy modification or copying 7 | * Take a `cabal sdist` style tar.gz of the ShellCheck directory on stdin 8 | * Output a tar.gz of artifacts on stdout, in a directory named for the arch 9 | 10 | This makes it simple to build any release without exotic hardware or software. 11 | 12 | An image can be built and tagged using `build_builder`, 13 | and run on a source tarball using `run_builder`. 14 | 15 | Tip: Are you developing an image that relies on QEmu usermode emulation? 16 | It's easy to accidentally depend on binfmt\_misc on the host OS. 17 | Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing. 18 | -------------------------------------------------------------------------------- /build/build_builder: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ $# -eq 0 ] 3 | then 4 | echo >&2 "No build image directories specified" 5 | echo >&2 "Example: $0 build/*/" 6 | exit 1 7 | fi 8 | 9 | for dir 10 | do 11 | ( cd "$dir" && docker build -t "$(cat tag)" . ) || exit 1 12 | done 13 | -------------------------------------------------------------------------------- /build/darwin.aarch64/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest 2 | 3 | ENV TARGET aarch64-apple-darwin22 4 | ENV TARGETNAME darwin.aarch64 5 | 6 | # Build dependencies 7 | USER root 8 | ENV DEBIAN_FRONTEND noninteractive 9 | ENV LC_ALL C.utf8 10 | 11 | # Install basic deps 12 | RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static 13 | 14 | # Install a more suitable host compiler 15 | WORKDIR /host-ghc 16 | 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 17 | 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 18 | RUN ./configure && make install 19 | 20 | # Build GHC. We have to use an old version because cross-compilation across OS has since broken. 21 | WORKDIR /ghc 22 | RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1 23 | RUN apt-get install -y llvm-12 24 | RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" 25 | RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" 26 | RUN make install 27 | 28 | # Due to an apparent cabal bug, we specify our options directly to cabal 29 | # It won't reuse caches if ghc-options are specified in ~/.cabal/config 30 | ENV CABALOPTS "--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0" 31 | 32 | # Prebuild the dependencies 33 | RUN cabal update 34 | RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck 35 | 36 | # Copy the build script 37 | COPY build /usr/bin 38 | 39 | WORKDIR /scratch 40 | ENTRYPOINT ["/usr/bin/build"] 41 | -------------------------------------------------------------------------------- /build/darwin.aarch64/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | { 4 | tar xzv --strip-components=1 5 | chmod +x striptests && ./striptests 6 | mkdir "$TARGETNAME" 7 | ( IFS=';'; cabal build $CABALOPTS ) 8 | find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; 9 | ls -l "$TARGETNAME" 10 | # Stripping invalidates the code signature and the build image does 11 | # not appear to have anything similar to the 'codesign' tool. 12 | # "$TARGET-strip" "$TARGETNAME/shellcheck" 13 | ls -l "$TARGETNAME" 14 | file "$TARGETNAME/shellcheck" | grep "Mach-O 64-bit arm64 executable" 15 | } >&2 16 | tar czv "$TARGETNAME" 17 | -------------------------------------------------------------------------------- /build/darwin.aarch64/tag: -------------------------------------------------------------------------------- 1 | koalaman/scbuilder-darwin-aarch64 2 | -------------------------------------------------------------------------------- /build/darwin.x86_64/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76 2 | 3 | ENV TARGET x86_64-apple-darwin18 4 | ENV TARGETNAME darwin.x86_64 5 | 6 | # Build dependencies 7 | USER root 8 | ENV DEBIAN_FRONTEND noninteractive 9 | RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list 10 | RUN apt-get update 11 | RUN apt-get dist-upgrade -y 12 | RUN apt-get install -y ghc automake autoconf llvm curl alex happy 13 | 14 | # Build GHC 15 | WORKDIR /ghc 16 | RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1 17 | RUN ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" 18 | RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" 19 | RUN make install 20 | 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 21 | 22 | # Due to an apparent cabal bug, we specify our options directly to cabal 23 | # It won't reuse caches if ghc-options are specified in ~/.cabal/config 24 | ENV CABALOPTS "--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg" 25 | 26 | # Prebuild the dependencies 27 | RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck 28 | 29 | # Copy the build script 30 | COPY build /usr/bin 31 | 32 | WORKDIR /scratch 33 | ENTRYPOINT ["/usr/bin/build"] 34 | -------------------------------------------------------------------------------- /build/darwin.x86_64/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | { 4 | tar xzv --strip-components=1 5 | chmod +x striptests && ./striptests 6 | mkdir "$TARGETNAME" 7 | ( IFS=';'; cabal build $CABALOPTS ) 8 | find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; 9 | ls -l "$TARGETNAME" 10 | "$TARGET-strip" -Sx "$TARGETNAME/shellcheck" 11 | ls -l "$TARGETNAME" 12 | } >&2 13 | tar czv "$TARGETNAME" 14 | -------------------------------------------------------------------------------- /build/darwin.x86_64/tag: -------------------------------------------------------------------------------- 1 | koalaman/scbuilder-darwin-x86_64 2 | -------------------------------------------------------------------------------- /build/linux.aarch64/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV TARGET aarch64-linux-gnu 4 | ENV TARGETNAME linux.aarch64 5 | 6 | # Build dependencies 7 | USER root 8 | ENV DEBIAN_FRONTEND noninteractive 9 | 10 | # These deps are from 20.04, because GHC's compiler/llvm support moves slowly 11 | RUN apt-get update && apt-get install -y llvm gcc-$TARGET 12 | 13 | # The rest are from 22.10 14 | RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list 15 | # Kinetic does not receive updates anymore, switch to last available 16 | RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list 17 | RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list 18 | 19 | RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static 20 | 21 | # Build GHC 22 | WORKDIR /ghc 23 | RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1 24 | RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" 25 | RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" 26 | RUN make install 27 | 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 28 | 29 | # Due to an apparent cabal bug, we specify our options directly to cabal 30 | # It won't reuse caches if ghc-options are specified in ~/.cabal/config 31 | 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" 32 | 33 | # Prebuild the dependencies 34 | RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck 35 | 36 | # Copy the build script 37 | COPY build /usr/bin 38 | 39 | WORKDIR /scratch 40 | ENTRYPOINT ["/usr/bin/build"] 41 | -------------------------------------------------------------------------------- /build/linux.aarch64/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | { 4 | tar xzv --strip-components=1 5 | chmod +x striptests && ./striptests 6 | mkdir "$TARGETNAME" 7 | ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) 8 | find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; 9 | ls -l "$TARGETNAME" 10 | "$TARGET-strip" -s "$TARGETNAME/shellcheck" 11 | ls -l "$TARGETNAME" 12 | qemu-aarch64-static "$TARGETNAME/shellcheck" --version 13 | } >&2 14 | tar czv "$TARGETNAME" 15 | -------------------------------------------------------------------------------- /build/linux.aarch64/tag: -------------------------------------------------------------------------------- 1 | koalaman/scbuilder-linux-aarch64 2 | -------------------------------------------------------------------------------- /build/linux.armv6hf/Dockerfile: -------------------------------------------------------------------------------- 1 | # This Docker file uses a custom QEmu fork with patches to follow execve 2 | # to build all of ShellCheck emulated. 3 | 4 | FROM ubuntu:24.04 5 | 6 | ENV TARGETNAME linux.armv6hf 7 | 8 | # Build QEmu with execve follow support 9 | USER root 10 | ENV DEBIAN_FRONTEND noninteractive 11 | RUN apt-get update 12 | 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 13 | WORKDIR /qemu 14 | RUN git clone --depth 1 https://github.com/koalaman/qemu . 15 | RUN ./configure --static --disable-werror && cd build && ninja qemu-arm 16 | ENV QEMU_EXECVE 1 17 | 18 | # Convenience utility 19 | COPY scutil /bin/scutil 20 | COPY scutil /chroot/bin/scutil 21 | RUN chmod +x /bin/scutil /chroot/bin/scutil 22 | 23 | # Set up an armv6 userspace 24 | WORKDIR / 25 | RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian 26 | RUN cp /qemu/build/qemu-arm /chroot/bin/qemu 27 | RUN scutil emu /debootstrap/debootstrap --second-stage 28 | 29 | # Install deps in the chroot 30 | RUN scutil emu apt-get update 31 | RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install 32 | RUN scutil emu cabal update 33 | 34 | # Finally we can build the current dependencies. This takes hours. 35 | ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections" 36 | # Generated with `cabal freeze --constraint 'hashable -arch-native'` 37 | COPY cabal.project.freeze /chroot/etc 38 | RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS 39 | 40 | # Copy the build script 41 | COPY build /chroot/bin 42 | ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"] 43 | -------------------------------------------------------------------------------- /build/linux.armv6hf/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | mkdir /scratch && cd /scratch 4 | { 5 | tar xzv --strip-components=1 6 | cp /etc/cabal.project.freeze . 7 | chmod +x striptests && ./striptests 8 | mkdir "$TARGETNAME" 9 | # This script does not cabal update because compiling anything new is slow 10 | ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) 11 | find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; 12 | ls -l "$TARGETNAME" 13 | strip -s "$TARGETNAME/shellcheck" 14 | ls -l "$TARGETNAME" 15 | "$TARGETNAME/shellcheck" --version 16 | } >&2 17 | tar czv "$TARGETNAME" 18 | -------------------------------------------------------------------------------- /build/linux.armv6hf/cabal.project.freeze: -------------------------------------------------------------------------------- 1 | active-repositories: hackage.haskell.org:merge 2 | constraints: any.Diff ==0.5, 3 | any.OneTuple ==0.4.2, 4 | any.QuickCheck ==2.14.3, 5 | QuickCheck -old-random +templatehaskell, 6 | any.StateVar ==1.2.2, 7 | any.aeson ==2.2.3.0, 8 | aeson +ordered-keymap, 9 | any.array ==0.5.4.0, 10 | any.assoc ==1.1.1, 11 | assoc -tagged, 12 | any.base ==4.15.1.0, 13 | any.base-orphans ==0.9.2, 14 | any.bifunctors ==5.6.2, 15 | bifunctors +tagged, 16 | any.binary ==0.8.8.0, 17 | any.bytestring ==0.10.12.1, 18 | any.character-ps ==0.1, 19 | any.comonad ==5.0.8, 20 | comonad +containers +distributive +indexed-traversable, 21 | any.containers ==0.6.4.1, 22 | any.contravariant ==1.5.5, 23 | contravariant +semigroups +statevar +tagged, 24 | any.data-array-byte ==0.1.0.1, 25 | any.data-fix ==0.3.3, 26 | any.deepseq ==1.4.5.0, 27 | any.directory ==1.3.6.2, 28 | any.distributive ==0.6.2.1, 29 | distributive +semigroups +tagged, 30 | any.dlist ==1.0, 31 | dlist -werror, 32 | any.exceptions ==0.10.4, 33 | any.fgl ==5.8.2.0, 34 | fgl +containers042, 35 | any.filepath ==1.4.2.1, 36 | any.foldable1-classes-compat ==0.1, 37 | foldable1-classes-compat +tagged, 38 | any.generically ==0.1.1, 39 | any.ghc-bignum ==1.1, 40 | any.ghc-boot-th ==9.0.2, 41 | any.ghc-prim ==0.7.0, 42 | any.hashable ==1.4.6.0, 43 | hashable -arch-native +integer-gmp -random-initial-seed, 44 | any.indexed-traversable ==0.1.4, 45 | any.indexed-traversable-instances ==0.1.2, 46 | any.integer-conversion ==0.1.1, 47 | any.integer-logarithms ==1.0.3.1, 48 | integer-logarithms -check-bounds +integer-gmp, 49 | any.mtl ==2.2.2, 50 | any.network-uri ==2.6.4.2, 51 | any.parsec ==3.1.14.0, 52 | any.pretty ==1.1.3.6, 53 | any.primitive ==0.9.0.0, 54 | any.process ==1.6.13.2, 55 | any.random ==1.2.1.2, 56 | any.regex-base ==0.94.0.2, 57 | any.regex-tdfa ==1.3.2.2, 58 | regex-tdfa +doctest -force-o2, 59 | any.rts ==1.0.2, 60 | any.scientific ==0.3.8.0, 61 | scientific -integer-simple, 62 | any.semialign ==1.3.1, 63 | semialign +semigroupoids, 64 | any.semigroupoids ==6.0.1, 65 | semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers, 66 | any.splitmix ==0.1.0.5, 67 | splitmix -optimised-mixer, 68 | any.stm ==2.5.0.0, 69 | any.strict ==0.5, 70 | any.tagged ==0.8.8, 71 | tagged +deepseq +transformers, 72 | any.template-haskell ==2.17.0.0, 73 | any.text ==1.2.5.0, 74 | any.text-iso8601 ==0.1.1, 75 | any.text-short ==0.1.6, 76 | text-short -asserts, 77 | any.th-abstraction ==0.7.0.0, 78 | any.th-compat ==0.1.5, 79 | any.these ==1.2.1, 80 | any.time ==1.9.3, 81 | any.time-compat ==1.9.7, 82 | any.transformers ==0.5.6.2, 83 | any.transformers-compat ==0.7.2, 84 | transformers-compat -five +five-three -four +generic-deriving +mtl -three -two, 85 | any.unix ==2.7.2.2, 86 | any.unordered-containers ==0.2.20, 87 | unordered-containers -debug, 88 | any.uuid-types ==1.0.6, 89 | any.vector ==0.13.1.0, 90 | vector +boundschecks -internalchecks -unsafechecks -wall, 91 | any.vector-stream ==0.1.0.1, 92 | any.witherable ==0.5 93 | index-state: hackage.haskell.org 2024-06-18T02:21:19Z 94 | -------------------------------------------------------------------------------- /build/linux.armv6hf/scutil: -------------------------------------------------------------------------------- 1 | #!/bin/dash 2 | # Various ShellCheck build utility functions 3 | 4 | # Generally set a ulimit to avoid QEmu using too much memory 5 | ulimit -v "$((10*1024*1024))" 6 | # If we happen to invoke or run under QEmu, make sure to follow execve. 7 | # This requires a patched QEmu. 8 | export QEMU_EXECVE=1 9 | 10 | # Retry a command until it succeeds 11 | # Usage: scutil retry 3 mycmd 12 | retry() { 13 | n="$1" 14 | ret=1 15 | shift 16 | while [ "$n" -gt 0 ] 17 | do 18 | "$@" 19 | ret=$? 20 | [ "$ret" = 0 ] && break 21 | n=$((n-1)) 22 | done 23 | return "$ret" 24 | } 25 | 26 | # Install all dependencies from a freeze file 27 | # Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install 28 | install_from_freeze() { 29 | linefeed=$(printf '\nx') 30 | linefeed=${linefeed%x} 31 | flags=$( 32 | sed 's/constraints:/&\n /' "$1" | 33 | grep -vw -e rts -e base | 34 | sed -n -e 's/^ *\([^,]*\).*/\1/p' | 35 | sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e') 36 | shift 37 | # shellcheck disable=SC2086 38 | ( IFS=$linefeed; set -x; "$@" $flags ) 39 | } 40 | 41 | # Run a command under emulation. 42 | # This assumes the correct emulator is named 'qemu' and the chroot is /chroot 43 | # Usage: scutil emu echo "Hello World" 44 | emu() { 45 | chroot /chroot /bin/qemu /usr/bin/env "$@" 46 | } 47 | 48 | "$@" 49 | -------------------------------------------------------------------------------- /build/linux.armv6hf/tag: -------------------------------------------------------------------------------- 1 | koalaman/scbuilder-linux-armv6hf 2 | -------------------------------------------------------------------------------- /build/linux.riscv64/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | ENV TARGETNAME linux.riscv64 4 | ENV TARGET riscv64-linux-gnu 5 | 6 | USER root 7 | ENV DEBIAN_FRONTEND noninteractive 8 | 9 | # Init base 10 | RUN apt-get update -y 11 | 12 | # Install qemu 13 | 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 14 | WORKDIR /qemu 15 | RUN git clone --depth 1 https://github.com/koalaman/qemu . 16 | RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror 17 | RUN cd build && ninja qemu-riscv64 18 | ENV QEMU_EXECVE 1 19 | 20 | # Convenience utility 21 | COPY scutil /bin/scutil 22 | # We have to copy to /usr/bin because debootstrap will try to symlink /bin and fail if it exists 23 | COPY scutil /chroot/usr/bin/scutil 24 | RUN chmod +x /bin/scutil /chroot/usr/bin/scutil 25 | 26 | # Set up a riscv64 userspace 27 | WORKDIR / 28 | RUN debootstrap --arch=riscv64 --variant=minbase --components=main,universe --foreign noble /chroot http://ports.ubuntu.com/ubuntu-ports 29 | RUN cp /qemu/build/qemu-riscv64 /chroot/bin/qemu 30 | RUN scutil emu /debootstrap/debootstrap --second-stage 31 | 32 | # Install deps in the chroot 33 | RUN scutil emu apt-get update 34 | RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install 35 | RUN scutil emu cabal update 36 | 37 | # Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it. 38 | COPY cabal.project.freeze /chroot/etc 39 | 40 | # Build all dependencies from the freeze file. The emulator segfaults at random, 41 | # so retry a few times. 42 | RUN scutil install_from_freeze /chroot/etc/cabal.project.freeze retry 5 emu cabal install --keep-going 43 | 44 | # Copy the build script 45 | COPY build /chroot/bin/build 46 | ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"] 47 | -------------------------------------------------------------------------------- /build/linux.riscv64/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | IFS=';' 4 | { 5 | mkdir -p /tmp/scratch 6 | cd /tmp/scratch 7 | tar xzv --strip-components=1 8 | chmod +x striptests && ./striptests 9 | # Use a freeze file to ensure we use the same dependencies we cached during 10 | # the docker image build. We don't want to spend time compiling anything new. 11 | cp /etc/cabal.project.freeze . 12 | mkdir "$TARGETNAME" 13 | # Retry in case of random segfault 14 | scutil retry 3 cabal build --enable-executable-static 15 | find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; 16 | ls -l "$TARGETNAME" 17 | "$TARGET-strip" -s "$TARGETNAME/shellcheck" 18 | ls -l "$TARGETNAME" 19 | "$TARGETNAME/shellcheck" --version 20 | } >&2 21 | tar czv "$TARGETNAME" 22 | -------------------------------------------------------------------------------- /build/linux.riscv64/cabal.project.freeze: -------------------------------------------------------------------------------- 1 | active-repositories: hackage.haskell.org:merge 2 | constraints: any.Diff ==0.5, 3 | any.OneTuple ==0.4.2, 4 | any.QuickCheck ==2.14.3, 5 | QuickCheck -old-random +templatehaskell, 6 | any.StateVar ==1.2.2, 7 | any.aeson ==2.2.3.0, 8 | aeson +ordered-keymap, 9 | any.array ==0.5.4.0, 10 | any.assoc ==1.1.1, 11 | assoc -tagged, 12 | any.base ==4.17.2.0, 13 | any.base-orphans ==0.9.2, 14 | any.bifunctors ==5.6.2, 15 | bifunctors +tagged, 16 | any.binary ==0.8.9.1, 17 | any.bytestring ==0.11.5.2, 18 | any.character-ps ==0.1, 19 | any.comonad ==5.0.8, 20 | comonad +containers +distributive +indexed-traversable, 21 | any.containers ==0.6.7, 22 | any.contravariant ==1.5.5, 23 | contravariant +semigroups +statevar +tagged, 24 | any.data-fix ==0.3.3, 25 | any.deepseq ==1.4.8.0, 26 | any.directory ==1.3.7.1, 27 | any.distributive ==0.6.2.1, 28 | distributive +semigroups +tagged, 29 | any.dlist ==1.0, 30 | dlist -werror, 31 | any.exceptions ==0.10.5, 32 | any.fgl ==5.8.2.0, 33 | fgl +containers042, 34 | any.filepath ==1.4.2.2, 35 | any.foldable1-classes-compat ==0.1, 36 | foldable1-classes-compat +tagged, 37 | any.generically ==0.1.1, 38 | any.ghc-bignum ==1.3, 39 | any.ghc-boot-th ==9.4.7, 40 | any.ghc-prim ==0.9.1, 41 | any.hashable ==1.4.6.0, 42 | hashable -arch-native +integer-gmp -random-initial-seed, 43 | any.indexed-traversable ==0.1.4, 44 | any.indexed-traversable-instances ==0.1.2, 45 | any.integer-conversion ==0.1.1, 46 | any.integer-logarithms ==1.0.3.1, 47 | integer-logarithms -check-bounds +integer-gmp, 48 | any.mtl ==2.2.2, 49 | any.network-uri ==2.6.4.2, 50 | any.os-string ==2.0.3, 51 | any.parsec ==3.1.16.1, 52 | any.pretty ==1.1.3.6, 53 | any.primitive ==0.9.0.0, 54 | any.process ==1.6.17.0, 55 | any.random ==1.2.1.2, 56 | any.regex-base ==0.94.0.2, 57 | any.regex-tdfa ==1.3.2.2, 58 | regex-tdfa +doctest -force-o2, 59 | any.rts ==1.0.2, 60 | any.scientific ==0.3.8.0, 61 | scientific -integer-simple, 62 | any.semialign ==1.3.1, 63 | semialign +semigroupoids, 64 | any.semigroupoids ==6.0.1, 65 | semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers, 66 | any.splitmix ==0.1.0.5, 67 | splitmix -optimised-mixer, 68 | any.stm ==2.5.1.0, 69 | any.strict ==0.5, 70 | any.tagged ==0.8.8, 71 | tagged +deepseq +transformers, 72 | any.template-haskell ==2.19.0.0, 73 | any.text ==2.0.2, 74 | any.text-iso8601 ==0.1.1, 75 | any.text-short ==0.1.6, 76 | text-short -asserts, 77 | any.th-abstraction ==0.7.0.0, 78 | any.th-compat ==0.1.5, 79 | any.these ==1.2.1, 80 | any.time ==1.12.2, 81 | any.time-compat ==1.9.7, 82 | any.transformers ==0.5.6.2, 83 | any.transformers-compat ==0.7.2, 84 | transformers-compat -five +five-three -four +generic-deriving +mtl -three -two, 85 | any.unix ==2.7.3, 86 | any.unordered-containers ==0.2.20, 87 | unordered-containers -debug, 88 | any.uuid-types ==1.0.6, 89 | any.vector ==0.13.1.0, 90 | vector +boundschecks -internalchecks -unsafechecks -wall, 91 | any.vector-stream ==0.1.0.1, 92 | any.witherable ==0.5 93 | index-state: hackage.haskell.org 2024-06-17T00:48:51Z 94 | -------------------------------------------------------------------------------- /build/linux.riscv64/tag: -------------------------------------------------------------------------------- 1 | koalaman/scbuilder-linux-riscv64 2 | -------------------------------------------------------------------------------- /build/linux.x86_64/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | # alpine:3.16 (GHC 9.0.1): 5.8 megabytes 3 | # alpine:3.17 (GHC 9.0.2): 15.0 megabytes 4 | # alpine:3.18 (GHC 9.4.4): 29.0 megabytes 5 | # alpine:3.19 (GHC 9.4.7): 29.0 megabytes 6 | 7 | ENV TARGETNAME linux.x86_64 8 | 9 | # Install GHC and cabal 10 | USER root 11 | RUN apk add ghc cabal g++ libffi-dev curl bash 12 | 13 | # Use ld.bfd instead of ld.gold due to 14 | # x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error: 15 | # relocation refers to local symbol "" [2], which is defined in a discarded section 16 | ENV CABALOPTS "--ghc-options;-optl-Wl,-fuse-ld=bfd -split-sections -optc-Os -optc-Wl,--gc-sections" 17 | 18 | # Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement 19 | 20 | # Copy the build script 21 | COPY build /usr/bin 22 | 23 | WORKDIR /scratch 24 | ENTRYPOINT ["/usr/bin/build"] 25 | -------------------------------------------------------------------------------- /build/linux.x86_64/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | { 4 | tar xzv --strip-components=1 5 | chmod +x striptests && ./striptests 6 | mkdir "$TARGETNAME" 7 | cabal update 8 | ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) 9 | find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; 10 | ls -l "$TARGETNAME" 11 | strip -s "$TARGETNAME/shellcheck" 12 | ls -l "$TARGETNAME" 13 | "$TARGETNAME/shellcheck" --version 14 | } >&2 15 | tar czv "$TARGETNAME" 16 | -------------------------------------------------------------------------------- /build/linux.x86_64/tag: -------------------------------------------------------------------------------- 1 | koalaman/scbuilder-linux-x86_64 2 | -------------------------------------------------------------------------------- /build/run_builder: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ $# -lt 2 ] 3 | then 4 | echo >&2 "This script builds a source archive (as produced by cabal sdist)" 5 | echo >&2 "Usage: $0 sourcefile.tar.gz builddir..." 6 | exit 1 7 | fi 8 | 9 | file=$(realpath "$1") 10 | shift 11 | 12 | if [ ! -e "$file" ] 13 | then 14 | echo >&2 "$file does not exist" 15 | exit 1 16 | fi 17 | 18 | set -ex -o pipefail 19 | 20 | for dir 21 | do 22 | tagfile="$dir/tag" 23 | if [ ! -e "$tagfile" ] 24 | then 25 | echo >&2 "$tagfile does not exist" 26 | exit 2 27 | fi 28 | 29 | docker run -i "$(< "$tagfile")" < "$file" | tar xz 30 | done 31 | -------------------------------------------------------------------------------- /build/windows.x86_64/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV TARGETNAME windows.x86_64 4 | 5 | # We don't need wine32, even though it complains 6 | USER root 7 | ENV DEBIAN_FRONTEND noninteractive 8 | RUN apt-get update && apt-get install -y curl busybox wine winbind 9 | 10 | # Fetch Windows version, will be available under z:\haskell 11 | WORKDIR /haskell 12 | 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 13 | WORKDIR /haskell/bin 14 | 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 - 15 | 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/* . 16 | ENV WINEPATH /haskell/bin 17 | 18 | # It's unknown whether Cabal on Windows suffers from the same issue 19 | # that necessitated this but I don't care enough to find out 20 | ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections" 21 | 22 | # Precompile some deps to speed up later builds 23 | RUN wine /haskell/bin/cabal.exe update && IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck 24 | 25 | COPY build /usr/bin 26 | WORKDIR /scratch 27 | ENTRYPOINT ["/usr/bin/build"] 28 | -------------------------------------------------------------------------------- /build/windows.x86_64/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cabal() { 3 | wine /haskell/bin/cabal.exe "$@" 4 | } 5 | 6 | set -xe 7 | { 8 | tar xzv --strip-components=1 9 | chmod +x striptests && ./striptests 10 | mkdir "$TARGETNAME" 11 | ( IFS=';'; cabal build $CABALOPTS ) 12 | find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \; 13 | ls -l "$TARGETNAME" 14 | wine "/haskell/mingw/bin/strip.exe" -s "$TARGETNAME/shellcheck.exe" 15 | ls -l "$TARGETNAME" 16 | wine "$TARGETNAME/shellcheck.exe" --version 17 | } >&2 18 | tar czv "$TARGETNAME" 19 | -------------------------------------------------------------------------------- /build/windows.x86_64/tag: -------------------------------------------------------------------------------- 1 | koalaman/scbuilder-windows-x86_64 2 | -------------------------------------------------------------------------------- /doc/emacs-flycheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koalaman/shellcheck/20d11c1c33df71a375406c981a629269ee5149a2/doc/emacs-flycheck.png -------------------------------------------------------------------------------- /doc/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koalaman/shellcheck/20d11c1c33df71a375406c981a629269ee5149a2/doc/terminal.png -------------------------------------------------------------------------------- /doc/vim-syntastic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koalaman/shellcheck/20d11c1c33df71a375406c981a629269ee5149a2/doc/vim-syntastic.png -------------------------------------------------------------------------------- /manpage: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo >&2 "Generating man page using pandoc" 3 | pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit 4 | echo >&2 "Done. You can read it with: man ./shellcheck.1" 5 | -------------------------------------------------------------------------------- /nextnumber: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # TODO: Find a less trashy way to get the next available error code 3 | if ! shopt -s globstar 4 | then 5 | echo "Error: This script depends on Bash 4." >&2 6 | exit 1 7 | fi 8 | 9 | for i in 1 2 3 10 | do 11 | last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1) 12 | echo "Next ${i}xxx: $((last+1))" 13 | done 14 | -------------------------------------------------------------------------------- /quickrun: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # quickrun runs ShellCheck in an interpreted mode. 3 | # This allows testing changes without recompiling. 4 | 5 | path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1) 6 | if [ -z "$path" ] 7 | then 8 | echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once." 9 | exit 1 10 | fi 11 | path="${path%/*}" 12 | 13 | exec runghc -isrc -i"$path" shellcheck.hs "$@" 14 | -------------------------------------------------------------------------------- /quicktest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # quicktest runs the ShellCheck unit tests in an interpreted mode. 3 | # This allows running tests without compiling, which can be faster. 4 | # 'cabal test' remains the source of truth. 5 | 6 | path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1) 7 | if [ -z "$path" ] 8 | then 9 | echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once." 10 | exit 1 11 | fi 12 | path="${path%/*}" 13 | 14 | 15 | ( 16 | var=$(echo 'main' | ghci -isrc -i"$path" test/shellcheck.hs 2>&1 | tee /dev/stderr) 17 | if [[ $var == *ExitSuccess* ]] 18 | then 19 | exit 0 20 | else 21 | grep -C 3 -e "Fail" -e "Tracing" <<< "$var" 22 | exit 1 23 | fi 24 | ) 2>&1 25 | -------------------------------------------------------------------------------- /setgitversion: -------------------------------------------------------------------------------- 1 | #!/bin/sh -xe 2 | # This script hardcodes the `git describe` version as ShellCheck's version number. 3 | # This is done to allow shellcheck --version to differ from the cabal version when 4 | # building git snapshots. 5 | 6 | file="src/ShellCheck/Data.hs" 7 | test -e "$file" 8 | tmp=$(mktemp) 9 | version=$(git describe) 10 | sed -e "s/=.*VERSIONSTRING.*/= \"$version\" -- VERSIONSTRING, DO NOT SUBMIT/" "$file" > "$tmp" 11 | mv "$tmp" "$file" 12 | -------------------------------------------------------------------------------- /shellcheck.1.md: -------------------------------------------------------------------------------- 1 | % SHELLCHECK(1) Shell script analysis tool 2 | 3 | # NAME 4 | 5 | shellcheck - Shell script analysis tool 6 | 7 | # SYNOPSIS 8 | 9 | **shellcheck** [*OPTIONS*...] *FILES*... 10 | 11 | # DESCRIPTION 12 | 13 | ShellCheck is a static analysis and linting tool for sh/bash scripts. It's 14 | mainly focused on handling typical beginner and intermediate level syntax 15 | errors and pitfalls where the shell just gives a cryptic error message or 16 | strange behavior, but it also reports on a few more advanced issues where 17 | corner cases can cause delayed failures. 18 | 19 | ShellCheck gives shell specific advice. Consider this line: 20 | 21 | (( area = 3.14*r*r )) 22 | 23 | + For scripts starting with `#!/bin/sh` (or when using `-s sh`), ShellCheck 24 | will warn that `(( .. ))` is not POSIX compliant (similar to checkbashisms). 25 | 26 | + For scripts starting with `#!/bin/bash` (or using `-s bash`), ShellCheck 27 | will warn that decimals are not supported. 28 | 29 | + For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will 30 | not warn at all, as `ksh` supports decimals in arithmetic contexts. 31 | 32 | # OPTIONS 33 | 34 | **-a**,\ **--check-sourced** 35 | 36 | : Emit warnings in sourced files. Normally, `shellcheck` will only warn 37 | about issues in the specified files. With this option, any issues in 38 | sourced files will also be reported. 39 | 40 | **-C**[*WHEN*],\ **--color**[=*WHEN*] 41 | 42 | : For TTY output, enable colors *always*, *never* or *auto*. The default 43 | is *auto*. **--color** without an argument is equivalent to 44 | **--color=always**. 45 | 46 | **-i**\ *CODE1*[,*CODE2*...],\ **--include=***CODE1*[,*CODE2*...] 47 | 48 | : Explicitly include only the specified codes in the report. Subsequent **-i** 49 | options are cumulative, but all the codes can be specified at once, 50 | comma-separated as a single argument. Include options override any provided 51 | exclude options. 52 | 53 | **-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...] 54 | 55 | : Explicitly exclude the specified codes from the report. Subsequent **-e** 56 | options are cumulative, but all the codes can be specified at once, 57 | comma-separated as a single argument. 58 | 59 | **--extended-analysis=true/false** 60 | 61 | : Enable/disable Dataflow Analysis to identify more issues (default true). If 62 | ShellCheck uses too much CPU/RAM when checking scripts with several 63 | thousand lines of code, extended analysis can be disabled with this flag 64 | or a directive. This flag overrides directives and rc files. 65 | 66 | **-f** *FORMAT*, **--format=***FORMAT* 67 | 68 | : Specify the output format of shellcheck, which prints its results in the 69 | standard output. Subsequent **-f** options are ignored, see **FORMATS** 70 | below for more information. 71 | 72 | **--list-optional** 73 | 74 | : Output a list of known optional checks. These can be enabled with **-o** 75 | flags or **enable** directives. 76 | 77 | **--norc** 78 | 79 | : Don't try to look for .shellcheckrc configuration files. 80 | 81 | **--rcfile** *RCFILE* 82 | 83 | : Prefer the specified configuration file over searching for one 84 | in the default locations. 85 | 86 | **-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...] 87 | 88 | : Enable optional checks. The special name *all* enables all of them. 89 | Subsequent **-o** options accumulate. This is equivalent to specifying 90 | **enable** directives. 91 | 92 | **-P**\ *SOURCEPATH*,\ **--source-path=***SOURCEPATH* 93 | 94 | : Specify paths to search for sourced files, separated by `:` on Unix and 95 | `;` on Windows. This is equivalent to specifying `search-path` 96 | directives. 97 | 98 | **-s**\ *shell*,\ **--shell=***shell* 99 | 100 | : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*, 101 | and *busybox*. 102 | The default is to deduce the shell from the file's `shell` directive, 103 | shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to 104 | POSIX `sh` (not the system's), and will warn of portability issues. 105 | 106 | **-S**\ *SEVERITY*,\ **--severity=***severity* 107 | 108 | : Specify minimum severity of errors to consider. Valid values in order of 109 | severity are *error*, *warning*, *info* and *style*. 110 | The default is *style*. 111 | 112 | **-V**,\ **--version** 113 | 114 | : Print version information and exit. 115 | 116 | **-W** *NUM*,\ **--wiki-link-count=NUM** 117 | 118 | : For TTY output, show *NUM* wiki links to more information about mentioned 119 | warnings. Set to 0 to disable them entirely. 120 | 121 | **-x**,\ **--external-sources** 122 | 123 | : Follow `source` statements even when the file is not specified as input. 124 | By default, `shellcheck` will only follow files specified on the command 125 | line (plus `/dev/null`). This option allows following any file the script 126 | may `source`. 127 | 128 | This option may also be enabled using `external-sources=true` in 129 | `.shellcheckrc`. This flag takes precedence. 130 | 131 | **FILES...** 132 | 133 | : One or more script files to check, or "-" for standard input. 134 | 135 | 136 | # FORMATS 137 | 138 | **tty** 139 | 140 | : Plain text, human readable output. This is the default. 141 | 142 | **gcc** 143 | 144 | : GCC compatible output. Useful for editors that support compiling and 145 | showing syntax errors. 146 | 147 | For example, in Vim, `:set makeprg=shellcheck\ -f\ gcc\ %` will allow 148 | using `:make` to check the script, and `:cnext` to jump to the next error. 149 | 150 | ::: : 151 | 152 | **checkstyle** 153 | 154 | : Checkstyle compatible XML output. Supported directly or through plugins 155 | by many IDEs and build monitoring systems. 156 | 157 | 158 | 159 | 160 | 166 | ... 167 | 168 | ... 169 | 170 | 171 | **diff** 172 | 173 | : Auto-fixes in unified diff format. Can be piped to `git apply` or `patch -p1` 174 | to automatically apply fixes. 175 | 176 | --- a/test.sh 177 | +++ b/test.sh 178 | @@ -2,6 +2,6 @@ 179 | ## Example of a broken script. 180 | for f in $(ls *.m3u) 181 | do 182 | - grep -qi hq.*mp3 $f \ 183 | + grep -qi hq.*mp3 "$f" \ 184 | && echo -e 'Playlist $f contains a HQ file in mp3 format' 185 | done 186 | 187 | 188 | **json1** 189 | 190 | : Json is a popular serialization format that is more suitable for web 191 | applications. ShellCheck's json is compact and contains only the bare 192 | minimum. Tabs are counted as 1 character. 193 | 194 | { 195 | comments: [ 196 | { 197 | "file": "filename", 198 | "line": lineNumber, 199 | "column": columnNumber, 200 | "level": "severitylevel", 201 | "code": errorCode, 202 | "message": "warning message" 203 | }, 204 | ... 205 | ] 206 | } 207 | 208 | **json** 209 | 210 | : This is a legacy version of the **json1** format. It's a raw array of 211 | comments, and all offsets have a tab stop of 8. 212 | 213 | **quiet** 214 | 215 | : Suppress all normal output. Exit with zero if no issues are found, 216 | otherwise exit with one. Stops processing after the first issue. 217 | 218 | 219 | # DIRECTIVES 220 | 221 | ShellCheck directives can be specified as comments in the shell script. 222 | If they appear before the first command, they are considered file-wide. 223 | Otherwise, they apply to the immediately following command or block: 224 | 225 | # shellcheck key=value key=value 226 | command-or-structure 227 | 228 | For example, to suppress SC2035 about using `./*.jpg`: 229 | 230 | # shellcheck disable=SC2035 231 | echo "Files: " *.jpg 232 | 233 | To tell ShellCheck where to look for an otherwise dynamically determined file: 234 | 235 | # shellcheck source=./lib.sh 236 | source "$(find_install_dir)/lib.sh" 237 | 238 | Here a shell brace group is used to suppress a warning on multiple lines: 239 | 240 | # shellcheck disable=SC2016 241 | { 242 | echo 'Modifying $PATH' 243 | echo 'PATH=foo:$PATH' >> ~/.bashrc 244 | } 245 | 246 | Valid keys are: 247 | 248 | **disable** 249 | : Disables a comma separated list of error codes for the following command. 250 | The command can be a simple command like `echo foo`, or a compound command 251 | like a function definition, subshell block or loop. A range can be 252 | be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx. 253 | All warnings can be disabled with `disable=all`. 254 | 255 | **enable** 256 | : Enable an optional check by name, as listed with **--list-optional**. 257 | Only file-wide `enable` directives are considered. 258 | 259 | **extended-analysis** 260 | : Set to true/false to enable/disable dataflow analysis. Specifying 261 | `# shellcheck extended-analysis=false` in particularly large (2000+ line) 262 | auto-generated scripts will reduce ShellCheck's resource usage at the 263 | expense of certain checks. Extended analysis is enabled by default. 264 | 265 | **external-sources** 266 | : Set to `true` in `.shellcheckrc` to always allow ShellCheck to open 267 | arbitrary files from 'source' statements (the way most tools do). 268 | 269 | This option defaults to `false` only due to ShellCheck's origin as a 270 | remote service for checking untrusted scripts. It can safely be enabled 271 | for normal development. 272 | 273 | **source** 274 | : Overrides the filename included by a `source`/`.` statement. This can be 275 | used to tell shellcheck where to look for a file whose name is determined 276 | at runtime, or to skip a source by telling it to use `/dev/null`. 277 | 278 | **source-path** 279 | : Add a directory to the search path for `source`/`.` statements (by default, 280 | only ShellCheck's working directory is included). Absolute paths will also 281 | be rooted in these paths. The special path `SCRIPTDIR` can be used to 282 | specify the currently checked script's directory, as in 283 | `source-path=SCRIPTDIR` or `source-path=SCRIPTDIR/../libs`. Multiple 284 | paths accumulate, and `-P` takes precedence over them. 285 | 286 | **shell** 287 | : Overrides the shell detected from the shebang. This is useful for 288 | files meant to be included (and thus lacking a shebang), or possibly 289 | as a more targeted alternative to 'disable=SC2039'. 290 | 291 | # RC FILES 292 | 293 | Unless `--norc` is used, ShellCheck will look for a file `.shellcheckrc` or 294 | `shellcheckrc` in the script's directory and each parent directory. If found, 295 | it will read `key=value` pairs from it and treat them as file-wide directives. 296 | 297 | Here is an example `.shellcheckrc`: 298 | 299 | # Look for 'source'd files relative to the checked script, 300 | # and also look for absolute paths in /mnt/chroot 301 | source-path=SCRIPTDIR 302 | source-path=/mnt/chroot 303 | 304 | # Since 0.9.0, values can be quoted with '' or "" to allow spaces 305 | source-path="My Documents/scripts" 306 | 307 | # Allow opening any 'source'd file, even if not specified as input 308 | external-sources=true 309 | 310 | # Turn on warnings for unquoted variables with safe values 311 | enable=quote-safe-variables 312 | 313 | # Turn on warnings for unassigned uppercase variables 314 | enable=check-unassigned-uppercase 315 | 316 | # Allow [ ! -z foo ] instead of suggesting -n 317 | disable=SC2236 318 | 319 | If no `.shellcheckrc` is found in any of the parent directories, ShellCheck 320 | will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME` 321 | (usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on 322 | Windows. Only the first file found will be used. 323 | 324 | Note for Snap users: the Snap sandbox disallows access to hidden files. 325 | Use `shellcheckrc` without the dot instead. 326 | 327 | Note for Docker users: ShellCheck will only be able to look for files that 328 | are mounted in the container, so `~/.shellcheckrc` will not be read. 329 | 330 | 331 | # ENVIRONMENT VARIABLES 332 | 333 | The environment variable `SHELLCHECK_OPTS` can be set with default flags: 334 | 335 | export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016' 336 | 337 | Its value will be split on spaces and prepended to the command line on each 338 | invocation. 339 | 340 | # RETURN VALUES 341 | 342 | ShellCheck uses the following exit codes: 343 | 344 | + 0: All files successfully scanned with no issues. 345 | + 1: All files successfully scanned with some issues. 346 | + 2: Some files could not be processed (e.g. file not found). 347 | + 3: ShellCheck was invoked with bad syntax (e.g. unknown flag). 348 | + 4: ShellCheck was invoked with bad options (e.g. unknown formatter). 349 | 350 | # LOCALE 351 | 352 | This version of ShellCheck is only available in English. All files are 353 | leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid 354 | sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for 355 | locales where encoding is unspecified (such as the `C` locale). 356 | 357 | Windows users seeing `commitBuffer: invalid argument (invalid character)` 358 | should set their terminal to use UTF-8 with `chcp 65001`. 359 | 360 | # KNOWN INCOMPATIBILITIES 361 | 362 | (If nothing in this section makes sense, you are unlikely to be affected by it) 363 | 364 | To avoid confusing and misguided suggestions, ShellCheck requires function 365 | bodies to be either `{ brace groups; }` or `( subshells )`, and function names 366 | containing `[]*=!` are only recognized after a `function` keyword. 367 | 368 | The following unconventional function definitions are identical in Bash, 369 | but ShellCheck only recognizes the latter. 370 | 371 | [x!=y] () [[ $1 ]] 372 | function [x!=y] () { [[ $1 ]]; } 373 | 374 | Shells without the `function` keyword do not allow these characters in function 375 | names to begin with. Function names containing `{}` are not supported at all. 376 | 377 | Further, if ShellCheck sees `[x!=y]` it will assume this is an invalid 378 | comparison. To invoke the above function, quote the command as in `'[x!=y]'`, 379 | or to retain the same globbing behavior, use `command [x!=y]`. 380 | 381 | ShellCheck imposes additional restrictions on the `[` command to help diagnose 382 | common invalid uses. While `[ $x= 1 ]` is defined in POSIX, ShellCheck will 383 | assume it was intended as the much more likely comparison `[ "$x" = 1 ]` and 384 | fail accordingly. For unconventional or dynamic uses of the `[` command, use 385 | `test` or `\[` instead. 386 | 387 | # REPORTING BUGS 388 | 389 | Bugs and issues can be reported on GitHub: 390 | 391 | https://github.com/koalaman/shellcheck/issues 392 | 393 | # AUTHORS 394 | 395 | ShellCheck is developed and maintained by Vidar Holen, with assistance from a 396 | long list of wonderful contributors. 397 | 398 | # COPYRIGHT 399 | 400 | Copyright 2012-2024, Vidar Holen and contributors. 401 | Licensed under the GNU General Public License version 3 or later, 402 | see https://gnu.org/licenses/gpl.html 403 | 404 | # SEE ALSO 405 | 406 | sh(1) bash(1) dash(1) ksh(1) 407 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: shellcheck 2 | summary: A shell script static analysis tool 3 | description: | 4 | ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh 5 | shell scripts. 6 | 7 | The goals of ShellCheck are 8 | 9 | - To point out and clarify typical beginner's syntax issues that cause a 10 | shell to give cryptic error messages. 11 | 12 | - To point out and clarify typical intermediate level semantic problems that 13 | cause a shell to behave strangely and counter-intuitively. 14 | 15 | - To point out subtle caveats, corner cases and pitfalls that may cause an 16 | advanced user's otherwise working script to fail under future 17 | circumstances. 18 | 19 | By default ShellCheck can only check non-hidden files under /home, to make 20 | ShellCheck be able to check files under /media and /run/media you must 21 | connect it to the `removable-media` interface manually: 22 | 23 | # snap connect shellcheck:removable-media 24 | 25 | version: git 26 | base: core20 27 | grade: stable 28 | confinement: strict 29 | 30 | apps: 31 | shellcheck: 32 | command: usr/bin/shellcheck 33 | plugs: [home, removable-media] 34 | environment: 35 | LANG: C.UTF-8 36 | 37 | parts: 38 | shellcheck: 39 | plugin: dump 40 | source: . 41 | build-packages: 42 | - cabal-install 43 | stage-packages: 44 | - libatomic1 45 | override-build: | 46 | # Give ourselves enough memory to build 47 | dd if=/dev/zero of=/tmp/swap bs=1M count=2000 48 | mkswap /tmp/swap 49 | swapon /tmp/swap 50 | 51 | cabal sandbox init 52 | cabal update 53 | cabal install -j 54 | 55 | install -d $SNAPCRAFT_PART_INSTALL/usr/bin 56 | install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin 57 | -------------------------------------------------------------------------------- /src/ShellCheck/AST.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2019 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | {-# LANGUAGE DeriveGeneric, DeriveAnyClass, DeriveTraversable, PatternSynonyms #-} 21 | module ShellCheck.AST where 22 | 23 | import GHC.Generics (Generic) 24 | import Control.Monad.Identity 25 | import Control.DeepSeq 26 | import Text.Parsec 27 | import qualified ShellCheck.Regex as Re 28 | import Prelude hiding (id) 29 | 30 | newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData) 31 | 32 | data Quoted = Quoted | Unquoted deriving (Show, Eq) 33 | data Dashed = Dashed | Undashed deriving (Show, Eq) 34 | data AssignmentMode = Assign | Append deriving (Show, Eq) 35 | newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq) 36 | newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq) 37 | data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq) 38 | 39 | newtype Root = Root Token 40 | data Token = OuterToken Id (InnerToken Token) deriving (Show) 41 | 42 | data InnerToken t = 43 | Inner_TA_Binary String t t 44 | | Inner_TA_Assignment String t t 45 | | Inner_TA_Variable String [t] 46 | | Inner_TA_Expansion [t] 47 | | Inner_TA_Sequence [t] 48 | | Inner_TA_Parenthesis t 49 | | Inner_TA_Trinary t t t 50 | | Inner_TA_Unary String t 51 | | Inner_TC_And ConditionType String t t 52 | | Inner_TC_Binary ConditionType String t t 53 | | Inner_TC_Group ConditionType t 54 | | Inner_TC_Nullary ConditionType t 55 | | Inner_TC_Or ConditionType String t t 56 | | Inner_TC_Unary ConditionType String t 57 | | Inner_TC_Empty ConditionType 58 | | Inner_T_AND_IF 59 | | Inner_T_AndIf t t 60 | | Inner_T_Arithmetic t 61 | | Inner_T_Array [t] 62 | | Inner_T_IndexedElement [t] t 63 | -- Store the index as string, and parse as arithmetic or string later 64 | | Inner_T_UnparsedIndex SourcePos String 65 | | Inner_T_Assignment AssignmentMode String [t] t 66 | | Inner_T_Backgrounded t 67 | | Inner_T_Backticked [t] 68 | | Inner_T_Bang 69 | | Inner_T_Banged t 70 | | Inner_T_BraceExpansion [t] 71 | | Inner_T_BraceGroup [t] 72 | | Inner_T_CLOBBER 73 | | Inner_T_Case 74 | | Inner_T_CaseExpression t [(CaseType, [t], [t])] 75 | | Inner_T_Condition ConditionType t 76 | | Inner_T_DGREAT 77 | | Inner_T_DLESS 78 | | Inner_T_DLESSDASH 79 | | Inner_T_DSEMI 80 | | Inner_T_Do 81 | | Inner_T_DollarArithmetic t 82 | | Inner_T_DollarBraced Bool t 83 | | Inner_T_DollarBracket t 84 | | Inner_T_DollarDoubleQuoted [t] 85 | | Inner_T_DollarExpansion [t] 86 | | Inner_T_DollarSingleQuoted String 87 | | Inner_T_DollarBraceCommandExpansion [t] 88 | | Inner_T_Done 89 | | Inner_T_DoubleQuoted [t] 90 | | Inner_T_EOF 91 | | Inner_T_Elif 92 | | Inner_T_Else 93 | | Inner_T_Esac 94 | | Inner_T_Extglob String [t] 95 | | Inner_T_FdRedirect String t 96 | | Inner_T_Fi 97 | | Inner_T_For 98 | | Inner_T_ForArithmetic t t t [t] 99 | | Inner_T_ForIn String [t] [t] 100 | | Inner_T_Function FunctionKeyword FunctionParentheses String t 101 | | Inner_T_GREATAND 102 | | Inner_T_Glob String 103 | | Inner_T_Greater 104 | | Inner_T_HereDoc Dashed Quoted String [t] 105 | | Inner_T_HereString t 106 | | Inner_T_If 107 | | Inner_T_IfExpression [([t],[t])] [t] 108 | | Inner_T_In 109 | | Inner_T_IoFile t t 110 | | Inner_T_IoDuplicate t String 111 | | Inner_T_LESSAND 112 | | Inner_T_LESSGREAT 113 | | Inner_T_Lbrace 114 | | Inner_T_Less 115 | | Inner_T_Literal String 116 | | Inner_T_Lparen 117 | | Inner_T_NEWLINE 118 | | Inner_T_NormalWord [t] 119 | | Inner_T_OR_IF 120 | | Inner_T_OrIf t t 121 | | Inner_T_ParamSubSpecialChar String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz} 122 | | Inner_T_Pipeline [t] [t] -- [Pipe separators] [Commands] 123 | | Inner_T_ProcSub String [t] 124 | | Inner_T_Rbrace 125 | | Inner_T_Redirecting [t] t 126 | | Inner_T_Rparen 127 | | Inner_T_Script t [t] -- Shebang T_Literal, followed by script. 128 | | Inner_T_Select 129 | | Inner_T_SelectIn String [t] [t] 130 | | Inner_T_Semi 131 | | Inner_T_SimpleCommand [t] [t] 132 | | Inner_T_SingleQuoted String 133 | | Inner_T_Subshell [t] 134 | | Inner_T_Then 135 | | Inner_T_Until 136 | | Inner_T_UntilExpression [t] [t] 137 | | Inner_T_While 138 | | Inner_T_WhileExpression [t] [t] 139 | | Inner_T_Annotation [Annotation] t 140 | | Inner_T_Pipe String 141 | | Inner_T_CoProc (Maybe Token) t 142 | | Inner_T_CoProcBody t 143 | | Inner_T_Include t 144 | | Inner_T_SourceCommand t t 145 | | Inner_T_BatsTest String t 146 | deriving (Show, Eq, Functor, Foldable, Traversable) 147 | 148 | data Annotation = 149 | DisableComment Integer Integer -- [from, to) 150 | | EnableComment String 151 | | SourceOverride String 152 | | ShellOverride String 153 | | SourcePath String 154 | | ExternalSources Bool 155 | | ExtendedAnalysis Bool 156 | deriving (Show, Eq) 157 | data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) 158 | 159 | pattern T_AND_IF id = OuterToken id Inner_T_AND_IF 160 | pattern T_Bang id = OuterToken id Inner_T_Bang 161 | pattern T_Case id = OuterToken id Inner_T_Case 162 | pattern TC_Empty id typ = OuterToken id (Inner_TC_Empty typ) 163 | pattern T_CLOBBER id = OuterToken id Inner_T_CLOBBER 164 | pattern T_DGREAT id = OuterToken id Inner_T_DGREAT 165 | pattern T_DLESS id = OuterToken id Inner_T_DLESS 166 | pattern T_DLESSDASH id = OuterToken id Inner_T_DLESSDASH 167 | pattern T_Do id = OuterToken id Inner_T_Do 168 | pattern T_DollarSingleQuoted id str = OuterToken id (Inner_T_DollarSingleQuoted str) 169 | pattern T_Done id = OuterToken id Inner_T_Done 170 | pattern T_DSEMI id = OuterToken id Inner_T_DSEMI 171 | pattern T_Elif id = OuterToken id Inner_T_Elif 172 | pattern T_Else id = OuterToken id Inner_T_Else 173 | pattern T_EOF id = OuterToken id Inner_T_EOF 174 | pattern T_Esac id = OuterToken id Inner_T_Esac 175 | pattern T_Fi id = OuterToken id Inner_T_Fi 176 | pattern T_For id = OuterToken id Inner_T_For 177 | pattern T_Glob id str = OuterToken id (Inner_T_Glob str) 178 | pattern T_GREATAND id = OuterToken id Inner_T_GREATAND 179 | pattern T_Greater id = OuterToken id Inner_T_Greater 180 | pattern T_If id = OuterToken id Inner_T_If 181 | pattern T_In id = OuterToken id Inner_T_In 182 | pattern T_Lbrace id = OuterToken id Inner_T_Lbrace 183 | pattern T_Less id = OuterToken id Inner_T_Less 184 | pattern T_LESSAND id = OuterToken id Inner_T_LESSAND 185 | pattern T_LESSGREAT id = OuterToken id Inner_T_LESSGREAT 186 | pattern T_Literal id str = OuterToken id (Inner_T_Literal str) 187 | pattern T_Lparen id = OuterToken id Inner_T_Lparen 188 | pattern T_NEWLINE id = OuterToken id Inner_T_NEWLINE 189 | pattern T_OR_IF id = OuterToken id Inner_T_OR_IF 190 | pattern T_ParamSubSpecialChar id str = OuterToken id (Inner_T_ParamSubSpecialChar str) 191 | pattern T_Pipe id str = OuterToken id (Inner_T_Pipe str) 192 | pattern T_Rbrace id = OuterToken id Inner_T_Rbrace 193 | pattern T_Rparen id = OuterToken id Inner_T_Rparen 194 | pattern T_Select id = OuterToken id Inner_T_Select 195 | pattern T_Semi id = OuterToken id Inner_T_Semi 196 | pattern T_SingleQuoted id str = OuterToken id (Inner_T_SingleQuoted str) 197 | pattern T_Then id = OuterToken id Inner_T_Then 198 | pattern T_UnparsedIndex id pos str = OuterToken id (Inner_T_UnparsedIndex pos str) 199 | pattern T_Until id = OuterToken id Inner_T_Until 200 | pattern T_While id = OuterToken id Inner_T_While 201 | pattern TA_Assignment id op t1 t2 = OuterToken id (Inner_TA_Assignment op t1 t2) 202 | pattern TA_Binary id op t1 t2 = OuterToken id (Inner_TA_Binary op t1 t2) 203 | pattern TA_Expansion id t = OuterToken id (Inner_TA_Expansion t) 204 | pattern T_AndIf id t u = OuterToken id (Inner_T_AndIf t u) 205 | pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t) 206 | pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c) 207 | pattern T_Array id t = OuterToken id (Inner_T_Array t) 208 | pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l) 209 | pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t) 210 | pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value) 211 | pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3) 212 | pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1) 213 | pattern TA_Variable id str t = OuterToken id (Inner_TA_Variable str t) 214 | pattern T_Backgrounded id l = OuterToken id (Inner_T_Backgrounded l) 215 | pattern T_Backticked id list = OuterToken id (Inner_T_Backticked list) 216 | pattern T_Banged id l = OuterToken id (Inner_T_Banged l) 217 | pattern T_BatsTest id name t = OuterToken id (Inner_T_BatsTest name t) 218 | pattern T_BraceExpansion id list = OuterToken id (Inner_T_BraceExpansion list) 219 | pattern T_BraceGroup id l = OuterToken id (Inner_T_BraceGroup l) 220 | pattern TC_And id typ str t1 t2 = OuterToken id (Inner_TC_And typ str t1 t2) 221 | pattern T_CaseExpression id word cases = OuterToken id (Inner_T_CaseExpression word cases) 222 | pattern TC_Binary id typ op lhs rhs = OuterToken id (Inner_TC_Binary typ op lhs rhs) 223 | pattern TC_Group id typ token = OuterToken id (Inner_TC_Group typ token) 224 | pattern TC_Nullary id typ token = OuterToken id (Inner_TC_Nullary typ token) 225 | pattern T_Condition id typ token = OuterToken id (Inner_T_Condition typ token) 226 | pattern T_CoProcBody id t = OuterToken id (Inner_T_CoProcBody t) 227 | pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body) 228 | pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2) 229 | pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token) 230 | pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c) 231 | pattern T_DollarBraceCommandExpansion id list = OuterToken id (Inner_T_DollarBraceCommandExpansion list) 232 | pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op) 233 | pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c) 234 | pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list) 235 | pattern T_DollarExpansion id list = OuterToken id (Inner_T_DollarExpansion list) 236 | pattern T_DoubleQuoted id list = OuterToken id (Inner_T_DoubleQuoted list) 237 | pattern T_Extglob id str l = OuterToken id (Inner_T_Extglob str l) 238 | pattern T_FdRedirect id v t = OuterToken id (Inner_T_FdRedirect v t) 239 | pattern T_ForArithmetic id a b c group = OuterToken id (Inner_T_ForArithmetic a b c group) 240 | pattern T_ForIn id v w l = OuterToken id (Inner_T_ForIn v w l) 241 | pattern T_Function id a b name body = OuterToken id (Inner_T_Function a b name body) 242 | pattern T_HereDoc id d q str l = OuterToken id (Inner_T_HereDoc d q str l) 243 | pattern T_HereString id word = OuterToken id (Inner_T_HereString word) 244 | pattern T_IfExpression id conditions elses = OuterToken id (Inner_T_IfExpression conditions elses) 245 | pattern T_Include id script = OuterToken id (Inner_T_Include script) 246 | pattern T_IndexedElement id indices t = OuterToken id (Inner_T_IndexedElement indices t) 247 | pattern T_IoDuplicate id op num = OuterToken id (Inner_T_IoDuplicate op num) 248 | pattern T_IoFile id op file = OuterToken id (Inner_T_IoFile op file) 249 | pattern T_NormalWord id list = OuterToken id (Inner_T_NormalWord list) 250 | pattern T_OrIf id t u = OuterToken id (Inner_T_OrIf t u) 251 | pattern T_Pipeline id l1 l2 = OuterToken id (Inner_T_Pipeline l1 l2) 252 | pattern T_ProcSub id typ l = OuterToken id (Inner_T_ProcSub typ l) 253 | pattern T_Redirecting id redirs cmd = OuterToken id (Inner_T_Redirecting redirs cmd) 254 | pattern T_Script id shebang list = OuterToken id (Inner_T_Script shebang list) 255 | pattern T_SelectIn id v w l = OuterToken id (Inner_T_SelectIn v w l) 256 | pattern T_SimpleCommand id vars cmds = OuterToken id (Inner_T_SimpleCommand vars cmds) 257 | pattern T_SourceCommand id includer t_include = OuterToken id (Inner_T_SourceCommand includer t_include) 258 | pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l) 259 | pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l) 260 | pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) 261 | 262 | {-# 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 #-} 263 | 264 | instance Eq Token where 265 | OuterToken _ a == OuterToken _ b = a == b 266 | 267 | analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token 268 | analyze f g i = 269 | round 270 | where 271 | round t@(OuterToken id it) = do 272 | f t 273 | newIt <- traverse round it 274 | g t 275 | i (OuterToken id newIt) 276 | 277 | getId :: Token -> Id 278 | getId (OuterToken id _) = id 279 | 280 | blank :: Monad m => Token -> m () 281 | blank = const $ return () 282 | doAnalysis :: Monad m => (Token -> m ()) -> Token -> m Token 283 | doAnalysis f = analyze f blank return 284 | doStackAnalysis :: Monad m => (Token -> m ()) -> (Token -> m ()) -> Token -> m Token 285 | doStackAnalysis startToken endToken = analyze startToken endToken return 286 | doTransform :: (Token -> Token) -> Token -> Token 287 | doTransform i = runIdentity . analyze blank blank (return . i) 288 | 289 | -------------------------------------------------------------------------------- /src/ShellCheck/Analyzer.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2022 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | module ShellCheck.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where 21 | 22 | import ShellCheck.Analytics 23 | import ShellCheck.AnalyzerLib 24 | import ShellCheck.Interface 25 | import Data.List 26 | import Data.Monoid 27 | import qualified ShellCheck.Checks.Commands 28 | import qualified ShellCheck.Checks.ControlFlow 29 | import qualified ShellCheck.Checks.Custom 30 | import qualified ShellCheck.Checks.ShellSupport 31 | 32 | 33 | -- TODO: Clean up the cruft this is layered on 34 | analyzeScript :: AnalysisSpec -> AnalysisResult 35 | analyzeScript spec = newAnalysisResult { 36 | arComments = 37 | filterByAnnotation spec params . nub $ 38 | runChecker params (checkers spec params) 39 | } 40 | where 41 | params = makeParameters spec 42 | 43 | checkers spec params = mconcat $ map ($ params) [ 44 | ShellCheck.Analytics.checker spec, 45 | ShellCheck.Checks.Commands.checker spec, 46 | ShellCheck.Checks.ControlFlow.checker spec, 47 | ShellCheck.Checks.Custom.checker, 48 | ShellCheck.Checks.ShellSupport.checker 49 | ] 50 | 51 | optionalChecks = mconcat $ [ 52 | ShellCheck.Analytics.optionalChecks, 53 | ShellCheck.Checks.Commands.optionalChecks, 54 | ShellCheck.Checks.ControlFlow.optionalChecks 55 | ] 56 | -------------------------------------------------------------------------------- /src/ShellCheck/Checker.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2022 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | {-# LANGUAGE TemplateHaskell #-} 21 | module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where 22 | 23 | import ShellCheck.Analyzer 24 | import ShellCheck.ASTLib 25 | import ShellCheck.Interface 26 | import ShellCheck.Parser 27 | 28 | import Debug.Trace -- DO NOT SUBMIT 29 | import Data.Either 30 | import Data.Functor 31 | import Data.List 32 | import Data.Maybe 33 | import Data.Ord 34 | import Control.Monad.Identity 35 | import qualified Data.Map as Map 36 | import qualified System.IO 37 | import Prelude hiding (readFile) 38 | import Control.Monad 39 | 40 | import Test.QuickCheck.All 41 | 42 | tokenToPosition startMap t = fromMaybe fail $ do 43 | span <- Map.lookup (tcId t) startMap 44 | return $ newPositionedComment { 45 | pcStartPos = fst span, 46 | pcEndPos = snd span, 47 | pcComment = tcComment t, 48 | pcFix = tcFix t 49 | } 50 | where 51 | fail = error "Internal shellcheck error: id doesn't exist. Please report!" 52 | 53 | shellFromFilename filename = listToMaybe candidates 54 | where 55 | shellExtensions = [(".ksh", Ksh) 56 | ,(".bash", Bash) 57 | ,(".bats", Bash) 58 | ,(".dash", Dash)] 59 | -- The `.sh` is too generic to determine the shell: 60 | -- We fallback to Bash in this case and emit SC2148 if there is no shebang 61 | candidates = 62 | [sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename] 63 | 64 | checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult 65 | checkScript sys spec = do 66 | results <- checkScript (csScript spec) 67 | return emptyCheckResult { 68 | crFilename = csFilename spec, 69 | crComments = results 70 | } 71 | where 72 | checkScript contents = do 73 | result <- parseScript sys newParseSpec { 74 | psFilename = csFilename spec, 75 | psScript = contents, 76 | psCheckSourced = csCheckSourced spec, 77 | psIgnoreRC = csIgnoreRC spec, 78 | psShellTypeOverride = csShellTypeOverride spec 79 | } 80 | let parseMessages = prComments result 81 | let tokenPositions = prTokenPositions result 82 | let analysisSpec root = 83 | as { 84 | asScript = root, 85 | asShellType = csShellTypeOverride spec, 86 | asFallbackShell = shellFromFilename $ csFilename spec, 87 | asCheckSourced = csCheckSourced spec, 88 | asExecutionMode = Executed, 89 | asTokenPositions = tokenPositions, 90 | asExtendedAnalysis = csExtendedAnalysis spec, 91 | asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec 92 | } where as = newAnalysisSpec root 93 | let analysisMessages = 94 | maybe [] 95 | (arComments . analyzeScript . analysisSpec) 96 | $ prRoot result 97 | let translator = tokenToPosition tokenPositions 98 | return . nub . sortMessages . filter shouldInclude $ 99 | (parseMessages ++ map translator analysisMessages) 100 | 101 | shouldInclude pc = 102 | severity <= csMinSeverity spec && 103 | case csIncludedWarnings spec of 104 | Nothing -> code `notElem` csExcludedWarnings spec 105 | Just includedWarnings -> code `elem` includedWarnings 106 | where 107 | code = cCode (pcComment pc) 108 | severity = cSeverity (pcComment pc) 109 | 110 | sortMessages = sortOn order 111 | order pc = 112 | let pos = pcStartPos pc 113 | comment = pcComment pc in 114 | (posFile pos, 115 | posLine pos, 116 | posColumn pos, 117 | cSeverity comment, 118 | cCode comment, 119 | cMessage comment) 120 | getPosition = pcStartPos 121 | 122 | 123 | getErrors sys spec = 124 | sort . map getCode . crComments $ 125 | runIdentity (checkScript sys spec) 126 | where 127 | getCode = cCode . pcComment 128 | 129 | check = checkWithIncludes [] 130 | 131 | checkWithSpec includes = 132 | getErrors (mockedSystemInterface includes) 133 | 134 | checkWithIncludes includes src = 135 | checkWithSpec includes emptyCheckSpec { 136 | csScript = src, 137 | csExcludedWarnings = [2148] 138 | } 139 | 140 | checkRecursive includes src = 141 | checkWithSpec includes emptyCheckSpec { 142 | csScript = src, 143 | csExcludedWarnings = [2148], 144 | csCheckSourced = True 145 | } 146 | 147 | checkOptionIncludes includes src = 148 | checkWithSpec [] emptyCheckSpec { 149 | csScript = src, 150 | csIncludedWarnings = includes, 151 | csCheckSourced = True 152 | } 153 | 154 | checkWithRc rc = getErrors 155 | (mockRcFile rc $ mockedSystemInterface []) 156 | 157 | checkWithIncludesAndSourcePath includes mapper = getErrors 158 | (mockedSystemInterface includes) { 159 | siFindSource = mapper 160 | } 161 | 162 | checkWithRcIncludesAndSourcePath rc includes mapper = getErrors 163 | (mockRcFile rc $ mockedSystemInterface includes) { 164 | siFindSource = mapper 165 | } 166 | 167 | prop_findsParseIssue = check "echo \"$12\"" == [1037] 168 | 169 | prop_commentDisablesParseIssue1 = 170 | null $ check "#shellcheck disable=SC1037\necho \"$12\"" 171 | prop_commentDisablesParseIssue2 = 172 | null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\"" 173 | 174 | prop_findsAnalysisIssue = 175 | check "echo $1" == [2086] 176 | prop_commentDisablesAnalysisIssue1 = 177 | null $ check "#shellcheck disable=SC2086\necho $1" 178 | prop_commentDisablesAnalysisIssue2 = 179 | null $ check "#shellcheck disable=SC2086\n#lol\necho $1" 180 | 181 | prop_optionDisablesIssue1 = 182 | null $ getErrors 183 | (mockedSystemInterface []) 184 | emptyCheckSpec { 185 | csScript = "echo $1", 186 | csExcludedWarnings = [2148, 2086] 187 | } 188 | 189 | prop_optionDisablesIssue2 = 190 | null $ getErrors 191 | (mockedSystemInterface []) 192 | emptyCheckSpec { 193 | csScript = "echo \"$10\"", 194 | csExcludedWarnings = [2148, 1037] 195 | } 196 | 197 | prop_wontParseBadShell = 198 | [1071] == check "#!/usr/bin/python\ntrue $1\n" 199 | 200 | prop_optionDisablesBadShebang = 201 | null $ getErrors 202 | (mockedSystemInterface []) 203 | emptyCheckSpec { 204 | csScript = "#!/usr/bin/python\ntrue\n", 205 | csShellTypeOverride = Just Sh 206 | } 207 | 208 | prop_annotationDisablesBadShebang = 209 | null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n" 210 | 211 | 212 | prop_canParseDevNull = 213 | null $ check "source /dev/null" 214 | 215 | prop_failsWhenNotSourcing = 216 | [1091, 2154] == check "source lol; echo \"$bar\"" 217 | 218 | prop_worksWhenSourcing = 219 | null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\"" 220 | 221 | prop_worksWhenSourcingWithDashDash = 222 | null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\"" 223 | 224 | prop_worksWhenDotting = 225 | null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\"" 226 | 227 | -- FIXME: This should really be giving [1093], "recursively sourced" 228 | prop_noInfiniteSourcing = 229 | null $ checkWithIncludes [("lib", "source lib")] "source lib" 230 | 231 | prop_canSourceBadSyntax = 232 | [1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1" 233 | 234 | prop_cantSourceDynamic = 235 | [1090] == checkWithIncludes [("lib", "")] ". \"$1\"" 236 | 237 | prop_cantSourceDynamic2 = 238 | [1090] == checkWithIncludes [("lib", "")] "source ~/foo" 239 | 240 | prop_canStripPrefixAndSource = 241 | null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\"" 242 | 243 | prop_canStripPrefixAndSource2 = 244 | null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\"" 245 | 246 | prop_canSourceDynamicWhenRedirected = 247 | null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" 248 | 249 | prop_canRedirectWithSpaces = 250 | null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\"" 251 | 252 | prop_recursiveAnalysis = 253 | [2086] == checkRecursive [("lib", "echo $1")] "source lib" 254 | 255 | prop_recursiveParsing = 256 | [1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib" 257 | 258 | prop_nonRecursiveAnalysis = 259 | null $ checkWithIncludes [("lib", "echo $1")] "source lib" 260 | 261 | prop_nonRecursiveParsing = 262 | null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib" 263 | 264 | prop_sourceDirectiveDoesntFollowFile = 265 | null $ checkWithIncludes 266 | [("foo", "source bar"), ("bar", "baz=3")] 267 | "#shellcheck source=foo\n. \"$1\"; echo \"$baz\"" 268 | 269 | prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1" 270 | prop_filewideAnnotation1 = null $ 271 | check "#!/bin/sh\n# shellcheck disable=2086\necho $1" 272 | prop_filewideAnnotation2 = null $ 273 | check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1" 274 | prop_filewideAnnotation3 = null $ 275 | check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1" 276 | prop_filewideAnnotation4 = null $ 277 | check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" 278 | prop_filewideAnnotation5 = null $ 279 | check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1" 280 | prop_filewideAnnotation6 = null $ 281 | check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1" 282 | prop_filewideAnnotation7 = null $ 283 | check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" 284 | 285 | prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1" 286 | prop_filewideAnnotation8 = null $ 287 | check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1" 288 | 289 | prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source' 290 | 3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh" 291 | 292 | prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n" 293 | 294 | prop_deducesTypeFromExtension = null result 295 | where 296 | result = checkWithSpec [] emptyCheckSpec { 297 | csFilename = "file.ksh", 298 | csScript = "(( 3.14 ))" 299 | } 300 | 301 | prop_deducesTypeFromExtension2 = result == [2079] 302 | where 303 | result = checkWithSpec [] emptyCheckSpec { 304 | csFilename = "file.bash", 305 | csScript = "(( 3.14 ))" 306 | } 307 | 308 | prop_canDisableShebangWarning = null $ result 309 | where 310 | result = checkWithSpec [] emptyCheckSpec { 311 | csFilename = "file.sh", 312 | csScript = "#shellcheck disable=SC2148\nfoo" 313 | } 314 | 315 | prop_canDisableAllWarnings = result == [2086] 316 | where 317 | result = checkWithSpec [] emptyCheckSpec { 318 | csFilename = "file.sh", 319 | csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`" 320 | } 321 | 322 | prop_canDisableParseErrors = null $ result 323 | where 324 | result = checkWithSpec [] emptyCheckSpec { 325 | csFilename = "file.sh", 326 | csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()" 327 | } 328 | 329 | prop_shExtensionDoesntMatter = result == [2148] 330 | where 331 | result = checkWithSpec [] emptyCheckSpec { 332 | csFilename = "file.sh", 333 | csScript = "echo 'hello world'" 334 | } 335 | 336 | prop_sourcedFileUsesOriginalShellExtension = result == [2079] 337 | where 338 | result = checkWithSpec [("file.ksh", "(( 3.14 ))")] emptyCheckSpec { 339 | csFilename = "file.bash", 340 | csScript = "source file.ksh", 341 | csCheckSourced = True 342 | } 343 | 344 | prop_canEnableOptionalsWithSpec = result == [2244] 345 | where 346 | result = checkWithSpec [] emptyCheckSpec { 347 | csFilename = "file.sh", 348 | csScript = "#!/bin/sh\n[ \"$1\" ]", 349 | csOptionalChecks = ["avoid-nullary-conditions"] 350 | } 351 | 352 | prop_optionIncludes1 = 353 | -- expect 2086, but not included, so nothing reported 354 | null $ checkOptionIncludes (Just [2080]) "#!/bin/sh\n var='a b'\n echo $var" 355 | 356 | prop_optionIncludes2 = 357 | -- expect 2086, included, so it is reported 358 | [2086] == checkOptionIncludes (Just [2086]) "#!/bin/sh\n var='a b'\n echo $var" 359 | 360 | prop_optionIncludes3 = 361 | -- expect 2086, no inclusions provided, so it is reported 362 | [2086] == checkOptionIncludes Nothing "#!/bin/sh\n var='a b'\n echo $var" 363 | 364 | prop_optionIncludes4 = 365 | -- expect 2086 & 2154, only 2154 included, so only that's reported 366 | [2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar" 367 | 368 | 369 | prop_readsRcFile = null result 370 | where 371 | result = checkWithRc "disable=2086" emptyCheckSpec { 372 | csScript = "#!/bin/sh\necho $1", 373 | csIgnoreRC = False 374 | } 375 | 376 | prop_canUseNoRC = result == [2086] 377 | where 378 | result = checkWithRc "disable=2086" emptyCheckSpec { 379 | csScript = "#!/bin/sh\necho $1", 380 | csIgnoreRC = True 381 | } 382 | 383 | prop_NoRCWontLookAtFile = result == [2086] 384 | where 385 | result = checkWithRc (error "Fail") emptyCheckSpec { 386 | csScript = "#!/bin/sh\necho $1", 387 | csIgnoreRC = True 388 | } 389 | 390 | prop_brokenRcGetsWarning = result == [1134, 2086] 391 | where 392 | result = checkWithRc "rofl" emptyCheckSpec { 393 | csScript = "#!/bin/sh\necho $1", 394 | csIgnoreRC = False 395 | } 396 | 397 | prop_canEnableOptionalsWithRc = result == [2244] 398 | where 399 | result = checkWithRc "enable=avoid-nullary-conditions" emptyCheckSpec { 400 | csScript = "#!/bin/sh\n[ \"$1\" ]" 401 | } 402 | 403 | prop_sourcePathRedirectsName = result == [2086] 404 | where 405 | f "dir/myscript" _ _ "lib" = return "foo/lib" 406 | result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { 407 | csScript = "#!/bin/bash\nsource lib", 408 | csFilename = "dir/myscript", 409 | csCheckSourced = True 410 | } 411 | 412 | prop_sourcePathAddsAnnotation = result == [2086] 413 | where 414 | f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib" 415 | result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { 416 | csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib", 417 | csFilename = "dir/myscript", 418 | csCheckSourced = True 419 | } 420 | 421 | prop_sourcePathWorksWithSpaces = result == [2086] 422 | where 423 | f "dir/myscript" _ ["my path"] "lib" = return "foo/lib" 424 | result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { 425 | csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib", 426 | csFilename = "dir/myscript", 427 | csCheckSourced = True 428 | } 429 | 430 | prop_sourcePathRedirectsDirective = result == [2086] 431 | where 432 | f "dir/myscript" _ _ "lib" = return "foo/lib" 433 | f _ _ _ _ = return "/dev/null" 434 | result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { 435 | csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens", 436 | csFilename = "dir/myscript", 437 | csCheckSourced = True 438 | } 439 | 440 | prop_rcCanAllowExternalSources = result == [2086] 441 | where 442 | f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib" 443 | f a b c d = error $ show ("Unexpected", a, b, c, d) 444 | result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec { 445 | csScript = "#!/bin/bash\nsource mylib", 446 | csFilename = "dir/myscript", 447 | csCheckSourced = True 448 | } 449 | 450 | prop_rcCanDenyExternalSources = result == [2086] 451 | where 452 | f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib" 453 | f a b c d = error $ show ("Unexpected", a, b, c, d) 454 | result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec { 455 | csScript = "#!/bin/bash\nsource mylib", 456 | csFilename = "dir/myscript", 457 | csCheckSourced = True 458 | } 459 | 460 | prop_rcCanLeaveExternalSourcesUnspecified = result == [2086] 461 | where 462 | f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib" 463 | f a b c d = error $ show ("Unexpected", a, b, c, d) 464 | result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec { 465 | csScript = "#!/bin/bash\nsource mylib", 466 | csFilename = "dir/myscript", 467 | csCheckSourced = True 468 | } 469 | 470 | prop_fileCanDisableExternalSources = result == [2006, 2086] 471 | where 472 | f "dir/myscript" (Just True) _ "withExternal" = return "withExternal" 473 | f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal" 474 | f a b c d = error $ show ("Unexpected", a, b, c, d) 475 | result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec { 476 | csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal", 477 | csFilename = "dir/myscript", 478 | csCheckSourced = True 479 | } 480 | 481 | prop_fileCannotEnableExternalSources = result == [1144] 482 | where 483 | f "dir/myscript" Nothing _ "foo" = return "foo" 484 | f a b c d = error $ show ("Unexpected", a, b, c, d) 485 | result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec { 486 | csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo", 487 | csFilename = "dir/myscript", 488 | csCheckSourced = True 489 | } 490 | 491 | prop_fileCannotEnableExternalSources2 = result == [1144] 492 | where 493 | f "dir/myscript" (Just False) _ "foo" = return "foo" 494 | f a b c d = error $ show ("Unexpected", a, b, c, d) 495 | result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec { 496 | csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo", 497 | csFilename = "dir/myscript", 498 | csCheckSourced = True 499 | } 500 | 501 | prop_rcCanSuppressEarlyProblems1 = null result 502 | where 503 | result = checkWithRc "disable=1071" emptyCheckSpec { 504 | csScript = "#!/bin/zsh\necho $1" 505 | } 506 | 507 | prop_rcCanSuppressEarlyProblems2 = null result 508 | where 509 | result = checkWithRc "disable=1104" emptyCheckSpec { 510 | csScript = "!/bin/bash\necho 'hello world'" 511 | } 512 | 513 | prop_sourceWithHereDocWorks = null result 514 | where 515 | result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof" 516 | 517 | prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result 518 | where 519 | result = check "cat << eof" 520 | 521 | prop_hereDocsWillHaveParsedIndices = null result 522 | where 523 | result = check "#!/bin/bash\nmy_array=(a b)\ncat <> ./test\n $(( 1 + my_array[1] ))\nEOF" 524 | 525 | prop_rcCanSuppressDfa = null result 526 | where 527 | result = checkWithRc "extended-analysis=false" emptyCheckSpec { 528 | csScript = "#!/bin/sh\nexit; foo;" 529 | } 530 | 531 | prop_fileCanSuppressDfa = null $ traceShowId result 532 | where 533 | result = checkWithRc "" emptyCheckSpec { 534 | csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;" 535 | } 536 | 537 | prop_fileWinsWhenSuppressingDfa1 = null result 538 | where 539 | result = checkWithRc "extended-analysis=true" emptyCheckSpec { 540 | csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;" 541 | } 542 | 543 | prop_fileWinsWhenSuppressingDfa2 = result == [2317] 544 | where 545 | result = checkWithRc "extended-analysis=false" emptyCheckSpec { 546 | csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;" 547 | } 548 | 549 | prop_flagWinsWhenSuppressingDfa1 = result == [2317] 550 | where 551 | result = checkWithRc "extended-analysis=false" emptyCheckSpec { 552 | csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;", 553 | csExtendedAnalysis = Just True 554 | } 555 | 556 | prop_flagWinsWhenSuppressingDfa2 = null result 557 | where 558 | result = checkWithRc "extended-analysis=true" emptyCheckSpec { 559 | csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;", 560 | csExtendedAnalysis = Just False 561 | } 562 | 563 | return [] 564 | runTests = $quickCheckAll 565 | -------------------------------------------------------------------------------- /src/ShellCheck/Checks/ControlFlow.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2022 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | {-# LANGUAGE TemplateHaskell #-} 21 | 22 | -- Checks that run on the Control Flow Graph (as opposed to the AST) 23 | -- This is scaffolding for a work in progress. 24 | 25 | module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where 26 | 27 | import ShellCheck.AST 28 | import ShellCheck.ASTLib 29 | import ShellCheck.CFG hiding (cfgAnalysis) 30 | import ShellCheck.CFGAnalysis 31 | import ShellCheck.AnalyzerLib 32 | import ShellCheck.Data 33 | import ShellCheck.Interface 34 | 35 | import Control.Monad 36 | import Control.Monad.Reader 37 | import Data.Graph.Inductive.Graph 38 | import qualified Data.Map as M 39 | import qualified Data.Set as S 40 | import Data.List 41 | import Data.Maybe 42 | 43 | import Test.QuickCheck.All (forAllProperties) 44 | import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) 45 | 46 | 47 | optionalChecks :: [CheckDescription] 48 | optionalChecks = [] 49 | 50 | -- A check that runs on the entire graph 51 | type ControlFlowCheck = Analysis 52 | -- A check invoked once per node, with its (pre,post) data 53 | type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis 54 | -- A check invoked once per effect, with its node's (pre,post) data 55 | type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis 56 | 57 | 58 | checker :: AnalysisSpec -> Parameters -> Checker 59 | checker spec params = Checker { 60 | perScript = const $ sequence_ controlFlowChecks, 61 | perToken = const $ return () 62 | } 63 | 64 | controlFlowChecks :: [ControlFlowCheck] 65 | controlFlowChecks = [ 66 | runNodeChecks controlFlowNodeChecks 67 | ] 68 | 69 | controlFlowNodeChecks :: [ControlFlowNodeCheck] 70 | controlFlowNodeChecks = [ 71 | runEffectChecks controlFlowEffectChecks 72 | ] 73 | 74 | controlFlowEffectChecks :: [ControlFlowEffectCheck] 75 | controlFlowEffectChecks = [ 76 | ] 77 | 78 | runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck 79 | runNodeChecks perNode = do 80 | cfg <- asks cfgAnalysis 81 | mapM_ runOnAll cfg 82 | where 83 | getData datas n@(node, label) = do 84 | (pre, post) <- M.lookup node datas 85 | return (n, (pre, post)) 86 | 87 | runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis 88 | runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode 89 | runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg) 90 | 91 | runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck 92 | runEffectChecks list = checkNode 93 | where 94 | checkNode (node, label) prepost = 95 | case label of 96 | CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects 97 | _ -> return () 98 | 99 | 100 | return [] 101 | runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) 102 | -------------------------------------------------------------------------------- /src/ShellCheck/Checks/Custom.hs: -------------------------------------------------------------------------------- 1 | {- 2 | This empty file is provided for ease of patching in site specific checks. 3 | However, there are no guarantees regarding compatibility between versions. 4 | -} 5 | 6 | {-# LANGUAGE TemplateHaskell #-} 7 | module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where 8 | 9 | import ShellCheck.AnalyzerLib 10 | import Test.QuickCheck 11 | 12 | checker :: Parameters -> Checker 13 | checker params = Checker { 14 | perScript = const $ return (), 15 | perToken = const $ return () 16 | } 17 | 18 | prop_CustomTestsWork = True 19 | 20 | return [] 21 | runTests = $quickCheckAll 22 | -------------------------------------------------------------------------------- /src/ShellCheck/Data.hs: -------------------------------------------------------------------------------- 1 | module ShellCheck.Data where 2 | 3 | import ShellCheck.Interface 4 | import Data.Version (showVersion) 5 | 6 | 7 | {- 8 | If you are here because you saw an error about Paths_ShellCheck in this file, 9 | simply comment out the import below and define the version as a constant string. 10 | 11 | Instead of: 12 | 13 | import Paths_ShellCheck (version) 14 | shellcheckVersion = showVersion version 15 | 16 | Use: 17 | 18 | -- import Paths_ShellCheck (version) 19 | shellcheckVersion = "kludge" 20 | 21 | -} 22 | 23 | import Paths_ShellCheck (version) 24 | shellcheckVersion = showVersion version -- VERSIONSTRING 25 | 26 | 27 | internalVariables = [ 28 | -- Generic 29 | "", "_", "rest", "REST", 30 | 31 | -- Bash 32 | "BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC", 33 | "BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND", 34 | "BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH", 35 | "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO", 36 | "BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT", 37 | "COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK", 38 | "EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD", 39 | "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD", 40 | "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM", 41 | "READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT", 42 | "REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT", 43 | "BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS", 44 | "COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE", 45 | "FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE", 46 | "HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS", 47 | "IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE", 48 | "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", 49 | "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", 50 | "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1", 51 | "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR", 52 | "BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT", 53 | "auto_resume", "histchars", 54 | 55 | -- Other 56 | "USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY", 57 | "HOSTNAME", "KRB5CCNAME", "XAUTHORITY" 58 | 59 | -- Ksh 60 | , ".sh.version" 61 | 62 | -- shflags 63 | , "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", 64 | "FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", 65 | "flags_error", "flags_return" 66 | 67 | -- Bats 68 | ,"stderr", "stderr_lines" 69 | ] 70 | 71 | specialIntegerVariables = [ 72 | "$", "?", "!", "#" 73 | ] 74 | 75 | specialVariablesWithoutSpaces = "-" : specialIntegerVariables 76 | 77 | variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [ 78 | "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", 79 | "EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM", 80 | "READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS", 81 | "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE", 82 | "HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG" 83 | 84 | -- shflags 85 | , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE" 86 | ] 87 | 88 | specialVariables = specialVariablesWithoutSpaces ++ ["@", "*"] 89 | 90 | unbracedVariables = specialVariables ++ [ 91 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" 92 | ] 93 | 94 | arrayVariables = [ 95 | "BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO", 96 | "BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC", 97 | "DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY" 98 | ] 99 | 100 | commonCommands = [ 101 | "admin", "alias", "ar", "asa", "at", "awk", "basename", "batch", 102 | "bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp", 103 | "chmod", "chown", "cksum", "cmp", "colon", "comm", "command", 104 | "compress", "continue", "cp", "crontab", "csplit", "ctags", "cut", 105 | "cxref", "date", "dd", "delta", "df", "diff", "dirname", "dot", 106 | "du", "echo", "ed", "env", "eval", "ex", "exec", "exit", "expand", 107 | "export", "expr", "fc", "fg", "file", "find", "fold", "fort77", 108 | "fuser", "gencat", "get", "getconf", "getopts", "grep", "hash", 109 | "head", "iconv", "ipcrm", "ipcs", "jobs", "join", "kill", "lex", 110 | "link", "ln", "locale", "localedef", "logger", "logname", "lp", 111 | "ls", "m4", "mailx", "make", "man", "mesg", "mkdir", "mkfifo", 112 | "more", "mv", "newgrp", "nice", "nl", "nm", "nohup", "od", "paste", 113 | "patch", "pathchk", "pax", "pr", "printf", "prs", "ps", "pwd", 114 | "qalter", "qdel", "qhold", "qmove", "qmsg", "qrerun", "qrls", 115 | "qselect", "qsig", "qstat", "qsub", "read", "readonly", "renice", 116 | "return", "rm", "rmdel", "rmdir", "sact", "sccs", "sed", "set", 117 | "sh", "shift", "sleep", "sort", "split", "strings", "strip", "stty", 118 | "tabs", "tail", "talk", "tee", "test", "time", "times", "touch", 119 | "tput", "tr", "trap", "tsort", "tty", "type", "ulimit", "umask", 120 | "unalias", "uname", "uncompress", "unexpand", "unget", "uniq", 121 | "unlink", "unset", "uucp", "uudecode", "uuencode", "uustat", "uux", 122 | "val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc", 123 | "zcat" 124 | ] 125 | 126 | nonReadingCommands = [ 127 | "alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown", 128 | "cp", "du", "echo", "export", "fg", "fuser", "getconf", 129 | "getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", 130 | "locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir", 131 | "set", "sleep", "touch", "trap", "ulimit", "unalias", "uname" 132 | ] 133 | 134 | sampleWords = [ 135 | "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", 136 | "golf", "hotel", "india", "juliett", "kilo", "lima", "mike", 137 | "november", "oscar", "papa", "quebec", "romeo", "sierra", 138 | "tango", "uniform", "victor", "whiskey", "xray", "yankee", 139 | "zulu" 140 | ] 141 | 142 | binaryTestOps = [ 143 | "-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", 144 | "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>=" 145 | ] 146 | 147 | arithmeticBinaryTestOps = [ 148 | "-eq", "-ne", "-lt", "-le", "-gt", "-ge" 149 | ] 150 | 151 | unaryTestOps = [ 152 | "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", 153 | "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", 154 | "-o", "-v", "-R" 155 | ] 156 | 157 | shellForExecutable :: String -> Maybe Shell 158 | shellForExecutable name = 159 | case name of 160 | "sh" -> return Sh 161 | "bash" -> return Bash 162 | "bats" -> return Bash 163 | "busybox" -> return BusyboxSh -- Used for directives and --shell=busybox 164 | "busybox sh" -> return BusyboxSh 165 | "busybox ash" -> return BusyboxSh 166 | "dash" -> return Dash 167 | "ash" -> return Dash -- There's also a warning for this. 168 | "ksh" -> return Ksh 169 | "ksh88" -> return Ksh 170 | "ksh93" -> return Ksh 171 | "oksh" -> return Ksh 172 | _ -> Nothing 173 | 174 | flagsForRead = "sreu:n:N:i:p:a:t:" 175 | flagsForMapfile = "d:n:O:s:u:C:c:t" 176 | 177 | declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"] 178 | -------------------------------------------------------------------------------- /src/ShellCheck/Debug.hs: -------------------------------------------------------------------------------- 1 | {- 2 | 3 | This file contains useful functions for debugging and developing ShellCheck. 4 | 5 | To invoke them interactively, run: 6 | 7 | cabal repl 8 | 9 | At the ghci prompt, enter: 10 | 11 | :load ShellCheck.Debug 12 | 13 | You can now invoke the functions. Here are some examples: 14 | 15 | shellcheckString "echo $1" 16 | stringToAst "(( x+1 ))" 17 | stringToCfg "if foo; then bar; else baz; fi" 18 | writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done" 19 | 20 | The latter file can be rendered to png with GraphViz: 21 | 22 | dot -Tpng /tmp/test.dot > /tmp/test.png 23 | 24 | To run all unit tests in a module: 25 | 26 | ShellCheck.Parser.runTests 27 | ShellCheck.Analytics.runTests 28 | 29 | To run a specific test: 30 | 31 | :load ShellCheck.Analytics 32 | prop_checkUuoc3 33 | 34 | If you make code changes, reload in seconds at any time with: 35 | 36 | :r 37 | 38 | =========================================================================== 39 | 40 | Crash course in printf debugging in Haskell: 41 | 42 | import Debug.Trace 43 | 44 | greet 0 = return () 45 | -- Print when a function is invoked 46 | greet n | trace ("calling greet " ++ show n) False = undefined 47 | greet n = do 48 | putStrLn "Enter name" 49 | name <- getLine 50 | -- Print at some point in any monadic function 51 | traceM $ "user entered " ++ name 52 | putStrLn $ "Hello " ++ name 53 | -- Print a value before passing it on 54 | greet $ traceShowId (n - 1) 55 | 56 | 57 | =========================================================================== 58 | 59 | If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to 60 | debug all of ShellCheck including I/O, you may see an error like this: 61 | 62 | src/ShellCheck/Data.hs:5:1: error: 63 | Could not load module ‘Paths_ShellCheck’ 64 | it is a hidden module in the package ‘ShellCheck-0.8.0’ 65 | 66 | This can easily be circumvented by running `./setgitversion` or manually 67 | editing src/ShellCheck/Data.hs to replace the auto-deduced version number 68 | with a constant string as indicated. 69 | 70 | Afterwards, you can run the ShellCheck tool, as if from the shell, with: 71 | 72 | $ ghci shellcheck.hs 73 | ghci> runMain ["-x", "file.sh"] 74 | 75 | -} 76 | 77 | module ShellCheck.Debug () where 78 | 79 | import ShellCheck.Analyzer 80 | import ShellCheck.AST 81 | import ShellCheck.CFG 82 | import ShellCheck.Checker 83 | import ShellCheck.CFGAnalysis as CF 84 | import ShellCheck.Interface 85 | import ShellCheck.Parser 86 | import ShellCheck.Prelude 87 | 88 | import Control.Monad 89 | import Control.Monad.Identity 90 | import Control.Monad.RWS 91 | import Control.Monad.Writer 92 | import Data.Graph.Inductive.Graph as G 93 | import Data.List 94 | import Data.Maybe 95 | import qualified Data.Map as M 96 | import qualified Data.Set as S 97 | 98 | 99 | -- Run all of ShellCheck (minus output formatters) 100 | shellcheckString :: String -> CheckResult 101 | shellcheckString scriptString = 102 | runIdentity $ checkScript dummySystemInterface checkSpec 103 | where 104 | checkSpec :: CheckSpec 105 | checkSpec = emptyCheckSpec { 106 | csScript = scriptString 107 | } 108 | 109 | dummySystemInterface :: SystemInterface Identity 110 | dummySystemInterface = mockedSystemInterface [ 111 | -- A tiny, fake filesystem for sourced files 112 | ("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"), 113 | ("lib/mylib2.sh", "bar=42") 114 | ] 115 | 116 | -- Parameters used when generating Control Flow Graphs 117 | cfgParams :: CFGParameters 118 | cfgParams = CFGParameters { 119 | cfLastpipe = False, 120 | cfPipefail = False 121 | } 122 | 123 | -- An example script to play with 124 | exampleScript :: String 125 | exampleScript = unlines [ 126 | "#!/bin/sh", 127 | "count=0", 128 | "for file in *", 129 | "do", 130 | " (( count++ ))", 131 | "done", 132 | "echo $count" 133 | ] 134 | 135 | -- Parse the script string into ShellCheck's ParseResult 136 | parseScriptString :: String -> ParseResult 137 | parseScriptString scriptString = 138 | runIdentity $ parseScript dummySystemInterface parseSpec 139 | where 140 | parseSpec :: ParseSpec 141 | parseSpec = newParseSpec { 142 | psFilename = "myscript", 143 | psScript = scriptString 144 | } 145 | 146 | 147 | -- Parse the script string into an Abstract Syntax Tree 148 | stringToAst :: String -> Token 149 | stringToAst scriptString = 150 | case maybeRoot of 151 | Just root -> root 152 | Nothing -> error $ "Script failed to parse: " ++ show parserWarnings 153 | where 154 | parseResult :: ParseResult 155 | parseResult = parseScriptString scriptString 156 | 157 | maybeRoot :: Maybe Token 158 | maybeRoot = prRoot parseResult 159 | 160 | parserWarnings :: [PositionedComment] 161 | parserWarnings = prComments parseResult 162 | 163 | 164 | astToCfgResult :: Token -> CFGResult 165 | astToCfgResult = buildGraph cfgParams 166 | 167 | astToDfa :: Token -> CFGAnalysis 168 | astToDfa = analyzeControlFlow cfgParams 169 | 170 | astToCfg :: Token -> CFGraph 171 | astToCfg = cfGraph . astToCfgResult 172 | 173 | stringToCfg :: String -> CFGraph 174 | stringToCfg = astToCfg . stringToAst 175 | 176 | stringToDfa :: String -> CFGAnalysis 177 | stringToDfa = astToDfa . stringToAst 178 | 179 | cfgToGraphViz :: CFGraph -> String 180 | cfgToGraphViz = cfgToGraphVizWith show 181 | 182 | stringToCfgViz :: String -> String 183 | stringToCfgViz = cfgToGraphViz . stringToCfg 184 | 185 | stringToDfaViz :: String -> String 186 | stringToDfaViz = dfaToGraphViz . stringToDfa 187 | 188 | -- Dump a Control Flow Graph as GraphViz with extended information 189 | stringToDetailedCfgViz :: String -> String 190 | stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph 191 | where 192 | ast :: Token 193 | ast = stringToAst scriptString 194 | 195 | cfgResult :: CFGResult 196 | cfgResult = astToCfgResult ast 197 | 198 | graph :: CFGraph 199 | graph = cfGraph cfgResult 200 | 201 | idToToken :: M.Map Id Token 202 | idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast 203 | 204 | idToNode :: M.Map Id (Node, Node) 205 | idToNode = cfIdToRange cfgResult 206 | 207 | nodeToStartIds :: M.Map Node (S.Set Id) 208 | nodeToStartIds = 209 | M.fromListWith S.union $ 210 | map (\(id, (start, _)) -> (start, S.singleton id)) $ 211 | M.toList idToNode 212 | 213 | nodeToEndIds :: M.Map Node (S.Set Id) 214 | nodeToEndIds = 215 | M.fromListWith S.union $ 216 | map (\(id, (_, end)) -> (end, S.singleton id)) $ 217 | M.toList idToNode 218 | 219 | formatId :: Id -> String 220 | formatId id = fromMaybe ("Unknown " ++ show id) $ do 221 | (OuterToken _ token) <- M.lookup id idToToken 222 | firstWord <- words (show token) !!! 0 223 | -- Strip off "Inner_" 224 | (_ : tokenName) <- return $ dropWhile (/= '_') firstWord 225 | return $ tokenName ++ " " ++ show id 226 | 227 | formatGroup :: S.Set Id -> String 228 | formatGroup set = intercalate ", " $ map formatId $ S.toList set 229 | 230 | nodeLabel (node, label) = unlines [ 231 | show node ++ ". " ++ show label, 232 | "Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds), 233 | "End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds) 234 | ] 235 | 236 | 237 | -- Dump a Control Flow Graph with Data Flow Analysis as GraphViz 238 | dfaToGraphViz :: CF.CFGAnalysis -> String 239 | dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis 240 | where 241 | label (node, label) = 242 | let 243 | desc = show node ++ ". " ++ show label 244 | in 245 | fromMaybe ("No DFA available\n\n" ++ desc) $ do 246 | (pre, post) <- M.lookup node $ CF.nodeToData analysis 247 | return $ unlines [ 248 | "Precondition: " ++ show pre, 249 | "", 250 | desc, 251 | "", 252 | "Postcondition: " ++ show post 253 | ] 254 | 255 | 256 | -- Dump an Control Flow Graph to GraphViz with a given node formatter 257 | cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String 258 | cfgToGraphVizWith nodeLabel graph = concat [ 259 | "digraph {\n", 260 | concatMap dumpNode (labNodes graph), 261 | concatMap dumpLink (labEdges graph), 262 | tagVizEntries graph, 263 | "}\n" 264 | ] 265 | where 266 | dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n" 267 | dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ) ++ "]\n" 268 | edgeStyle CFEFlow = "solid" 269 | edgeStyle CFEExit = "bold" 270 | edgeStyle CFEFalseFlow = "dotted" 271 | 272 | quoteViz str = "\"" ++ escapeViz str ++ "\"" 273 | escapeViz [] = [] 274 | escapeViz (c:rest) = 275 | case c of 276 | '\"' -> '\\' : '\"' : escapeViz rest 277 | '\n' -> '\\' : 'l' : escapeViz rest 278 | '\\' -> '\\' : '\\' : escapeViz rest 279 | _ -> c : escapeViz rest 280 | 281 | 282 | -- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format 283 | astToGraphViz :: Token -> String 284 | astToGraphViz token = concat [ 285 | "digraph {\n", 286 | formatTree token, 287 | "}\n" 288 | ] 289 | where 290 | formatTree :: Token -> String 291 | formatTree t = snd $ execRWS (doStackAnalysis push pop t) () [] 292 | 293 | push :: Token -> RWS () String [Int] () 294 | push (OuterToken (Id n) inner) = do 295 | stack <- get 296 | put (n : stack) 297 | case stack of 298 | [] -> return () 299 | (top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n" 300 | tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n" 301 | 302 | pop :: Token -> RWS () String [Int] () 303 | pop _ = modify tail 304 | 305 | 306 | -- For each entry point, set the rank so that they'll align in the graph 307 | tagVizEntries :: CFGraph -> String 308 | tagVizEntries graph = "{ rank=same " ++ rank ++ " }" 309 | where 310 | entries = mapMaybe find $ labNodes graph 311 | find (node, CFEntryPoint name) = return (node, name) 312 | find _ = Nothing 313 | rank = unwords $ map (\(c, _) -> show c) entries 314 | -------------------------------------------------------------------------------- /src/ShellCheck/Fixer.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2018-2019 Vidar Holen, Ng Zhi An 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | 21 | {-# LANGUAGE TemplateHaskell #-} 22 | module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where 23 | 24 | import ShellCheck.Interface 25 | import ShellCheck.Prelude 26 | import Control.Monad 27 | import Control.Monad.State 28 | import Data.Array 29 | import Data.List 30 | import Data.Semigroup 31 | import GHC.Exts (sortWith) 32 | import Test.QuickCheck 33 | 34 | -- The Ranged class is used for types that has a start and end position. 35 | class Ranged a where 36 | start :: a -> Position 37 | end :: a -> Position 38 | overlap :: a -> a -> Bool 39 | overlap x y = 40 | xEnd > yStart && yEnd > xStart 41 | where 42 | yStart = start y 43 | yEnd = end y 44 | xStart = start x 45 | xEnd = end x 46 | -- Set a new start and end position on a Ranged 47 | setRange :: (Position, Position) -> a -> a 48 | 49 | -- Tests auto-verify that overlap commutes 50 | assertOverlap x y = overlap x y && overlap y x 51 | assertNoOverlap x y = not (overlap x y) && not (overlap y x) 52 | 53 | prop_overlap_contiguous = assertNoOverlap 54 | (tFromStart 10 12 "foo" 1) 55 | (tFromStart 12 14 "bar" 2) 56 | 57 | prop_overlap_adjacent_zerowidth = assertNoOverlap 58 | (tFromStart 3 3 "foo" 1) 59 | (tFromStart 3 3 "bar" 2) 60 | 61 | prop_overlap_enclosed = assertOverlap 62 | (tFromStart 3 5 "foo" 1) 63 | (tFromStart 1 10 "bar" 2) 64 | 65 | prop_overlap_partial = assertOverlap 66 | (tFromStart 1 5 "foo" 1) 67 | (tFromStart 3 7 "bar" 2) 68 | 69 | 70 | instance Ranged PositionedComment where 71 | start = pcStartPos 72 | end = pcEndPos 73 | setRange (s, e) pc = pc { 74 | pcStartPos = s, 75 | pcEndPos = e 76 | } 77 | 78 | instance Ranged Replacement where 79 | start = repStartPos 80 | end = repEndPos 81 | setRange (s, e) r = r { 82 | repStartPos = s, 83 | repEndPos = e 84 | } 85 | 86 | -- The Monoid instance for Fix merges fixes that do not conflict. 87 | -- TODO: Make an efficient 'mconcat' 88 | instance Monoid Fix where 89 | mempty = newFix 90 | mappend = (<>) 91 | mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap 92 | 93 | instance Semigroup Fix where 94 | f1 <> f2 = 95 | -- FIXME: This might need to also discard adjacent zero-width ranges for 96 | -- when two fixes change the same AST node, e.g. `foo` -> "$(foo)" 97 | if or [ r2 `overlap` r1 | r1 <- fixReplacements f1, r2 <- fixReplacements f2 ] 98 | then f1 99 | else newFix { 100 | fixReplacements = fixReplacements f1 ++ fixReplacements f2 101 | } 102 | 103 | -- Conveniently apply a transformation to positions in a Fix 104 | mapPositions :: (Position -> Position) -> Fix -> Fix 105 | mapPositions f = adjustFix 106 | where 107 | adjustReplacement rep = 108 | rep { 109 | repStartPos = f $ repStartPos rep, 110 | repEndPos = f $ repEndPos rep 111 | } 112 | adjustFix fix = 113 | fix { 114 | fixReplacements = map adjustReplacement $ fixReplacements fix 115 | } 116 | 117 | -- Rewrite a Ranged from a tabstop of 8 to 1 118 | removeTabStops :: Ranged a => a -> Array Int String -> a 119 | removeTabStops range ls = 120 | let startColumn = realignColumn lineNo colNo range 121 | endColumn = realignColumn endLineNo endColNo range 122 | startPosition = (start range) { posColumn = startColumn } 123 | endPosition = (end range) { posColumn = endColumn } in 124 | setRange (startPosition, endPosition) range 125 | where 126 | realignColumn lineNo colNo c = 127 | if lineNo c > 0 && lineNo c <= fromIntegral (length ls) 128 | then real (ls ! fromIntegral (lineNo c)) 0 0 (colNo c) 129 | else colNo c 130 | real _ r v target | target <= v = r 131 | -- hit this case at the end of line, and if we don't hit the target 132 | -- return real + (target - v) 133 | real [] r v target = r + (target - v) 134 | real ('\t':rest) r v target = real rest (r+1) (v + 8 - (v `mod` 8)) target 135 | real (_:rest) r v target = real rest (r+1) (v+1) target 136 | lineNo = posLine . start 137 | endLineNo = posLine . end 138 | colNo = posColumn . start 139 | endColNo = posColumn . end 140 | 141 | 142 | -- A replacement that spans multiple line is applied by: 143 | -- 1. merging the affected lines into a single string using `unlines` 144 | -- 2. apply the replacement as if it only spanned a single line 145 | -- The tricky part is adjusting the end column of the replacement 146 | -- (the end line doesn't matter because there is only one line) 147 | -- 148 | -- aaS <--- start of replacement (row 1 column 3) 149 | -- bbbb 150 | -- cEc 151 | -- \------- end of replacement (row 3 column 2) 152 | -- 153 | -- a flattened string will look like: 154 | -- 155 | -- "aaS\nbbbb\ncEc\n" 156 | -- 157 | -- The column of E has to be adjusted by: 158 | -- 1. lengths of lines to be replaced, except the end row itself 159 | -- 2. end column of the replacement 160 | -- 3. number of '\n' by `unlines` 161 | multiToSingleLine :: [Fix] -> Array Int String -> ([Fix], String) 162 | multiToSingleLine fixes lines = 163 | (map (mapPositions adjust) fixes, unlines $ elems lines) 164 | where 165 | -- A prefix sum tree from line number to column shift. 166 | -- FIXME: The tree will be totally unbalanced. 167 | shiftTree :: PSTree Int 168 | shiftTree = 169 | foldl (\t (n,s) -> addPSValue (n+1) (length s + 1) t) newPSTree $ 170 | assocs lines 171 | singleString = unlines $ elems lines 172 | adjust pos = 173 | pos { 174 | posLine = 1, 175 | posColumn = (posColumn pos) + 176 | (fromIntegral $ getPrefixSum (fromIntegral $ posLine pos) shiftTree) 177 | } 178 | 179 | -- Apply a fix and return resulting lines. 180 | -- The number of lines can increase or decrease with no obvious mapping back, so 181 | -- the function does not return an array. 182 | applyFix :: Fix -> Array Int String -> [String] 183 | applyFix fix fileLines = 184 | let 185 | untabbed = fix { 186 | fixReplacements = 187 | map (\c -> removeTabStops c fileLines) $ 188 | fixReplacements fix 189 | } 190 | (adjustedFixes, singleLine) = multiToSingleLine [untabbed] fileLines 191 | in 192 | lines . runFixer $ applyFixes2 adjustedFixes singleLine 193 | 194 | 195 | -- start and end comes from pos, which is 1 based 196 | prop_doReplace1 = doReplace 0 0 "1234" "A" == "A1234" -- technically not valid 197 | prop_doReplace2 = doReplace 1 1 "1234" "A" == "A1234" 198 | prop_doReplace3 = doReplace 1 2 "1234" "A" == "A234" 199 | prop_doReplace4 = doReplace 3 3 "1234" "A" == "12A34" 200 | prop_doReplace5 = doReplace 4 4 "1234" "A" == "123A4" 201 | prop_doReplace6 = doReplace 5 5 "1234" "A" == "1234A" 202 | doReplace start end o r = 203 | let si = fromIntegral (start-1) 204 | ei = fromIntegral (end-1) 205 | (x, xs) = splitAt si o 206 | z = drop (ei - si) xs 207 | in 208 | x ++ r ++ z 209 | 210 | -- Fail if the 'expected' string is not result when applying 'fixes' to 'original'. 211 | testFixes :: String -> String -> [Fix] -> Bool 212 | testFixes expected original fixes = 213 | actual == expected 214 | where 215 | actual = runFixer (applyFixes2 fixes original) 216 | 217 | 218 | -- A Fixer allows doing repeated modifications of a string where each 219 | -- replacement automatically accounts for shifts from previous ones. 220 | type Fixer a = State (PSTree Int) a 221 | 222 | -- Apply a single replacement using its indices into the original string. 223 | -- It does not handle multiple lines, all line indices must be 1. 224 | applyReplacement2 :: Replacement -> String -> Fixer String 225 | applyReplacement2 rep string = do 226 | tree <- get 227 | let transform pos = pos + getPrefixSum pos tree 228 | let originalPos = (repStartPos rep, repEndPos rep) 229 | (oldStart, oldEnd) = tmap (fromInteger . posColumn) originalPos 230 | (newStart, newEnd) = tmap transform (oldStart, oldEnd) 231 | 232 | let (l1, l2) = tmap posLine originalPos in 233 | when (l1 /= 1 || l2 /= 1) $ 234 | error $ pleaseReport "bad cross-line fix" 235 | 236 | let replacer = repString rep 237 | let shift = (length replacer) - (oldEnd - oldStart) 238 | let insertionPoint = 239 | case repInsertionPoint rep of 240 | InsertBefore -> oldStart 241 | InsertAfter -> oldEnd+1 242 | put $ addPSValue insertionPoint shift tree 243 | 244 | return $ doReplace newStart newEnd string replacer 245 | where 246 | tmap f (a,b) = (f a, f b) 247 | 248 | -- Apply a list of Replacements in the correct order 249 | applyReplacements2 :: [Replacement] -> String -> Fixer String 250 | applyReplacements2 reps str = 251 | foldM (flip applyReplacement2) str $ 252 | reverse $ sortWith repPrecedence reps 253 | 254 | -- Apply all fixes with replacements in the correct order 255 | applyFixes2 :: [Fix] -> String -> Fixer String 256 | applyFixes2 fixes = applyReplacements2 (concatMap fixReplacements fixes) 257 | 258 | -- Get the final value of a Fixer. 259 | runFixer :: Fixer a -> a 260 | runFixer f = evalState f newPSTree 261 | 262 | 263 | 264 | -- A Prefix Sum Tree that lets you look up the sum of values at and below an index. 265 | -- It's implemented essentially as a Fenwick tree without the bit-based balancing. 266 | -- The last Num is the sum of the left branch plus current element. 267 | data PSTree n = PSBranch n (PSTree n) (PSTree n) n | PSLeaf 268 | deriving (Show) 269 | 270 | newPSTree :: Num n => PSTree n 271 | newPSTree = PSLeaf 272 | 273 | -- Get the sum of values whose keys are <= 'target' 274 | getPrefixSum :: (Ord n, Num n) => n -> PSTree n -> n 275 | getPrefixSum = f 0 276 | where 277 | f sum _ PSLeaf = sum 278 | f sum target (PSBranch pivot left right cumulative) = 279 | case target `compare` pivot of 280 | LT -> f sum target left 281 | GT -> f (sum+cumulative) target right 282 | EQ -> sum+cumulative 283 | 284 | -- Add a value to the Prefix Sum tree at the given index. 285 | -- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5 286 | addPSValue :: (Ord n, Num n) => n -> n -> PSTree n -> PSTree n 287 | addPSValue key value tree = if value == 0 then tree else f tree 288 | where 289 | f PSLeaf = PSBranch key PSLeaf PSLeaf value 290 | f (PSBranch pivot left right sum) = 291 | case key `compare` pivot of 292 | LT -> PSBranch pivot (f left) right (sum + value) 293 | GT -> PSBranch pivot left (f right) sum 294 | EQ -> PSBranch pivot left right (sum + value) 295 | 296 | prop_pstreeSumsCorrectly kvs targets = 297 | let 298 | -- Trivial O(n * m) implementation 299 | dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int] 300 | dumbPrefixSums kvs targets = 301 | let prefixSum target = sum [v | (k,v) <- kvs, k <= target] 302 | in map prefixSum targets 303 | -- PSTree O(n * log m) implementation 304 | smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int] 305 | smartPrefixSums kvs targets = 306 | let tree = foldl (\tree (pos, shift) -> addPSValue pos shift tree) PSLeaf kvs 307 | in map (\x -> getPrefixSum x tree) targets 308 | in smartPrefixSums kvs targets == dumbPrefixSums kvs targets 309 | 310 | 311 | -- Semi-convenient functions for constructing tests. 312 | testFix :: [Replacement] -> Fix 313 | testFix list = newFix { 314 | fixReplacements = list 315 | } 316 | 317 | tFromStart :: Int -> Int -> String -> Int -> Replacement 318 | tFromStart start end repl order = 319 | newReplacement { 320 | repStartPos = newPosition { 321 | posLine = 1, 322 | posColumn = fromIntegral start 323 | }, 324 | repEndPos = newPosition { 325 | posLine = 1, 326 | posColumn = fromIntegral end 327 | }, 328 | repString = repl, 329 | repPrecedence = order, 330 | repInsertionPoint = InsertAfter 331 | } 332 | 333 | tFromEnd start end repl order = 334 | (tFromStart start end repl order) { 335 | repInsertionPoint = InsertBefore 336 | } 337 | 338 | prop_simpleFix1 = testFixes "hello world" "hell world" [ 339 | testFix [ 340 | tFromEnd 5 5 "o" 1 341 | ]] 342 | 343 | prop_anchorsLeft = testFixes "-->foobar<--" "--><--" [ 344 | testFix [ 345 | tFromStart 4 4 "foo" 1, 346 | tFromStart 4 4 "bar" 2 347 | ]] 348 | 349 | prop_anchorsRight = testFixes "-->foobar<--" "--><--" [ 350 | testFix [ 351 | tFromEnd 4 4 "bar" 1, 352 | tFromEnd 4 4 "foo" 2 353 | ]] 354 | 355 | prop_anchorsBoth1 = testFixes "-->foobar<--" "--><--" [ 356 | testFix [ 357 | tFromStart 4 4 "bar" 2, 358 | tFromEnd 4 4 "foo" 1 359 | ]] 360 | 361 | prop_anchorsBoth2 = testFixes "-->foobar<--" "--><--" [ 362 | testFix [ 363 | tFromEnd 4 4 "foo" 2, 364 | tFromStart 4 4 "bar" 1 365 | ]] 366 | 367 | prop_composeFixes1 = testFixes "cd \"$1\" || exit" "cd $1" [ 368 | testFix [ 369 | tFromStart 4 4 "\"" 10, 370 | tFromEnd 6 6 "\"" 10 371 | ], 372 | testFix [ 373 | tFromEnd 6 6 " || exit" 5 374 | ]] 375 | 376 | prop_composeFixes2 = testFixes "$(\"$1\")" "`$1`" [ 377 | testFix [ 378 | tFromStart 1 2 "$(" 5, 379 | tFromEnd 4 5 ")" 5 380 | ], 381 | testFix [ 382 | tFromStart 2 2 "\"" 10, 383 | tFromEnd 4 4 "\"" 10 384 | ]] 385 | 386 | prop_composeFixes3 = testFixes "(x)[x]" "xx" [ 387 | testFix [ 388 | tFromStart 1 1 "(" 4, 389 | tFromEnd 2 2 ")" 3, 390 | tFromStart 2 2 "[" 2, 391 | tFromEnd 3 3 "]" 1 392 | ]] 393 | 394 | prop_composeFixes4 = testFixes "(x)[x]" "xx" [ 395 | testFix [ 396 | tFromStart 1 1 "(" 4, 397 | tFromStart 2 2 "[" 3, 398 | tFromEnd 2 2 ")" 2, 399 | tFromEnd 3 3 "]" 1 400 | ]] 401 | 402 | prop_composeFixes5 = testFixes "\"$(x)\"" "`x`" [ 403 | testFix [ 404 | tFromStart 1 2 "$(" 2, 405 | tFromEnd 3 4 ")" 2, 406 | tFromStart 1 1 "\"" 1, 407 | tFromEnd 4 4 "\"" 1 408 | ]] 409 | 410 | 411 | return [] 412 | runTests = $quickCheckAll 413 | -------------------------------------------------------------------------------- /src/ShellCheck/Formatter/CheckStyle.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2019 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | module ShellCheck.Formatter.CheckStyle (format) where 21 | 22 | import ShellCheck.Interface 23 | import ShellCheck.Formatter.Format 24 | 25 | import Data.Char 26 | import Data.List 27 | import System.IO 28 | import qualified Data.List.NonEmpty as NE 29 | 30 | format :: IO Formatter 31 | format = return Formatter { 32 | header = do 33 | putStrLn "" 34 | putStrLn "", 35 | 36 | onFailure = outputError, 37 | onResult = outputResults, 38 | 39 | footer = putStrLn "" 40 | } 41 | 42 | outputResults cr sys = 43 | if null comments 44 | then outputFile (crFilename cr) "" [] 45 | else mapM_ outputGroup fileGroups 46 | where 47 | comments = crComments cr 48 | fileGroups = NE.groupWith sourceFile comments 49 | outputGroup group = do 50 | let filename = sourceFile (NE.head group) 51 | result <- siReadFile sys (Just True) filename 52 | let contents = either (const "") id result 53 | outputFile filename contents (NE.toList group) 54 | 55 | outputFile filename contents warnings = do 56 | let comments = makeNonVirtual warnings contents 57 | putStrLn . formatFile filename $ comments 58 | 59 | formatFile name comments = concat [ 60 | "\n", 61 | concatMap formatComment comments, 62 | "" 63 | ] 64 | 65 | formatComment c = concat [ 66 | "\n" 73 | ] 74 | 75 | outputError file error = putStrLn $ concat [ 76 | "\n", 77 | "\n", 84 | "" 85 | ] 86 | 87 | 88 | attr s v = concat [ s, "='", escape v, "' " ] 89 | escape = concatMap escape' 90 | escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";" 91 | isOk x = any ($ x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")] 92 | 93 | severity "error" = "error" 94 | severity "warning" = "warning" 95 | severity _ = "info" 96 | -------------------------------------------------------------------------------- /src/ShellCheck/Formatter/Diff.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2019 Vidar 'koala_man' Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | {-# LANGUAGE TemplateHaskell #-} 21 | module ShellCheck.Formatter.Diff (format, ShellCheck.Formatter.Diff.runTests) where 22 | 23 | import ShellCheck.Interface 24 | import ShellCheck.Fixer 25 | import ShellCheck.Formatter.Format 26 | 27 | import Control.Monad 28 | import Data.Algorithm.Diff 29 | import Data.Array 30 | import Data.IORef 31 | import Data.List 32 | import qualified Data.Monoid as Monoid 33 | import Data.Maybe 34 | import qualified Data.Map as M 35 | import GHC.Exts (sortWith) 36 | import System.IO 37 | import System.FilePath 38 | 39 | import Test.QuickCheck 40 | 41 | format :: FormatterOptions -> IO Formatter 42 | format options = do 43 | foundIssues <- newIORef False 44 | reportedIssues <- newIORef False 45 | shouldColor <- shouldOutputColor (foColorOption options) 46 | let color = if shouldColor then colorize else nocolor 47 | return Formatter { 48 | header = return (), 49 | footer = checkFooter foundIssues reportedIssues color, 50 | onFailure = reportFailure color, 51 | onResult = reportResult foundIssues reportedIssues color 52 | } 53 | 54 | 55 | contextSize = 3 56 | red = 31 57 | green = 32 58 | yellow = 33 59 | cyan = 36 60 | bold = 1 61 | 62 | nocolor n = id 63 | colorize n s = (ansi n) ++ s ++ (ansi 0) 64 | ansi n = "\x1B[" ++ show n ++ "m" 65 | 66 | printErr :: ColorFunc -> String -> IO () 67 | printErr color = hPutStrLn stderr . color bold . color red 68 | reportFailure color file msg = printErr color $ file ++ ": " ++ msg 69 | 70 | checkFooter foundIssues reportedIssues color = do 71 | found <- readIORef foundIssues 72 | output <- readIORef reportedIssues 73 | when (found && not output) $ 74 | printErr color "Issues were detected, but none were auto-fixable. Use another format to see them." 75 | 76 | type ColorFunc = (Int -> String -> String) 77 | data LFStatus = LinefeedMissing | LinefeedOk 78 | data DiffDoc a = DiffDoc String LFStatus [DiffRegion a] 79 | data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a] 80 | 81 | reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO () 82 | reportResult foundIssues reportedIssues color result sys = do 83 | let comments = crComments result 84 | unless (null comments) $ writeIORef foundIssues True 85 | let suggestedFixes = mapMaybe pcFix comments 86 | let fixmap = buildFixMap suggestedFixes 87 | mapM_ output $ M.toList fixmap 88 | where 89 | output (name, fix) = do 90 | file <- siReadFile sys (Just True) name 91 | case file of 92 | Right contents -> do 93 | putStrLn $ formatDoc color $ makeDiff name contents fix 94 | writeIORef reportedIssues True 95 | Left msg -> reportFailure color name msg 96 | 97 | hasTrailingLinefeed str = 98 | case str of 99 | [] -> True 100 | _ -> last str == '\n' 101 | 102 | coversLastLine regions = 103 | case regions of 104 | [] -> False 105 | _ -> (fst $ last regions) 106 | 107 | -- TODO: Factor this out into a unified diff library because we're doing a lot 108 | -- of the heavy lifting anyways. 109 | makeDiff :: String -> String -> Fix -> DiffDoc String 110 | makeDiff name contents fix = do 111 | let hunks = groupDiff $ computeDiff contents fix 112 | let lf = if coversLastLine hunks && not (hasTrailingLinefeed contents) 113 | then LinefeedMissing 114 | else LinefeedOk 115 | DiffDoc name lf $ findRegions hunks 116 | 117 | computeDiff :: String -> Fix -> [Diff String] 118 | computeDiff contents fix = 119 | let old = lines contents 120 | array = listArray (1, fromIntegral $ (length old)) old 121 | new = applyFix fix array 122 | in getDiff old new 123 | 124 | -- Group changes into hunks 125 | groupDiff :: [Diff a] -> [(Bool, [Diff a])] 126 | groupDiff = filter (\(_, l) -> not (null l)) . hunt [] 127 | where 128 | -- Churn through 'Both's until we find a difference 129 | hunt current [] = [(False, reverse current)] 130 | hunt current (x@Both {}:rest) = hunt (x:current) rest 131 | hunt current list = 132 | let (context, previous) = splitAt contextSize current 133 | in (False, reverse previous) : gather context 0 list 134 | 135 | -- Pick out differences until we find a run of Both's 136 | gather current n [] = 137 | let (extras, patch) = splitAt (max 0 $ n - contextSize) current 138 | in [(True, reverse patch), (False, reverse extras)] 139 | 140 | gather current n list@(Both {}:_) | n == contextSize*2 = 141 | let (context, previous) = splitAt contextSize current 142 | in (True, reverse previous) : hunt context list 143 | 144 | gather current n (x@Both {}:rest) = gather (x:current) (n+1) rest 145 | gather current n (x:rest) = gather (x:current) 0 rest 146 | 147 | -- Get line numbers for hunks 148 | findRegions :: [(Bool, [Diff String])] -> [DiffRegion String] 149 | findRegions = find' 1 1 150 | where 151 | find' _ _ [] = [] 152 | find' left right ((output, run):rest) = 153 | let (dl, dr) = countDelta run 154 | remainder = find' (left+dl) (right+dr) rest 155 | in 156 | if output 157 | then DiffRegion (left, dl) (right, dr) run : remainder 158 | else remainder 159 | 160 | -- Get left/right line counts for a hunk 161 | countDelta :: [Diff a] -> (Int, Int) 162 | countDelta = count' 0 0 163 | where 164 | count' left right [] = (left, right) 165 | count' left right (x:rest) = 166 | case x of 167 | Both {} -> count' (left+1) (right+1) rest 168 | First {} -> count' (left+1) right rest 169 | Second {} -> count' left (right+1) rest 170 | 171 | formatRegion :: ColorFunc -> LFStatus -> DiffRegion String -> String 172 | formatRegion color lf (DiffRegion left right diffs) = 173 | let header = color cyan ("@@ -" ++ (tup left) ++ " +" ++ (tup right) ++" @@") 174 | in 175 | unlines $ header : reverse (getStrings lf (reverse diffs)) 176 | where 177 | noLF = "\\ No newline at end of file" 178 | 179 | getStrings LinefeedOk list = map format list 180 | getStrings LinefeedMissing list@((Both _ _):_) = noLF : map format list 181 | getStrings LinefeedMissing list@((First _):_) = noLF : map format list 182 | getStrings LinefeedMissing (last:rest) = format last : getStrings LinefeedMissing rest 183 | 184 | tup (a,b) = (show a) ++ "," ++ (show b) 185 | format (Both x _) = ' ':x 186 | format (First x) = color red $ '-':x 187 | format (Second x) = color green $ '+':x 188 | 189 | splitLast [] = ([], []) 190 | splitLast x = 191 | let (last, rest) = splitAt 1 $ reverse x 192 | in (reverse rest, last) 193 | 194 | formatDoc color (DiffDoc name lf regions) = 195 | let (most, last) = splitLast regions 196 | in 197 | (color bold $ "--- " ++ ("a" name)) ++ "\n" ++ 198 | (color bold $ "+++ " ++ ("b" name)) ++ "\n" ++ 199 | concatMap (formatRegion color LinefeedOk) most ++ 200 | concatMap (formatRegion color lf) last 201 | 202 | -- Create a Map from filename to Fix 203 | buildFixMap :: [Fix] -> M.Map String Fix 204 | buildFixMap fixes = perFile 205 | where 206 | splitFixes = splitFixByFile $ mconcat fixes 207 | perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes 208 | 209 | splitFixByFile :: Fix -> [Fix] 210 | splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix) 211 | where 212 | sameFile rep1 rep2 = (posFile $ repStartPos rep1) == (posFile $ repStartPos rep2) 213 | makeFix reps = newFix { fixReplacements = reps } 214 | 215 | groupByMap :: (Ord k, Monoid v) => (v -> k) -> [v] -> M.Map k v 216 | groupByMap f = M.fromListWith Monoid.mappend . map (\x -> (f x, x)) 217 | 218 | -- For building unit tests 219 | b n = Both n n 220 | l = First 221 | r = Second 222 | 223 | prop_identifiesProperContext = groupDiff [b 1, b 2, b 3, b 4, l 5, b 6, b 7, b 8, b 9] == 224 | [(False, [b 1]), -- Omitted 225 | (True, [b 2, b 3, b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context 226 | (False, [b 9])] -- Omitted 227 | 228 | prop_includesContextFromStartIfNecessary = groupDiff [b 4, l 5, b 6, b 7, b 8, b 9] == 229 | [ -- Nothing omitted 230 | (True, [b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context 231 | (False, [b 9])] -- Omitted 232 | 233 | prop_includesContextUntilEndIfNecessary = groupDiff [b 4, l 5] == 234 | [ -- Nothing omitted 235 | (True, [b 4, l 5]) 236 | ] -- Nothing Omitted 237 | 238 | prop_splitsIntoMultipleHunks = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, b 7, r 8] == 239 | [ -- Nothing omitted 240 | (True, [l 1, b 1, b 2, b 3]), 241 | (False, [b 4]), 242 | (True, [b 5, b 6, b 7, r 8]) 243 | ] -- Nothing Omitted 244 | 245 | prop_splitsIntoMultipleHunksUnlessTouching = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7] == 246 | [ 247 | (True, [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7]) 248 | ] 249 | 250 | prop_countDeltasWorks = countDelta [b 1, l 2, r 3, r 4, b 5] == (3,4) 251 | prop_countDeltasWorks2 = countDelta [] == (0,0) 252 | 253 | return [] 254 | runTests = $quickCheckAll 255 | -------------------------------------------------------------------------------- /src/ShellCheck/Formatter/Format.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2019 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | module ShellCheck.Formatter.Format where 21 | 22 | import ShellCheck.Data 23 | import ShellCheck.Interface 24 | import ShellCheck.Fixer 25 | 26 | import Control.Monad 27 | import Data.Array 28 | import Data.List 29 | import System.IO 30 | import System.Info 31 | import System.Environment 32 | 33 | -- A formatter that carries along an arbitrary piece of data 34 | data Formatter = Formatter { 35 | header :: IO (), 36 | onResult :: CheckResult -> SystemInterface IO -> IO (), 37 | onFailure :: FilePath -> ErrorMessage -> IO (), 38 | footer :: IO () 39 | } 40 | 41 | sourceFile = posFile . pcStartPos 42 | lineNo = posLine . pcStartPos 43 | endLineNo = posLine . pcEndPos 44 | colNo = posColumn . pcStartPos 45 | endColNo = posColumn . pcEndPos 46 | codeNo = cCode . pcComment 47 | messageText = cMessage . pcComment 48 | 49 | severityText :: PositionedComment -> String 50 | severityText pc = 51 | case cSeverity (pcComment pc) of 52 | ErrorC -> "error" 53 | WarningC -> "warning" 54 | InfoC -> "info" 55 | StyleC -> "style" 56 | 57 | -- Realign comments from a tabstop of 8 to 1 58 | makeNonVirtual comments contents = 59 | map fix comments 60 | where 61 | list = lines contents 62 | arr = listArray (1, length list) list 63 | untabbedFix f = newFix { 64 | fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f) 65 | } 66 | fix c = (removeTabStops c arr) { 67 | pcFix = fmap untabbedFix (pcFix c) 68 | } 69 | 70 | 71 | shouldOutputColor :: ColorOption -> IO Bool 72 | shouldOutputColor colorOption = 73 | case colorOption of 74 | ColorAlways -> return True 75 | ColorNever -> return False 76 | ColorAuto -> do 77 | isTerminal <- hIsTerminalDevice stdout 78 | term <- lookupEnv "TERM" 79 | let windows = "mingw" `isPrefixOf` os 80 | let dumbTerm = term `elem` [Just "dumb", Just "", Nothing] 81 | let isUsableTty = isTerminal && not windows && not dumbTerm 82 | return isUsableTty 83 | -------------------------------------------------------------------------------- /src/ShellCheck/Formatter/GCC.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2019 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | module ShellCheck.Formatter.GCC (format) where 21 | 22 | import ShellCheck.Interface 23 | import ShellCheck.Formatter.Format 24 | 25 | import Data.List 26 | import System.IO 27 | import qualified Data.List.NonEmpty as NE 28 | 29 | format :: IO Formatter 30 | format = return Formatter { 31 | header = return (), 32 | footer = return (), 33 | onFailure = outputError, 34 | onResult = outputAll 35 | } 36 | 37 | outputError file error = hPutStrLn stderr $ file ++ ": " ++ error 38 | 39 | outputAll cr sys = mapM_ f groups 40 | where 41 | comments = crComments cr 42 | groups = NE.groupWith sourceFile comments 43 | f :: NE.NonEmpty PositionedComment -> IO () 44 | f group = do 45 | let filename = sourceFile (NE.head group) 46 | result <- siReadFile sys (Just True) filename 47 | let contents = either (const "") id result 48 | outputResult filename contents (NE.toList group) 49 | 50 | outputResult filename contents warnings = do 51 | let comments = makeNonVirtual warnings contents 52 | mapM_ (putStrLn . formatComment filename) comments 53 | 54 | formatComment filename c = concat [ 55 | filename, ":", 56 | show $ lineNo c, ":", 57 | show $ colNo c, ": ", 58 | case severityText c of 59 | "error" -> "error" 60 | "warning" -> "warning" 61 | _ -> "note", 62 | ": ", 63 | concat . lines $ messageText c, 64 | " [SC", show $ codeNo c, "]" 65 | ] 66 | -------------------------------------------------------------------------------- /src/ShellCheck/Formatter/JSON.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {- 3 | Copyright 2012-2019 Vidar Holen 4 | 5 | This file is part of ShellCheck. 6 | https://www.shellcheck.net 7 | 8 | ShellCheck is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | ShellCheck is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | -} 21 | module ShellCheck.Formatter.JSON (format) where 22 | 23 | import ShellCheck.Interface 24 | import ShellCheck.Formatter.Format 25 | 26 | import Control.DeepSeq 27 | import Data.Aeson 28 | import Data.IORef 29 | import Data.Monoid 30 | import GHC.Exts 31 | import System.IO 32 | import qualified Data.ByteString.Lazy.Char8 as BL 33 | 34 | format :: IO Formatter 35 | format = do 36 | ref <- newIORef [] 37 | return Formatter { 38 | header = return (), 39 | onResult = collectResult ref, 40 | onFailure = outputError, 41 | footer = finish ref 42 | } 43 | 44 | instance ToJSON Replacement where 45 | toJSON replacement = 46 | let start = repStartPos replacement 47 | end = repEndPos replacement 48 | str = repString replacement in 49 | object [ 50 | "precedence" .= repPrecedence replacement, 51 | "insertionPoint" .= 52 | case repInsertionPoint replacement of 53 | InsertBefore -> "beforeStart" :: String 54 | InsertAfter -> "afterEnd", 55 | "line" .= posLine start, 56 | "column" .= posColumn start, 57 | "endLine" .= posLine end, 58 | "endColumn" .= posColumn end, 59 | "replacement" .= str 60 | ] 61 | 62 | instance ToJSON PositionedComment where 63 | toJSON comment = 64 | let start = pcStartPos comment 65 | end = pcEndPos comment 66 | c = pcComment comment in 67 | object [ 68 | "file" .= posFile start, 69 | "line" .= posLine start, 70 | "endLine" .= posLine end, 71 | "column" .= posColumn start, 72 | "endColumn" .= posColumn end, 73 | "level" .= severityText comment, 74 | "code" .= cCode c, 75 | "message" .= cMessage c, 76 | "fix" .= pcFix comment 77 | ] 78 | 79 | toEncoding comment = 80 | let start = pcStartPos comment 81 | end = pcEndPos comment 82 | c = pcComment comment in 83 | pairs ( 84 | "file" .= posFile start 85 | <> "line" .= posLine start 86 | <> "endLine" .= posLine end 87 | <> "column" .= posColumn start 88 | <> "endColumn" .= posColumn end 89 | <> "level" .= severityText comment 90 | <> "code" .= cCode c 91 | <> "message" .= cMessage c 92 | <> "fix" .= pcFix comment 93 | ) 94 | 95 | instance ToJSON Fix where 96 | toJSON fix = object [ 97 | "replacements" .= fixReplacements fix 98 | ] 99 | 100 | outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg 101 | 102 | collectResult ref cr sys = mapM_ f groups 103 | where 104 | comments = crComments cr 105 | groups = groupWith sourceFile comments 106 | f :: [PositionedComment] -> IO () 107 | f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x) 108 | 109 | finish ref = do 110 | list <- readIORef ref 111 | BL.putStrLn $ encode list 112 | -------------------------------------------------------------------------------- /src/ShellCheck/Formatter/JSON1.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {- 3 | Copyright 2012-2019 Vidar Holen 4 | 5 | This file is part of ShellCheck. 6 | https://www.shellcheck.net 7 | 8 | ShellCheck is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | ShellCheck is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | -} 21 | module ShellCheck.Formatter.JSON1 (format) where 22 | 23 | import ShellCheck.Interface 24 | import ShellCheck.Formatter.Format 25 | 26 | import Control.DeepSeq 27 | import Data.Aeson 28 | import Data.IORef 29 | import Data.Monoid 30 | import System.IO 31 | import qualified Data.ByteString.Lazy.Char8 as BL 32 | import qualified Data.List.NonEmpty as NE 33 | 34 | format :: IO Formatter 35 | format = do 36 | ref <- newIORef [] 37 | return Formatter { 38 | header = return (), 39 | onResult = collectResult ref, 40 | onFailure = outputError, 41 | footer = finish ref 42 | } 43 | 44 | data Json1Output = Json1Output { 45 | comments :: [PositionedComment] 46 | } 47 | 48 | instance ToJSON Json1Output where 49 | toJSON result = object [ 50 | "comments" .= comments result 51 | ] 52 | toEncoding result = pairs ( 53 | "comments" .= comments result 54 | ) 55 | 56 | instance ToJSON Replacement where 57 | toJSON replacement = 58 | let start = repStartPos replacement 59 | end = repEndPos replacement 60 | str = repString replacement in 61 | object [ 62 | "precedence" .= repPrecedence replacement, 63 | "insertionPoint" .= 64 | case repInsertionPoint replacement of 65 | InsertBefore -> "beforeStart" :: String 66 | InsertAfter -> "afterEnd", 67 | "line" .= posLine start, 68 | "column" .= posColumn start, 69 | "endLine" .= posLine end, 70 | "endColumn" .= posColumn end, 71 | "replacement" .= str 72 | ] 73 | 74 | instance ToJSON PositionedComment where 75 | toJSON comment = 76 | let start = pcStartPos comment 77 | end = pcEndPos comment 78 | c = pcComment comment in 79 | object [ 80 | "file" .= posFile start, 81 | "line" .= posLine start, 82 | "endLine" .= posLine end, 83 | "column" .= posColumn start, 84 | "endColumn" .= posColumn end, 85 | "level" .= severityText comment, 86 | "code" .= cCode c, 87 | "message" .= cMessage c, 88 | "fix" .= pcFix comment 89 | ] 90 | 91 | toEncoding comment = 92 | let start = pcStartPos comment 93 | end = pcEndPos comment 94 | c = pcComment comment in 95 | pairs ( 96 | "file" .= posFile start 97 | <> "line" .= posLine start 98 | <> "endLine" .= posLine end 99 | <> "column" .= posColumn start 100 | <> "endColumn" .= posColumn end 101 | <> "level" .= severityText comment 102 | <> "code" .= cCode c 103 | <> "message" .= cMessage c 104 | <> "fix" .= pcFix comment 105 | ) 106 | 107 | instance ToJSON Fix where 108 | toJSON fix = object [ 109 | "replacements" .= fixReplacements fix 110 | ] 111 | 112 | outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg 113 | 114 | collectResult ref cr sys = mapM_ f groups 115 | where 116 | comments = crComments cr 117 | groups = NE.groupWith sourceFile comments 118 | f :: NE.NonEmpty PositionedComment -> IO () 119 | f group = do 120 | let filename = sourceFile (NE.head group) 121 | result <- siReadFile sys (Just True) filename 122 | let contents = either (const "") id result 123 | let comments' = makeNonVirtual comments contents 124 | deepseq comments' $ modifyIORef ref (\x -> comments' ++ x) 125 | 126 | finish ref = do 127 | list <- readIORef ref 128 | BL.putStrLn $ encode $ Json1Output { comments = list } 129 | -------------------------------------------------------------------------------- /src/ShellCheck/Formatter/Quiet.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2019 Austin Voecks 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | module ShellCheck.Formatter.Quiet (format) where 21 | 22 | import ShellCheck.Interface 23 | import ShellCheck.Formatter.Format 24 | 25 | import Control.Monad 26 | import Data.IORef 27 | import System.Exit 28 | 29 | format :: FormatterOptions -> IO Formatter 30 | format options = 31 | return Formatter { 32 | header = return (), 33 | footer = return (), 34 | onFailure = \ _ _ -> exitFailure, 35 | onResult = \ result _ -> unless (null $ crComments result) exitFailure 36 | } 37 | -------------------------------------------------------------------------------- /src/ShellCheck/Formatter/TTY.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2019 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | module ShellCheck.Formatter.TTY (format) where 21 | 22 | import ShellCheck.Fixer 23 | import ShellCheck.Interface 24 | import ShellCheck.Formatter.Format 25 | 26 | import Control.DeepSeq 27 | import Control.Monad 28 | import Data.Array 29 | import Data.Foldable 30 | import Data.Ord 31 | import Data.IORef 32 | import Data.List 33 | import Data.Maybe 34 | import System.IO 35 | import System.Info 36 | import qualified Data.List.NonEmpty as NE 37 | 38 | wikiLink = "https://www.shellcheck.net/wiki/" 39 | 40 | -- An arbitrary Ord thing to order warnings 41 | type Ranking = (Char, Severity, Integer) 42 | -- Ansi coloring function 43 | type ColorFunc = (String -> String -> String) 44 | 45 | format :: FormatterOptions -> IO Formatter 46 | format options = do 47 | topErrorRef <- newIORef [] 48 | return Formatter { 49 | header = return (), 50 | footer = outputWiki topErrorRef, 51 | onFailure = outputError options, 52 | onResult = outputResult options topErrorRef 53 | } 54 | 55 | colorForLevel level = 56 | case level of 57 | "error" -> 31 -- red 58 | "warning" -> 33 -- yellow 59 | "info" -> 32 -- green 60 | "style" -> 32 -- green 61 | "verbose" -> 32 -- green 62 | "message" -> 1 -- bold 63 | "source" -> 0 -- none 64 | _ -> 0 -- none 65 | 66 | rankError :: PositionedComment -> Ranking 67 | rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err) 68 | where 69 | ranking = 70 | if cCode (pcComment err) `elem` uninteresting 71 | then 'Z' 72 | else 'A' 73 | 74 | -- A list of the most generic, least directly helpful 75 | -- error codes to downrank. 76 | uninteresting = [ 77 | 1009, -- Mentioned parser error was.. 78 | 1019, -- Expected this to be an argument 79 | 1036, -- ( is invalid here 80 | 1047, -- Expected 'fi' 81 | 1062, -- Expected 'done' 82 | 1070, -- Parsing stopped here (generic) 83 | 1072, -- Missing/unexpected .. 84 | 1073, -- Couldn't parse this .. 85 | 1088, -- Parsing stopped here (paren) 86 | 1089 -- Parsing stopped here (keyword) 87 | ] 88 | 89 | appendComments errRef comments max = do 90 | previous <- readIORef errRef 91 | let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments 92 | writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current 93 | where 94 | fst3 (x,_,_) = x 95 | equal x y = fst3 x == fst3 y 96 | 97 | outputWiki :: IORef [(Ranking, Integer, String)] -> IO () 98 | outputWiki errRef = do 99 | issues <- readIORef errRef 100 | unless (null issues) $ do 101 | putStrLn "For more information:" 102 | mapM_ showErr issues 103 | where 104 | showErr (_, code, msg) = 105 | putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg 106 | limit = 36 107 | shorten msg = 108 | if length msg < limit 109 | then msg 110 | else (take (limit-3) msg) ++ "..." 111 | 112 | outputError options file error = do 113 | color <- getColorFunc $ foColorOption options 114 | hPutStrLn stderr $ color "error" $ file ++ ": " ++ error 115 | 116 | outputResult options ref result sys = do 117 | color <- getColorFunc $ foColorOption options 118 | let comments = crComments result 119 | appendComments ref comments (fromIntegral $ foWikiLinkCount options) 120 | let fileGroups = NE.groupWith sourceFile comments 121 | mapM_ (outputForFile color sys) fileGroups 122 | 123 | outputForFile color sys comments = do 124 | let fileName = sourceFile (NE.head comments) 125 | result <- siReadFile sys (Just True) fileName 126 | let contents = either (const "") id result 127 | let fileLinesList = lines contents 128 | let lineCount = length fileLinesList 129 | let fileLines = listArray (1, lineCount) fileLinesList 130 | let groups = NE.groupWith lineNo comments 131 | forM_ groups $ \commentsForLine -> do 132 | let lineNum = fromIntegral $ lineNo (NE.head commentsForLine) 133 | let line = if lineNum < 1 || lineNum > lineCount 134 | then "" 135 | else fileLines ! fromIntegral lineNum 136 | putStrLn "" 137 | putStrLn $ color "message" $ 138 | "In " ++ fileName ++" line " ++ show lineNum ++ ":" 139 | putStrLn (color "source" line) 140 | forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c 141 | putStrLn "" 142 | showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines 143 | 144 | -- Pick out only the lines necessary to show a fix in action 145 | sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) 146 | sliceFile fix lines = 147 | (mapPositions adjust fix, sliceLines lines) 148 | where 149 | (minLine, maxLine) = 150 | foldl (\(mm, mx) pos -> ((min mm $ fromIntegral $ posLine pos), (max mx $ fromIntegral $ posLine pos))) 151 | (maxBound, minBound) $ 152 | concatMap (\x -> [repStartPos x, repEndPos x]) $ fixReplacements fix 153 | sliceLines :: Array Int String -> Array Int String 154 | sliceLines = ixmap (1, maxLine - minLine + 1) (\x -> x + minLine - 1) 155 | adjust pos = 156 | pos { 157 | posLine = posLine pos - (fromIntegral minLine) + 1 158 | } 159 | 160 | showFixedString :: ColorFunc -> [PositionedComment] -> Int -> Array Int String -> IO () 161 | showFixedString color comments lineNum fileLines = 162 | let line = fileLines ! fromIntegral lineNum in 163 | case mapMaybe pcFix comments of 164 | [] -> return () 165 | fixes -> do 166 | -- Folding automatically removes overlap 167 | let mergedFix = fold fixes 168 | -- We show the complete, associated fixes, whether or not it includes this 169 | -- and/or other unrelated lines. 170 | let (excerptFix, excerpt) = sliceFile mergedFix fileLines 171 | -- in the spirit of error prone 172 | putStrLn $ color "message" "Did you mean:" 173 | putStrLn $ unlines $ applyFix excerptFix excerpt 174 | 175 | cuteIndent :: PositionedComment -> String 176 | cuteIndent comment = 177 | replicate (fromIntegral $ colNo comment - 1) ' ' ++ 178 | makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment 179 | where 180 | arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^" 181 | makeArrow = 182 | let sameLine = lineNo comment == endLineNo comment 183 | delta = endColNo comment - colNo comment 184 | in 185 | if sameLine && delta > 2 && delta < 32 then arrow delta else "^--" 186 | 187 | code num = "SC" ++ show num 188 | 189 | getColorFunc :: ColorOption -> IO ColorFunc 190 | getColorFunc colorOption = do 191 | useColor <- shouldOutputColor colorOption 192 | return $ if useColor then colorComment else const id 193 | where 194 | colorComment level comment = 195 | ansi (colorForLevel level) ++ comment ++ clear 196 | clear = ansi 0 197 | ansi n = "\x1B[" ++ show n ++ "m" 198 | -------------------------------------------------------------------------------- /src/ShellCheck/Interface.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2024 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | {-# LANGUAGE DeriveGeneric, DeriveAnyClass #-} 21 | module ShellCheck.Interface 22 | ( 23 | SystemInterface(..) 24 | , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks) 25 | , CheckResult(crFilename, crComments) 26 | , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) 27 | , ParseResult(prComments, prTokenPositions, prRoot) 28 | , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks) 29 | , AnalysisResult(arComments) 30 | , FormatterOptions(foColorOption, foWikiLinkCount) 31 | , Shell(Ksh, Sh, Bash, Dash, BusyboxSh) 32 | , ExecutionMode(Executed, Sourced) 33 | , ErrorMessage 34 | , Code 35 | , Severity(ErrorC, WarningC, InfoC, StyleC) 36 | , Position(posFile, posLine, posColumn) 37 | , Comment(cSeverity, cCode, cMessage) 38 | , PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix) 39 | , ColorOption(ColorAuto, ColorAlways, ColorNever) 40 | , TokenComment(tcId, tcComment, tcFix) 41 | , emptyCheckResult 42 | , newAnalysisResult 43 | , newAnalysisSpec 44 | , newFormatterOptions 45 | , newParseResult 46 | , newPosition 47 | , newSystemInterface 48 | , newTokenComment 49 | , mockedSystemInterface 50 | , mockRcFile 51 | , newParseSpec 52 | , emptyCheckSpec 53 | , newPositionedComment 54 | , newComment 55 | , Fix(fixReplacements) 56 | , newFix 57 | , InsertionPoint(InsertBefore, InsertAfter) 58 | , Replacement(repStartPos, repEndPos, repString, repPrecedence, repInsertionPoint) 59 | , newReplacement 60 | , CheckDescription(cdName, cdDescription, cdPositive, cdNegative) 61 | , newCheckDescription 62 | ) where 63 | 64 | import ShellCheck.AST 65 | 66 | import Control.DeepSeq 67 | import Control.Monad.Identity 68 | import Data.List 69 | import Data.Monoid 70 | import Data.Ord 71 | import Data.Semigroup 72 | import GHC.Generics (Generic) 73 | import qualified Data.Map as Map 74 | 75 | 76 | data SystemInterface m = SystemInterface { 77 | -- | Given: 78 | -- What annotations say about including external files (if anything) 79 | -- A resolved filename from siFindSource 80 | -- Read the file or return an error 81 | siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String), 82 | -- | Given: 83 | -- the current script, 84 | -- what annotations say about including external files (if anything) 85 | -- a list of source-path annotations in effect, 86 | -- and a sourced file, 87 | -- find the sourced file 88 | siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath, 89 | -- | Get the configuration file (name, contents) for a filename 90 | siGetConfig :: String -> m (Maybe (FilePath, String)) 91 | } 92 | 93 | -- ShellCheck input and output 94 | data CheckSpec = CheckSpec { 95 | csFilename :: String, 96 | csScript :: String, 97 | csCheckSourced :: Bool, 98 | csIgnoreRC :: Bool, 99 | csExcludedWarnings :: [Integer], 100 | csIncludedWarnings :: Maybe [Integer], 101 | csShellTypeOverride :: Maybe Shell, 102 | csMinSeverity :: Severity, 103 | csExtendedAnalysis :: Maybe Bool, 104 | csOptionalChecks :: [String] 105 | } deriving (Show, Eq) 106 | 107 | data CheckResult = CheckResult { 108 | crFilename :: String, 109 | crComments :: [PositionedComment] 110 | } deriving (Show, Eq) 111 | 112 | emptyCheckResult :: CheckResult 113 | emptyCheckResult = CheckResult { 114 | crFilename = "", 115 | crComments = [] 116 | } 117 | 118 | emptyCheckSpec :: CheckSpec 119 | emptyCheckSpec = CheckSpec { 120 | csFilename = "", 121 | csScript = "", 122 | csCheckSourced = False, 123 | csIgnoreRC = False, 124 | csExcludedWarnings = [], 125 | csIncludedWarnings = Nothing, 126 | csShellTypeOverride = Nothing, 127 | csMinSeverity = StyleC, 128 | csExtendedAnalysis = Nothing, 129 | csOptionalChecks = [] 130 | } 131 | 132 | newParseSpec :: ParseSpec 133 | newParseSpec = ParseSpec { 134 | psFilename = "", 135 | psScript = "", 136 | psCheckSourced = False, 137 | psIgnoreRC = False, 138 | psShellTypeOverride = Nothing 139 | } 140 | 141 | newSystemInterface :: Monad m => SystemInterface m 142 | newSystemInterface = 143 | SystemInterface { 144 | siReadFile = \_ _ -> return $ Left "Not implemented", 145 | siFindSource = \_ _ _ name -> return name, 146 | siGetConfig = \_ -> return Nothing 147 | } 148 | 149 | -- Parser input and output 150 | data ParseSpec = ParseSpec { 151 | psFilename :: String, 152 | psScript :: String, 153 | psCheckSourced :: Bool, 154 | psIgnoreRC :: Bool, 155 | psShellTypeOverride :: Maybe Shell 156 | } deriving (Show, Eq) 157 | 158 | data ParseResult = ParseResult { 159 | prComments :: [PositionedComment], 160 | prTokenPositions :: Map.Map Id (Position, Position), 161 | prRoot :: Maybe Token 162 | } deriving (Show, Eq) 163 | 164 | newParseResult :: ParseResult 165 | newParseResult = ParseResult { 166 | prComments = [], 167 | prTokenPositions = Map.empty, 168 | prRoot = Nothing 169 | } 170 | 171 | -- Analyzer input and output 172 | data AnalysisSpec = AnalysisSpec { 173 | asScript :: Token, 174 | asShellType :: Maybe Shell, 175 | asFallbackShell :: Maybe Shell, 176 | asExecutionMode :: ExecutionMode, 177 | asCheckSourced :: Bool, 178 | asOptionalChecks :: [String], 179 | asExtendedAnalysis :: Maybe Bool, 180 | asTokenPositions :: Map.Map Id (Position, Position) 181 | } 182 | 183 | newAnalysisSpec token = AnalysisSpec { 184 | asScript = token, 185 | asShellType = Nothing, 186 | asFallbackShell = Nothing, 187 | asExecutionMode = Executed, 188 | asCheckSourced = False, 189 | asOptionalChecks = [], 190 | asExtendedAnalysis = Nothing, 191 | asTokenPositions = Map.empty 192 | } 193 | 194 | newtype AnalysisResult = AnalysisResult { 195 | arComments :: [TokenComment] 196 | } 197 | 198 | newAnalysisResult = AnalysisResult { 199 | arComments = [] 200 | } 201 | 202 | -- Formatter options 203 | data FormatterOptions = FormatterOptions { 204 | foColorOption :: ColorOption, 205 | foWikiLinkCount :: Integer 206 | } 207 | 208 | newFormatterOptions = FormatterOptions { 209 | foColorOption = ColorAuto, 210 | foWikiLinkCount = 3 211 | } 212 | 213 | data CheckDescription = CheckDescription { 214 | cdName :: String, 215 | cdDescription :: String, 216 | cdPositive :: String, 217 | cdNegative :: String 218 | } 219 | 220 | newCheckDescription = CheckDescription { 221 | cdName = "", 222 | cdDescription = "", 223 | cdPositive = "", 224 | cdNegative = "" 225 | } 226 | 227 | -- Supporting data types 228 | data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq) 229 | data ExecutionMode = Executed | Sourced deriving (Show, Eq) 230 | 231 | type ErrorMessage = String 232 | type Code = Integer 233 | 234 | data Severity = ErrorC | WarningC | InfoC | StyleC 235 | deriving (Show, Eq, Ord, Generic, NFData) 236 | data Position = Position { 237 | posFile :: String, -- Filename 238 | posLine :: Integer, -- 1 based source line 239 | posColumn :: Integer -- 1 based source column, where tabs are 8 240 | } deriving (Show, Eq, Generic, NFData, Ord) 241 | 242 | newPosition :: Position 243 | newPosition = Position { 244 | posFile = "", 245 | posLine = 1, 246 | posColumn = 1 247 | } 248 | 249 | data Comment = Comment { 250 | cSeverity :: Severity, 251 | cCode :: Code, 252 | cMessage :: String 253 | } deriving (Show, Eq, Generic, NFData) 254 | 255 | newComment :: Comment 256 | newComment = Comment { 257 | cSeverity = StyleC, 258 | cCode = 0, 259 | cMessage = "" 260 | } 261 | 262 | -- only support single line for now 263 | data Replacement = Replacement { 264 | repStartPos :: Position, 265 | repEndPos :: Position, 266 | repString :: String, 267 | -- Order in which the replacements should happen: highest precedence first. 268 | repPrecedence :: Int, 269 | -- Whether to insert immediately before or immediately after the specified region. 270 | repInsertionPoint :: InsertionPoint 271 | } deriving (Show, Eq, Generic, NFData) 272 | 273 | data InsertionPoint = InsertBefore | InsertAfter 274 | deriving (Show, Eq, Generic, NFData) 275 | 276 | newReplacement = Replacement { 277 | repStartPos = newPosition, 278 | repEndPos = newPosition, 279 | repString = "", 280 | repPrecedence = 1, 281 | repInsertionPoint = InsertAfter 282 | } 283 | 284 | data Fix = Fix { 285 | fixReplacements :: [Replacement] 286 | } deriving (Show, Eq, Generic, NFData) 287 | 288 | newFix = Fix { 289 | fixReplacements = [] 290 | } 291 | 292 | data PositionedComment = PositionedComment { 293 | pcStartPos :: Position, 294 | pcEndPos :: Position, 295 | pcComment :: Comment, 296 | pcFix :: Maybe Fix 297 | } deriving (Show, Eq, Generic, NFData) 298 | 299 | newPositionedComment :: PositionedComment 300 | newPositionedComment = PositionedComment { 301 | pcStartPos = newPosition, 302 | pcEndPos = newPosition, 303 | pcComment = newComment, 304 | pcFix = Nothing 305 | } 306 | 307 | data TokenComment = TokenComment { 308 | tcId :: Id, 309 | tcComment :: Comment, 310 | tcFix :: Maybe Fix 311 | } deriving (Show, Eq, Generic, NFData) 312 | 313 | newTokenComment = TokenComment { 314 | tcId = Id 0, 315 | tcComment = newComment, 316 | tcFix = Nothing 317 | } 318 | 319 | data ColorOption = 320 | ColorAuto 321 | | ColorAlways 322 | | ColorNever 323 | deriving (Ord, Eq, Show) 324 | 325 | -- For testing 326 | mockedSystemInterface :: [(String, String)] -> SystemInterface Identity 327 | mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) { 328 | siReadFile = rf, 329 | siFindSource = fs, 330 | siGetConfig = const $ return Nothing 331 | } 332 | where 333 | rf _ file = return $ 334 | case find ((== file) . fst) files of 335 | Nothing -> Left "File not included in mock." 336 | Just (_, contents) -> Right contents 337 | fs _ _ _ file = return file 338 | 339 | mockRcFile rcfile mock = mock { 340 | siGetConfig = const . return $ Just (".shellcheckrc", rcfile) 341 | } 342 | -------------------------------------------------------------------------------- /src/ShellCheck/Prelude.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2022 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | 21 | -- Generic basic utility functions 22 | module ShellCheck.Prelude where 23 | 24 | import Data.Semigroup 25 | 26 | 27 | -- Get element 0 or a default. Like `head` but safe. 28 | headOrDefault _ (a:_) = a 29 | headOrDefault def _ = def 30 | 31 | -- Get the last element or a default. Like `last` but safe. 32 | lastOrDefault def [] = def 33 | lastOrDefault _ list = last list 34 | 35 | --- Get element n of a list, or Nothing. Like `!!` but safe. 36 | (!!!) list i = 37 | case drop i list of 38 | [] -> Nothing 39 | (r:_) -> Just r 40 | 41 | 42 | -- Like mconcat but for Semigroups 43 | sconcat1 :: (Semigroup t) => [t] -> t 44 | sconcat1 [x] = x 45 | sconcat1 (x:xs) = x <> sconcat1 xs 46 | 47 | sconcatOrDefault def [] = def 48 | sconcatOrDefault _ list = sconcat1 list 49 | 50 | -- For more actionable "impossible" errors 51 | pleaseReport str = "ShellCheck internal error, please report: " ++ str 52 | -------------------------------------------------------------------------------- /src/ShellCheck/Regex.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright 2012-2019 Vidar Holen 3 | 4 | This file is part of ShellCheck. 5 | https://www.shellcheck.net 6 | 7 | ShellCheck is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | ShellCheck is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -} 20 | {-# LANGUAGE FlexibleContexts #-} 21 | 22 | -- Basically Text.Regex based on regex-tdfa instead of the buggy regex-posix. 23 | module ShellCheck.Regex where 24 | 25 | import Data.List 26 | import Data.Maybe 27 | import Control.Monad 28 | import Text.Regex.TDFA 29 | 30 | -- Precompile the regex 31 | mkRegex :: String -> Regex 32 | mkRegex str = 33 | let make :: String -> Regex 34 | make = makeRegex 35 | in 36 | make str 37 | 38 | -- Does the regex match? 39 | matches :: String -> Regex -> Bool 40 | matches = flip match 41 | 42 | -- Get all subgroups of the first match 43 | matchRegex :: Regex -> String -> Maybe [String] 44 | matchRegex re str = do 45 | (_, _, _, groups) <- matchM re str :: Maybe (String,String,String,[String]) 46 | return groups 47 | 48 | -- Get all full matches 49 | matchAllStrings :: Regex -> String -> [String] 50 | matchAllStrings re = unfoldr f 51 | where 52 | f :: String -> Maybe (String, String) 53 | f str = do 54 | (_, match, rest, _) <- matchM re str :: Maybe (String, String, String, [String]) 55 | return (match, rest) 56 | 57 | -- Get all subgroups from all matches 58 | matchAllSubgroups :: Regex -> String -> [[String]] 59 | matchAllSubgroups re = unfoldr f 60 | where 61 | f :: String -> Maybe ([String], String) 62 | f str = do 63 | (_, _, rest, groups) <- matchM re str :: Maybe (String, String, String, [String]) 64 | return (groups, rest) 65 | 66 | -- Replace regex in input with string 67 | subRegex :: Regex -> String -> String -> String 68 | subRegex re input replacement = f input 69 | where 70 | f str = fromMaybe str $ do 71 | (before, match, after) <- matchM re str :: Maybe (String, String, String) 72 | when (null match) $ error ("Internal error: substituted empty in " ++ str) 73 | return $ before ++ replacement ++ f after 74 | 75 | -- Split a string based on a regex. 76 | splitOn :: String -> Regex -> [String] 77 | splitOn input re = 78 | case matchM re input :: Maybe (String, String, String) of 79 | Just (before, match, after) -> before : after `splitOn` re 80 | Nothing -> [input] 81 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by stack init 2 | # For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/ 3 | 4 | # Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) 5 | resolver: lts-18.15 6 | 7 | # Local packages, usually specified by relative directory name 8 | packages: 9 | - '.' 10 | # Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3) 11 | extra-deps: [] 12 | 13 | # Override default flag values for local packages and extra-deps 14 | flags: {} 15 | 16 | # Extra package databases containing global packages 17 | extra-package-dbs: [] 18 | 19 | # Control whether we use the GHC we find on the path 20 | # system-ghc: true 21 | 22 | # Require a specific version of stack, using version ranges 23 | # require-stack-version: -any # Default 24 | # require-stack-version: >= 1.0.0 25 | 26 | # Override the architecture used by stack, especially useful on Windows 27 | # arch: i386 28 | # arch: x86_64 29 | 30 | # Extra directories used by stack for building 31 | # extra-include-dirs: [/path/to/dir] 32 | # extra-lib-dirs: [/path/to/dir] 33 | 34 | # Allow a newer minor version of GHC than the snapshot specifies 35 | # compiler-check: newer-minor 36 | -------------------------------------------------------------------------------- /striptests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file strips all unit tests from ShellCheck, removing 3 | # the dependency on QuickCheck and Template Haskell and 4 | # reduces the binary size considerably. 5 | set -o pipefail 6 | 7 | sponge() { 8 | local data 9 | data="$(cat)" 10 | printf '%s\n' "$data" > "$1" 11 | } 12 | 13 | modify() { 14 | if ! "${@:2}" < "$1" | sponge "$1" 15 | then 16 | { 17 | printf 'Failed to modify %s: ' "$1" 18 | printf '%q ' "${@:2}" 19 | printf '\n' 20 | } >&2 21 | exit 1 22 | fi 23 | } 24 | 25 | detestify() { 26 | printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.' 27 | awk ' 28 | BEGIN { 29 | state = 0; 30 | } 31 | 32 | /STRIP/ { next; } 33 | /LANGUAGE TemplateHaskell/ { next; } 34 | /^import.*Test\./ { next; } 35 | 36 | /^module/ { 37 | sub(/,[^,)]*runTests/, ""); 38 | } 39 | 40 | # Delete tests 41 | /^prop_/ { state = 1; next; } 42 | 43 | # ..and any blank lines following them. 44 | state == 1 && /^ / { next; } 45 | 46 | # Template Haskell marker 47 | /^return / { 48 | exit; 49 | } 50 | 51 | { state = 0; print; } 52 | ' 53 | } 54 | 55 | 56 | 57 | if [[ ! -e 'ShellCheck.cabal' ]] 58 | then 59 | echo "Run me from the ShellCheck directory." >&2 60 | exit 1 61 | fi 62 | 63 | if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1 64 | then 65 | echo "You have local changes! These may be overwritten." >&2 66 | exit 2 67 | fi 68 | 69 | modify 'ShellCheck.cabal' sed -e ' 70 | /QuickCheck/d 71 | /^test-suite/{ s/.*//; q; } 72 | ' 73 | 74 | find . -name '.git' -prune -o -type f -name '*.hs' -print | 75 | while IFS= read -r file 76 | do 77 | modify "$file" detestify 78 | done 79 | -------------------------------------------------------------------------------- /test/buildtest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script configures, builds and runs tests. 3 | # It's meant for automatic cross-distro testing. 4 | 5 | die() { echo "$*" >&2; exit 1; } 6 | 7 | [ -e "ShellCheck.cabal" ] || 8 | die "ShellCheck.cabal not in current dir" 9 | command -v cabal || 10 | die "cabal is missing" 11 | 12 | cabal update || 13 | die "can't update" 14 | 15 | if [ -e /etc/arch-release ] 16 | then 17 | # Arch has an unconventional packaging setup 18 | flags=(--disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic) 19 | else 20 | flags=() 21 | fi 22 | 23 | cabal install --dependencies-only --enable-tests "${flags[@]}" || 24 | cabal install --dependencies-only "${flags[@]}" || 25 | cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" || 26 | die "can't install dependencies" 27 | cabal configure --enable-tests "${flags[@]}" || 28 | die "configure failed" 29 | cabal build || 30 | die "build failed" 31 | cabal test || 32 | die "test failed" 33 | cabal haddock || 34 | die "haddock failed" 35 | 36 | sc="$(find . -name shellcheck -type f -perm -111)" 37 | [ -x "$sc" ] || die "Can't find executable" 38 | 39 | "$sc" - << 'EOF' || die "execution failed" 40 | #!/bin/sh 41 | echo "Hello World" 42 | EOF 43 | 44 | "$sc" - << 'EOF' && die "negative execution failed" 45 | #!/bin/sh 46 | echo $1 47 | EOF 48 | 49 | 50 | echo "Success" 51 | exit 0 52 | -------------------------------------------------------------------------------- /test/check_release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2257 3 | 4 | failed=0 5 | fail() { 6 | echo "$(tput setaf 1)$*$(tput sgr0)" 7 | failed=1 8 | } 9 | 10 | if git diff | grep -q "" 11 | then 12 | fail "There are uncommitted changes" 13 | fi 14 | 15 | version=${current#v} 16 | if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version" 17 | then 18 | fail "The cabal file does not match tag version $version" 19 | fi 20 | 21 | if ! grep -qF "## $current" CHANGELOG.md 22 | then 23 | fail "CHANGELOG.md does not contain '## $current'" 24 | fi 25 | 26 | current=$(git tag --points-at) 27 | if [[ -z "$current" ]] 28 | then 29 | fail "No git tag on the current commit" 30 | echo "Create one with: git tag -a v0.0.0" 31 | fi 32 | 33 | if [[ "$current" != v* ]] 34 | then 35 | fail "Bad tag format: expected v0.0.0" 36 | fi 37 | 38 | if [[ "$(git cat-file -t "$current")" != "tag" ]] 39 | then 40 | fail "Current tag is not annotated (required for Snap)." 41 | fi 42 | 43 | if [[ "$(git tag --points-at master)" != "$current" ]] 44 | then 45 | fail "You are not on master" 46 | fi 47 | 48 | if [[ $(git log -1 --pretty=%B) != "Stable version "* ]] 49 | then 50 | fail "Expected git log message to be 'Stable version ...'" 51 | fi 52 | 53 | if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]] 54 | then 55 | fail "Expected git log message to contain CHANGELOG" 56 | fi 57 | 58 | i=1 j=1 59 | cat << EOF 60 | 61 | Manual Checklist 62 | 63 | $((i++)). Make sure none of the automated checks above failed 64 | $((i++)). Run \`build/build_builder build/*/\` to update all builder images. 65 | $((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` to verify that they work. 66 | $((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them. 67 | $((i++)). Run test/distrotest to ensure that most distros can build OOTB. 68 | $((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions 69 | $((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman 70 | $((i++)). Format and read over the manual for bad formatting and outdated info. 71 | $((i++)). Make sure the Hackage package builds locally. 72 | 73 | Release Steps 74 | 75 | $((j++)). \`cabal sdist\` to generate a Hackage package 76 | $((j++)). \`git push --follow-tags\` to push commit 77 | $((j++)). Wait for GitHub Actions to build. 78 | $((j++)). Verify release: 79 | a. Check that the new versions are uploaded: https://github.com/koalaman/shellcheck/tags 80 | b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman 81 | $((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload 82 | $((j++)). Push a new commit that updates CHANGELOG.md 83 | EOF 84 | exit "$failed" 85 | -------------------------------------------------------------------------------- /test/distrotest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script runs 'buildtest' on each of several distros 3 | # via Docker. 4 | set -o pipefail 5 | 6 | exec 3>&1 4>&2 7 | die() { echo "$*" >&4; exit 1; } 8 | 9 | [ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir" 10 | 11 | [ "$1" = "--run" ] || { 12 | cat << EOF 13 | This script pulls multiple distros via Docker and compiles 14 | ShellCheck and dependencies for each one. It takes hours, 15 | and is still highly experimental. 16 | 17 | Make sure you're plugged in and have screen/tmux in place, 18 | then re-run with $0 --run to continue. 19 | 20 | Also note that dist*/ and .stack-work/ will be deleted. 21 | EOF 22 | exit 0 23 | } 24 | 25 | echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..." 26 | rm -rf dist dist-newstyle .stack-work 27 | 28 | execs=$(find . -name shellcheck) 29 | 30 | if [ -n "$execs" ] 31 | then 32 | die "Found unexpected executables. Remove and try again: $execs" 33 | fi 34 | 35 | log=$(mktemp) || die "Can't create temp file" 36 | date >> "$log" || die "Can't write to log" 37 | 38 | echo "Logging to $log" >&3 39 | exec >> "$log" 2>&1 40 | 41 | final=0 42 | while read -r distro setup 43 | do 44 | [[ "$distro" = "#"* || -z "$distro" ]] && continue 45 | printf '%s ' "$distro" >&3 46 | docker pull "$distro" || die "Can't pull $distro" 47 | printf 'pulled. ' >&3 48 | 49 | tmp=$(mktemp -d) || die "Can't make temp dir" 50 | cp -r . "$tmp/" || die "Can't populate test dir" 51 | printf 'Result: ' >&3 52 | < /dev/null docker run -v "$tmp:/mnt" "$distro" sh -c " 53 | $setup 54 | cd /mnt || exit 1 55 | test/buildtest 56 | " 57 | ret=$? 58 | if [ "$ret" = 0 ] 59 | then 60 | echo "OK" >&3 61 | else 62 | echo "FAIL with $ret. See $log" >&3 63 | final=1 64 | fi 65 | rm -rf "$tmp" 66 | done << EOF 67 | # Docker tag Setup command 68 | debian:stable apt-get update && apt-get install -y cabal-install 69 | debian:testing apt-get update && apt-get install -y cabal-install 70 | ubuntu:latest apt-get update && apt-get install -y cabal-install 71 | haskell:latest true 72 | opensuse/leap:latest zypper install -y cabal-install ghc 73 | fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++ 74 | archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel 75 | 76 | # Ubuntu LTS 77 | ubuntu:24.04 apt-get update && apt-get install -y cabal-install 78 | ubuntu:22.04 apt-get update && apt-get install -y cabal-install 79 | ubuntu:20.04 apt-get update && apt-get install -y cabal-install 80 | 81 | # Stack on Ubuntu LTS 82 | 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 83 | EOF 84 | 85 | exit "$final" 86 | -------------------------------------------------------------------------------- /test/shellcheck.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Control.Monad 4 | import System.Exit 5 | import qualified ShellCheck.Analytics 6 | import qualified ShellCheck.AnalyzerLib 7 | import qualified ShellCheck.ASTLib 8 | import qualified ShellCheck.CFG 9 | import qualified ShellCheck.CFGAnalysis 10 | import qualified ShellCheck.Checker 11 | import qualified ShellCheck.Checks.Commands 12 | import qualified ShellCheck.Checks.ControlFlow 13 | import qualified ShellCheck.Checks.Custom 14 | import qualified ShellCheck.Checks.ShellSupport 15 | import qualified ShellCheck.Fixer 16 | import qualified ShellCheck.Formatter.Diff 17 | import qualified ShellCheck.Parser 18 | 19 | main = do 20 | putStrLn "Running ShellCheck tests..." 21 | failures <- filter (not . snd) <$> mapM sequenceA tests 22 | if null failures then exitSuccess else do 23 | putStrLn "Tests failed for the following module(s):" 24 | mapM (putStrLn . ("- ShellCheck." ++) . fst) failures 25 | exitFailure 26 | where 27 | tests = 28 | [ ("Analytics" , ShellCheck.Analytics.runTests) 29 | , ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests) 30 | , ("ASTLib" , ShellCheck.ASTLib.runTests) 31 | , ("CFG" , ShellCheck.CFG.runTests) 32 | , ("CFGAnalysis" , ShellCheck.CFGAnalysis.runTests) 33 | , ("Checker" , ShellCheck.Checker.runTests) 34 | , ("Checks.Commands" , ShellCheck.Checks.Commands.runTests) 35 | , ("Checks.ControlFlow" , ShellCheck.Checks.ControlFlow.runTests) 36 | , ("Checks.Custom" , ShellCheck.Checks.Custom.runTests) 37 | , ("Checks.ShellSupport", ShellCheck.Checks.ShellSupport.runTests) 38 | , ("Fixer" , ShellCheck.Fixer.runTests) 39 | , ("Formatter.Diff" , ShellCheck.Formatter.Diff.runTests) 40 | , ("Parser" , ShellCheck.Parser.runTests) 41 | ] 42 | -------------------------------------------------------------------------------- /test/stacktest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script builds ShellCheck through `stack` using 3 | # various resolvers. It's run via distrotest. 4 | 5 | resolvers=( 6 | # nightly-"$(date -d "3 days ago" +"%Y-%m-%d")" 7 | ) 8 | 9 | die() { echo "$*" >&2; exit 1; } 10 | 11 | [ -e "ShellCheck.cabal" ] || 12 | die "ShellCheck.cabal not in current dir" 13 | [ -e "stack.yaml" ] || 14 | die "stack.yaml not in current dir" 15 | command -v stack || 16 | die "stack is missing" 17 | 18 | stack setup --allow-different-user || die "Failed to setup with default resolver" 19 | stack build --test || die "Failed to build/test with default resolver" 20 | 21 | # Nice to haves, but not necessary 22 | for resolver in "${resolvers[@]}" 23 | do 24 | stack --resolver="$resolver" setup || die "Failed to setup $resolver. This probably doesn't matter." 25 | stack --resolver="$resolver" build --test || die "Failed build/test with $resolver! This probably doesn't matter." 26 | done 27 | 28 | echo "Success" 29 | --------------------------------------------------------------------------------