diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 39d8893..0000000 --- a/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -* -!LICENSE -!Setup.hs -!ShellCheck.cabal -!shellcheck.hs -!src diff --git a/.ghci b/.ghci deleted file mode 100644 index f20fa67..0000000 --- a/.ghci +++ /dev/null @@ -1 +0,0 @@ -:set -idist/build/autogen -isrc diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 493b465..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,27 +0,0 @@ -#### For bugs -- Rule Id (if any, e.g. SC1000): -- My shellcheck version (`shellcheck --version` or "online"): -- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086) -- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit - -#### For new checks and feature suggestions -- [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this -- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related - - -#### Here's a snippet or screenshot that shows the problem: - -```sh - -#!/your/interpreter -your script here - -``` - -#### Here's what shellcheck currently says: - - - -#### Here's what I wanted or expected to see: - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 11b90d8..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Bug report -about: Create a new bug report -title: '' -labels: '' -assignees: '' - ---- - -#### For bugs with existing features - -- Rule Id (if any, e.g. SC1000): -- My shellcheck version (`shellcheck --version` or "online"): -- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086) -- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit - -#### Here's a snippet or screenshot that shows the problem: - -```sh -#!/bin/sh -your script here -``` - -#### Here's what shellcheck currently says: - - - -#### Here's what I wanted or expected to see: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 7184769..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -#### For new checks and feature suggestions -- [ ] https://www.shellcheck.net/ (i.e. the latest commit) currently gives no useful warnings about this -- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related - -#### Here's a snippet or screenshot that shows a potential problem: - -```sh -#!/bin/sh -your script here -``` - -#### Here's what shellcheck currently says: - - - -#### Here's what I wanted to see: diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 81bae9a..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index cf5d334..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,159 +0,0 @@ -name: Build ShellCheck - -# Run this workflow every time a new commit pushed to your repository -on: push - -jobs: - package_source: - name: Package Source Code - runs-on: ubuntu-latest - steps: - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-mark manual ghc # Don't bother installing ghc just to tar up source - sudo apt-get install cabal-install - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Deduce tags - run: | - mkdir source - echo "latest" > source/tags - if tag=$(git describe --exact-match --tags) - then - echo "stable" >> source/tags - echo "$tag" >> source/tags - fi - cat source/tags - - - name: Package Source - run: | - grep "stable" source/tags || ./setgitversion - cabal sdist - mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: source - path: source/ - - run_tests: - name: Run tests - needs: package_source - runs-on: ubuntu-latest - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - - - name: Install dependencies - run: | - sudo apt-get update && sudo apt-get install ghc cabal-install - cabal update - - - name: Unpack source - run: | - cd source - tar xvf source.tar.gz --strip-components=1 - - - name: Build and run tests - run: | - cd source - cabal test - - build_source: - name: Build - needs: package_source - strategy: - matrix: - build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64] - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download artifacts - uses: actions/download-artifact@v4 - - - name: Build source - run: | - mkdir -p bin - mkdir -p bin/${{matrix.build}} - ( cd bin && ../builders/run_builder ../source/source.tar.gz ../builders/${{matrix.build}} ) - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: ${{matrix.build}}.bin - path: bin/ - - package_binary: - name: Package Binaries - needs: build_source - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download artifacts - uses: actions/download-artifact@v4 - - - name: Work around GitHub permissions bug - run: chmod +x *.bin/*/shellcheck* - - - name: Package binaries - run: | - export TAGS="$(cat source/tags)" - mkdir -p deploy - cp -r *.bin/* deploy - cd deploy - ../.prepare_deploy - rm -rf */ README* LICENSE* - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: deploy - path: deploy/ - - deploy: - name: Deploy binaries - needs: package_binary - runs-on: ubuntu-latest - environment: Deploy - steps: - - name: Install Dependencies - run: | - sudo apt-get update - sudo apt-get install hub - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download artifacts - uses: actions/download-artifact@v4 - - - name: Upload to GitHub - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - export TAGS="$(cat source/tags)" - ./.github_deploy - - - name: Waiting for GitHub to replicate uploaded releases - run: | - sleep 300 - - - name: Upload to Docker Hub - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_EMAIL: ${{ secrets.DOCKER_EMAIL }} - DOCKER_BASE: ${{ secrets.DOCKER_USERNAME }}/shellcheck - run: | - export TAGS="$(cat source/tags)" - ( source ./.multi_arch_docker && set -eux && multi_arch_docker::main ) diff --git a/.github_deploy b/.github_deploy deleted file mode 100755 index 82c8ec5..0000000 --- a/.github_deploy +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -x -shopt -s extglob - -export EDITOR="touch" - -# Sanity check -gh --version || exit 1 -hub release show latest || exit 1 - -for tag in $TAGS -do - if ! hub release show "$tag" - then - echo "Creating new release $tag" - git show --no-patch --format='format:%B' > description - hub release create -F description "$tag" - fi - - files=() - for file in deploy/* - do - [[ $file == *.@(xz|gz|zip) ]] || continue - [[ $file == *"$tag"* ]] || continue - files+=("$file") - done - gh release upload "$tag" "${files[@]}" --clobber || exit 1 -done diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 050e7f1..0000000 --- a/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Created by https://www.gitignore.io - -### Haskell ### -dist -cabal-dev -*.o -*.hi -*.chi -*.chs.h -.virtualenv -.hsenv -.cabal-sandbox/ -cabal.sandbox.config -cabal.config -cabal.project.freeze -.stack-work - -### Snap ### -/snap/.snapcraft/ -/stage/ -/parts/ -/prime/ -*.snap -/dist-newstyle/ diff --git a/.multi_arch_docker b/.multi_arch_docker deleted file mode 100755 index 81048a2..0000000 --- a/.multi_arch_docker +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash -# This script builds and deploys multi-architecture docker images from the -# binaries previously built and deployed to GitHub. - -function multi_arch_docker::install_docker_buildx() { - # Install QEMU multi-architecture support for docker buildx. - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - - # Instantiate docker buildx builder with multi-architecture support. - docker buildx create --name mybuilder - docker buildx use mybuilder - # Start up buildx and verify that all is OK. - docker buildx inspect --bootstrap -} - -# Log in to Docker Hub for deployment. -function multi_arch_docker::login_to_docker_hub() { - echo "$DOCKER_PASSWORD" | docker login -u="$DOCKER_USERNAME" --password-stdin -} - -# Run buildx build and push. Passed in arguments augment the command line. -function multi_arch_docker::buildx() { - mkdir -p /tmp/empty - docker buildx build \ - --platform "${DOCKER_PLATFORMS// /,}" \ - --push \ - --progress plain \ - -f Dockerfile.multi-arch \ - "$@" \ - /tmp/empty - rmdir /tmp/empty -} - -# Build and push plain and alpine docker images for all tags. -function multi_arch_docker::build_and_push_all() { - for tag in $TAGS; do - multi_arch_docker::buildx -t "$DOCKER_BASE:$tag" --build-arg "tag=$tag" - multi_arch_docker::buildx -t "$DOCKER_BASE-alpine:$tag" \ - --build-arg "tag=$tag" --target alpine - done -} - -# Test all pushed docker images. -function multi_arch_docker::test_all() { - printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript - - for platform in $DOCKER_PLATFORMS; do - for tag in $TAGS; do - for ext in '-alpine' ''; do - image="${DOCKER_BASE}${ext}:${tag}" - msg="Testing docker image $image on platform $platform" - line="${msg//?/=}" - printf '\n%s\n%s\n%s\n' "${line}" "${msg}" "${line}" - docker pull -q --platform "$platform" "$image" - if [ -n "$ext" ]; then - echo -n "Image architecture: " - docker run --rm --entrypoint /bin/sh "$image" -c 'uname -m' - version=$(docker run --rm "$image" shellcheck --version \ - | grep 'version:') - else - version=$(docker run --rm "$image" --version | grep 'version:') - fi - version=${version/#version: /v} - echo "shellcheck version: $version" - if [[ ! ("$tag" =~ ^(latest|stable)$) && "$tag" != "$version" ]]; then - echo "Version mismatch: shellcheck $version tagged as $tag" - exit 1 - fi - if [ -n "$ext" ]; then - docker run --rm -v "$PWD:/mnt" -w /mnt "$image" shellcheck myscript - else - docker run --rm -v "$PWD:/mnt" "$image" myscript - fi - done - done - done -} - -function multi_arch_docker::main() { - export DOCKER_PLATFORMS='linux/amd64' - DOCKER_PLATFORMS+=' linux/arm64' - DOCKER_PLATFORMS+=' linux/arm/v6' - DOCKER_PLATFORMS+=' linux/riscv64' - - multi_arch_docker::install_docker_buildx - multi_arch_docker::login_to_docker_hub - multi_arch_docker::build_and_push_all - multi_arch_docker::test_all -} diff --git a/.prepare_deploy b/.prepare_deploy deleted file mode 100755 index 9f39912..0000000 --- a/.prepare_deploy +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# This script packages up compiled binaries -set -ex -shopt -s nullglob extglob - -ls -l - -cp ../LICENSE LICENSE.txt -sed -e $'s/$/\r/' > README.txt << END -This is a precompiled ShellCheck binary. - https://www.shellcheck.net/ - -ShellCheck is a static analysis tool for shell scripts. -It's licensed under the GNU General Public License v3.0. -Information and source code is available on the website. - -This binary was compiled on $(date -u). - - - - ====== Latest commits ====== - -$(git log -n 3) -END - -for dir in */ -do - cp LICENSE.txt README.txt "$dir" -done - -echo "Tags are $TAGS" - -for tag in $TAGS -do - - for dir in windows.*/ - do - ( cd "$dir" && zip "../shellcheck-$tag.zip" * ) - done - - for dir in {linux,darwin}.*/ - do - base="${dir%/}" - ( cd "$dir" && tar -cJf "../shellcheck-$tag.$base.tar.xz" --transform="s:^:shellcheck-$tag/:" * ) - done -done - -for file in ./* -do - [[ -f "$file" ]] || continue - sha512sum "$file" > "$file.sha512sum" -done - -ls -l diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 2ac9ef6..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,566 +0,0 @@ -## Git -### Added - -### Changed - -### Fixed - -### Removed - - -## v0.11.0 - 2025-08-03 -### Added -- SC2327/SC2328: Warn about capturing the output of redirected commands. -- SC2329: Warn when (non-escaping) functions are never invoked. -- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox. -- SC2331: Suggest using standard -e instead of unary -a in tests. -- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash. -- SC3062: Warn about bashism `[ -o opt ]`. -- Optional `avoid-negated-conditions`: suggest replacing `[ ! a -eq b ]` - with `[ a -ne b ]`, and similar for -ge/-lt/=/!=/etc (SC2335). -- Precompiled binaries for Linux riscv64 (linux.riscv64) - -### Changed -- SC2002 about Useless Use Of Cat is now disabled by default. It can be - re-enabled with `--enable=useless-use-of-cat` or equivalent directive. -- SC2236/SC2237 about replacing `[ ! -n .. ]` with `[ -z ]` and vice versa - is now optional under `avoid-negated-conditions`. -- SC2015 about `A && B || C` no longer triggers when B is a test command. -- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024 -- Diff output now uses / as path separator on Windows - -### Fixed -- SC2218 about function use-before-define is now more accurate. -- SC2317 about unreachable commands is now less spammy for nested ones. -- SC2292, optional suggestion for [[ ]], now triggers for Busybox. -- Updates for Bash 5.3, including `${| cmd; }` and `source -p` - -### Removed -- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024 - - -## v0.10.0 - 2024-03-07 -### Added -- Precompiled binaries for macOS ARM64 (darwin.aarch64) -- Added support for busybox sh -- Added flag --rcfile to specify an rc file by name. -- Added `extended-analysis=true` directive to enable/disable dataflow analysis - (with a corresponding --extended-analysis flag). -- SC2324: Warn when x+=1 appends instead of increments -- SC2325: Warn about multiple `!`s in dash/sh. -- SC2326: Warn about `foo | ! bar` in bash/dash/sh. -- SC3012: Warn about lexicographic-compare bashism in test like in [ ] -- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ] -- SC3014: Warn bashism `test _ == _` like in [ ] -- SC3015: Warn bashism `test _ =~ _` like in [ ] -- SC3016: Warn bashism `test -v _` like in [ ] -- SC3017: Warn bashism `test -a _` like in [ ] - -### Fixed -- source statements with here docs now work correctly -- "(Array.!): undefined array element" error should no longer occur - - -## v0.9.0 - 2022-12-12 -### Added -- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!) -- SC2317: Warn about unreachable commands -- SC2318: Warn about backreferences in 'declare x=1 y=$x' -- SC2319/SC2320: Warn when $? refers to echo/printf/[ ]/[[ ]]/test -- SC2321: Suggest removing $((..)) in array[$((idx))]=val -- SC2322: Suggest collapsing double parentheses in arithmetic contexts -- SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val - -### Fixed -- SC2086: Now uses DFA to make more accurate predictions about values -- SC2086: No longer warns about values declared as integer with declare -i - -### Changed -- ShellCheck now has a Data Flow Analysis engine to make smarter decisions - based on control flow rather than just syntax. Existing checks will - gradually start using it, which may cause them to trigger differently - (but more accurately). -- Values in directives/shellcheckrc can now be quoted with '' or "" - - -## v0.8.0 - 2021-11-06 -### Added -- `disable=all` now conveniently disables all warnings -- `external-sources=true` directive can be added to .shellcheckrc to make - shellcheck behave as if `-x` was specified. -- Optional `check-extra-masked-returns` for pointing out commands with - suppressed exit codes (SC2312). -- Optional `require-double-brackets` for recommending \[\[ ]] (SC2292). -- SC2286-SC2288: Warn when command name ends in a symbol like `/.)'"` -- SC2289: Warn when command name contains tabs or linefeeds -- SC2291: Warn about repeated unquoted spaces between words in echo -- SC2292: Suggest [[ over [ in Bash/Ksh scripts (optional) -- SC2293/SC2294: Warn when calling `eval` with arrays -- SC2295: Warn about "${x#$y}" treating $y as a pattern when not quoted -- SC2296-SC2301: Improved warnings for bad parameter expansions -- SC2302/SC2303: Warn about loops over array values when using them as keys -- SC2304-SC2306: Warn about unquoted globs in expr arguments -- SC2307: Warn about insufficient number of arguments to expr -- SC2308: Suggest other approaches for non-standard expr extensions -- SC2313: Warn about `read` with unquoted, array indexed variable - -### Fixed -- SC2102 about repetitions in ranges no longer triggers on [[ -v arr[xx] ]] -- SC2155 now recognizes `typeset` and local read-only `declare` statements -- SC2181 now tries to avoid triggering for error handling functions -- SC2290: Warn about misused = in declare & co, which were not caught by SC2270+ -- The flag --color=auto no longer outputs color when TERM is "dumb" or unset - -### Changed -- SC2048: Warning about $\* now also applies to ${array[\*]} -- SC2181 now only triggers on single condition tests like `[ $? = 0 ]`. -- Quote warnings are now emitted for declaration utilities in sh -- Leading `_` can now be used to suppress warnings about unused variables -- TTY output now includes warning level in text as well as color - -### Removed -- SC1004: Literal backslash+linefeed in '' was found to be usually correct - - -## v0.7.2 - 2021-04-19 -### Added -- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000` -- SC1143: Warn about line continuations in comments -- SC2259/SC2260: Warn when redirections override pipes -- SC2261: Warn about multiple competing redirections -- SC2262/SC2263: Warn about aliases declared and used in the same parsing unit -- SC2264: Warn about wrapper functions that blatantly recurse -- SC2265/SC2266: Warn when using & or | with test statements -- SC2267: Warn when using xargs -i instead of -I -- SC2268: Warn about unnecessary x-comparisons like `[ x$var = xval ]` - -### Fixed -- SC1072/SC1073 now respond to disable annotations, though ignoring parse errors - is still purely cosmetic and does not allow ShellCheck to continue. -- Improved error reporting for trailing tokens after ]/]] and compound commands -- `#!/usr/bin/env -S shell` is now handled correctly -- Here docs with \r are now parsed correctly and give better warnings - -### Changed -- Assignments are now parsed to spec, without leniency for leading $ or spaces -- POSIX/dash unsupported feature warnings now have individual SC3xxx codes -- SC1090: A leading `$x/` or `$(x)/` is now treated as `./` when locating files -- SC2154: Variables appearing in -z/-n tests are no longer considered unassigned -- SC2270-SC2285: Improved warnings about misused `=`, e.g. `${var}=42` - - -## v0.7.1 - 2020-04-04 -### Fixed -- `-f diff` no longer claims that it found more issues when it didn't -- Known empty variables now correctly trigger SC2086 -- ShellCheck should now be compatible with Cabal 3 -- SC2154 and all command-specific checks now trigger for builtins - called with `builtin` - -### Added -- SC1136: Warn about unexpected characters after ]/]] -- SC2254: Suggest quoting expansions in case statements -- SC2255: Suggest using `$((..))` in `[ 2*3 -eq 6 ]` -- SC2256: Warn about translated strings that are known variables -- SC2257: Warn about arithmetic mutation in redirections -- SC2258: Warn about trailing commas in for loop elements - -### Changed -- SC2230: 'command -v' suggestion is now off by default (-i deprecate-which) -- SC1081: Keywords are now correctly parsed case sensitively, with a warning - - -## v0.7.0 - 2019-07-28 -### Added -- Precompiled binaries for macOS and Linux aarch64 -- Preliminary support for fix suggestions -- New `-f diff` unified diff format for auto-fixes -- Files containing Bats tests can now be checked -- Directory wide directives can now be placed in a `.shellcheckrc` -- Optional checks: Use `--list-optional` to show a list of tests, - Enable with `-o` flags or `enable=name` directives -- Source paths: Use `-P dir1:dir2` or a `source-path=dir1` directive - to specify search paths for sourced files. -- json1 format like --format=json but treats tabs as single characters -- Recognize FLAGS variables created by the shflags library. -- Site-specific changes can now be made in Custom.hs for ease of patching -- SC2154: Also warn about unassigned uppercase variables (optional) -- SC2252: Warn about `[ $a != x ] || [ $a != y ]`, similar to SC2055 -- SC2251: Inform about ineffectual ! in front of commands -- SC2250: Warn about variable references without braces (optional) -- SC2249: Warn about `case` with missing default case (optional) -- SC2248: Warn about unquoted variables without special chars (optional) -- SC2247: Warn about $"(cmd)" and $"{var}" -- SC2246: Warn if a shebang's interpreter ends with / -- SC2245: Warn that Ksh ignores all but the first glob result in `[` -- SC2243/SC2244: Suggest using explicit -n for `[ $foo ]` (optional) -- SC1135: Suggest not ending double quotes just to make $ literal - -### Changed -- If a directive or shebang is not specified, a `.bash/.bats/.dash/.ksh` - extension will be used to infer the shell type when present. -- Disabling SC2120 on a function now disables SC2119 on call sites - -### Fixed -- SC2183 no longer warns about missing printf args for `%()T` - -## v0.6.0 - 2018-12-02 -### Added -- Command line option --severity/-S for filtering by minimum severity -- Command line option --wiki-link-count/-W for showing wiki links -- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"` -- SC2236/SC2237: Suggest -n/-z instead of ! -z/-n -- SC2238: Warn when redirecting to a known command name, e.g. ls > rm -- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh -- SC2240: Warn when passing additional arguments to dot (.) in sh/dash -- SC1133: Better diagnostics when starting a line with |/||/&& - -### Changed -- Most warnings now have useful end positions -- SC1117 about unknown double-quoted escape sequences has been retired - -### Fixed -- SC2021 no longer triggers for equivalence classes like `[=e=]` -- SC2221/SC2222 no longer mistriggers on fall-through case branches -- SC2081 about glob matches in `[ .. ]` now also triggers for `!=` -- SC2086 no longer warns about spaces in `$#` -- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..` -- `read -a` is now correctly considered an array assignment -- SC2039 no longer warns about LINENO now that it's POSIX - -## v0.5.0 - 2018-05-31 -### Added -- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests -- SC2232: Warn about invalid arguments to sudo -- SC2231: Suggest quoting expansions in for loop globs -- SC2229: Warn about 'read $var' -- SC2227: Warn about redirections in the middle of 'find' commands -- SC2224/SC2225/SC2226: Warn when using mv/cp/ln without a destination -- SC2223: Quote warning specific to `: ${var=value}` -- SC1131: Warn when using `elseif` or `elsif` -- SC1128: Warn about blanks/comments before shebang -- SC1127: Warn about C-style comments - -### Fixed -- Annotations intended for a command's here documents now work -- Escaped characters inside groups in =~ regexes now parse -- Associative arrays are now respected in arithmetic contexts -- SC1087 about `$var[@]` now correctly triggers on any index -- Bad expansions in here documents are no longer ignored -- FD move operations like {fd}>1- now parse correctly - -### Changed -- Here docs are now terminated as per spec, rather than by presumed intent -- SC1073: 'else if' is now parsed correctly and not like 'elif' -- SC2163: 'export $name' can now be silenced with 'export ${name?}' -- SC2183: Now warns when printf arg count is not a multiple of format count - -## v0.4.7 - 2017-12-08 -### Added -- Statically linked binaries for Linux and Windows (see README.md)! -- `-a` flag to also include warnings in `source`d files -- SC2221/SC2222: Warn about overridden case branches -- SC2220: Warn about unhandled error cases in getopt loops -- SC2218: Warn when using functions before they're defined -- SC2216/SC2217: Warn when piping/redirecting to mv/cp and other non-readers -- SC2215: Warn about commands starting with leading dash -- SC2214: Warn about superfluous getopt flags -- SC2213: Warn about unhandled getopt flags -- SC2212: Suggest `false` over `[ ]` -- SC2211: Warn when using a glob as a command name -- SC2210: Warn when redirecting to an integer, e.g. `foo 1>2` -- SC2206/SC2207: Suggest alternatives when using word splitting in arrays -- SC1117: Warn about double quoted, undefined backslash sequences -- SC1113/SC1114/SC1115: Recognized more malformed shebangs - -### Fixed -- `[ -v foo ]` no longer warns if `foo` is undefined -- SC2037 is now suppressed by quotes, e.g. `PAGER="cat" man foo` -- Ksh nested array declarations now parse correctly -- Parameter Expansion without colons are now recognized, e.g. `${foo+bar}` -- The `lastpipe` option is now respected with regard to subshell warnings -- `\(` is now respected for grouping in `[` -- Leading `\` is now ignored for commands, to allow alias suppression -- Comments are now allowed after directives to e.g. explain 'disable' - - -## v0.4.6 - 2017-03-26 -### Added -- SC2204/SC2205: Warn about `( -z foo )` and `( foo -eq bar )` -- SC2200/SC2201: Warn about brace expansion in [/[[ -- SC2198/SC2199: Warn about arrays in [/[[ -- SC2196/SC2197: Warn about deprecated egrep/fgrep -- SC2195: Warn about unmatchable case branches -- SC2194: Warn about constant 'case' statements -- SC2193: Warn about `[[ file.png == *.mp3 ]]` and other unmatchables -- SC2188/SC2189: Warn about redirections without commands -- SC2186: Warn about deprecated `tempfile` -- SC1109: Warn when finding `&`/`>`/`<` unquoted -- SC1108: Warn about missing spaces in `[ var= foo ]` - -### Changed -- All files are now read as UTF-8 with lenient latin1 fallback, ignoring locale -- Unicode quotes are no longer considered syntactic quotes -- `ash` scripts will now be checked as `dash` with a warning - -### Fixed -- `-c` no longer suggested when using `grep -o | wc` -- Comments and whitespace are now allowed before filewide directives -- Here doc delimiters with esoteric quoting like `foo""` are now handled -- SC2095 about `ssh` in while read loops is now suppressed when using `-n` -- `%(%Y%M%D)T` now recognized as a single formatter in `printf` checks -- `grep -F` now suppresses regex related suggestions -- Command name checks now recognize busybox applet names - - -## v0.4.5 - 2016-10-21 -### Added -- A Docker build (thanks, kpankonen!) -- SC2185: Suggest explicitly adding path for `find` -- SC2184: Warn about unsetting globs (e.g. `unset foo[1]`) -- SC2183: Warn about `printf` with more formatters than variables -- SC2182: Warn about ignored arguments with `printf` -- SC2181: Suggest using command directly instead of `if [ $? -eq 0 ]` -- SC1106: Warn when using `test` operators in `(( 1 -eq 2 ))` - -### Changed -- Unrecognized directives now causes a warning rather than parse failure. - -### Fixed -- Indices in associative arrays are now parsed correctly -- Missing shebang warning squashed when specifying with a directive -- Ksh multidimensional arrays are now supported -- Variables in substring ${a:x:y} expansions now count as referenced -- SC1102 now also handles ambiguous `$((` -- Using `$(seq ..)` will no longer suggest quoting -- SC2148 (missing shebang) is now suppressed when using shell directives -- `[ a '>' b ]` is now recognized as being correctly escaped - - -## v0.4.4 - 2016-05-15 -### Added -- Haskell Stack support (thanks, Arguggi!) -- SC2179/SC2178: Warn when assigning/appending strings to arrays -- SC1102: Warn about ambiguous `$(((` -- SC1101: Warn when \\ linebreaks have trailing spaces - -### Changed -- Directives directly after the shebang now apply to the entire file - -### Fixed -- `{$i..10}` is now flagged similar to `{1..$i}` - - -## v0.4.3 - 2016-01-13 -### Fixed -- Build now works on GHC 7.6.3 as found on Debian Stable/Ubuntu LTS - - -## v0.4.2 - 2016-01-09 -### Added -- First class support for the `dash` shell -- The `--color` flag similar to ls/grep's (thanks, haguenau!) -- SC2174: Warn about unexpected behavior of `mkdir -pm` (thanks, eatnumber1!) -- SC2172: Warn about non-portable use of signal numbers in `trap` -- SC2171: Warn about `]]` without leading `[[` -- SC2168: Warn about `local` outside functions - -### Fixed -- Warnings about unchecked `cd` will no longer trigger with `set -e` -- `[ a -nt/-ot/-ef b ]` no longer warns about being constant -- Quoted test operators like `[ foo "<" bar ]` now parse -- Escaped quotes in backticks now parse correctly - - -## v0.4.1 - 2015-09-05 -### Fixed -- Added missing files to Cabal, fixing the build - - -## v0.4.0 - 2015-09-05 -### Added -- Support for following `source`d files -- Support for setting default flags in `SHELLCHECK_OPTS` -- An `--external-sources` flag for following arbitrary `source`d files -- A `source` directive to override the filename to `source` -- SC2166: Suggest using `[ p ] && [ q ]` over `[ p -a q ]` -- SC2165: Warn when nested `for` loops use the same variable name -- SC2164: Warn when using `cd` without checking that it succeeds -- SC2163: Warn about `export $var` -- SC2162: Warn when using `read` without `-r` -- SC2157: Warn about `[ "$var " ]` and similar never-empty string matches - -### Fixed -- `cat -vnE file` and similar will no longer flag as UUOC -- Nested trinary operators in `(( ))` now parse correctly -- Ksh `${ ..; }` command expansions now parse - - -## v0.3.8 - 2015-06-20 -### Changed -- ShellCheck's license has changed from AGPLv3 to GPLv3. - -### Added -- SC2156: Warn about injecting filenames in `find -exec sh -c "{}" \;` - -### Fixed -- Variables and command substitutions in brace expansions are now parsed -- ANSI colors are now disabled on Windows -- Empty scripts now parse - - -## v0.3.7 - 2015-04-16 -### Fixed -- Build now works on GHC 7.10 -- Use `regex-tdfa` over `regex-compat` since the latter crashes on OS X. - -## v0.3.6 - 2015-03-28 -### Added -- SC2155: Warn about masked return values in `export foo=$(exit 1)` -- SC2154: Warn when a lowercase variable is referenced but not assigned -- SC2152/SC2151: Warn about bad `return` values like `1234` and `"foo"` -- SC2150: Warn about `find -exec "shell command" \;` - -### Fixed -- `coproc` is now supported -- Trinary operator now recognized in `((..))` - -### Removed -- Zsh support has been removed - - -## v0.3.5 - 2014-11-09 -### Added -- SC2148: Warn when not including a shebang -- SC2147: Warn about literal ~ in PATH -- SC1086: Warn about `$` in for loop variables, e.g. `for $i in ..` -- SC1084: Warn when the shebang uses `!#` instead of `#!` - -### Fixed -- Empty and comment-only backtick expansions now parse -- Variables used in PS1/PROMPT\_COMMAND/trap now count as referenced -- ShellCheck now skips unreadable files and directories -- `-f gcc` on empty files no longer crashes -- Variables in $".." are now considered quoted -- Warnings about expansions in single quotes now include backticks - - -## v0.3.4 - 2014-07-08 -### Added -- SC2146: Warn about precedence when combining `find -o` with actions -- SC2145: Warn when concatenating arrays and strings - -### Fixed -- Case statements now support `;&` and `;;&` -- Indices in array declarations now parse correctly -- `let` expressions now parsed as arithmetic expressions -- Escaping is now respected in here documents - -### Changed -- Completely drop Makefile in favor of Cabal (thanks rodrigosetti!) - - -## v0.3.3 - 2014-05-29 -### Added -- SC2144: Warn when using globs in `[/[[` -- SC2143: Suggesting using `grep -q` over `[ "$(.. | grep)" ]` -- SC2142: Warn when referencing positional parameters in aliases -- SC2141: Warn about suspicious IFS assignments like `IFS="\n"` -- SC2140: Warn about bad embedded quotes like `echo "var="value""` -- SC2130: Warn when using `-eq` on strings -- SC2139: Warn about define time expansions in alias definitions -- SC2129: Suggest command grouping over `a >> log; b >> log; c >> log` -- SC2128: Warn when expanding arrays without an index -- SC2126: Suggest `grep -c` over `grep|wc` -- SC2123: Warn about accidentally overriding `$PATH`, e.g. `PATH=/my/dir` -- SC1083: Warn about literal `{/}` outside of quotes -- SC1082: Warn about UTF-8 BOMs - -### Fixed -- SC2051 no longer triggers for `{1,$n}`, only `{1..$n}` -- Improved detection of single quoted `sed` variables, e.g. `sed '$s///'` -- Stop warning about single quoted variables in `PS1` and similar -- Support for Zsh short form loops, `=(..)` - -### Removed -- SC1000 about unescaped lonely `$`, e.g. `grep "^foo$"` - - -## v0.3.2 - 2014-03-22 -### Added -- SC2121: Warn about trying to `set` variables, e.g. `set var = value` -- SC2120/SC2119: Warn when a function uses `$1..` if none are ever passed -- SC2117: Warn when using `su` in interactive mode, e.g. `su foo; whoami` -- SC2116: Detect useless use of echo, e.g. `for i in $(echo $var)` -- SC2115/SC2114: Detect some catastrophic `rm -r "$empty/"` mistakes -- SC1081: Warn when capitalizing keywords like `While` -- SC1077: Warn when using acute accents instead of backticks - -### Fixed -- Shells are now properly recognized in shebangs containing flags -- Stop warning about math on decimals in ksh/zsh -- Stop warning about decimal comparisons with `=`, e.g. `[ $version = 1.2 ]` -- Parsing of `|&` -- `${a[x]}` not counting as a reference of `x` -- `(( x[0] ))` not counting as a reference of `x` - - -## v0.3.1 - 2014-02-03 -### Added -- The `-s` flag to specify shell dialect -- SC2105/SC2104: Warn about `break/continue` outside loops -- SC1076: Detect invalid `[/[[` arithmetic like `[ 1 + 2 = 3 ]` -- SC1075: Suggest using `elif` over `else if` - -### Fixed -- Don't warn when comma separating elements in brace expansions -- Improved detection of single quoted `sed` variables, e.g. `sed '$d'` -- Parsing of arithmetic for loops using `{..}` instead of `do..done` -- Don't treat the last pipeline stage as a subshell in ksh/zsh - - -## v0.3.0 - 2014-01-19 -### Added -- A man page (thanks Dridi!) -- GCC compatible error reporting (`shellcheck -f gcc`) -- CheckStyle compatible XML error reporting (`shellcheck -f checkstyle`) -- Error codes for each warning, e.g. SC1234 -- Allow disabling warnings with `# shellcheck disable=SC1234` -- Allow disabling warnings with `--exclude` -- SC2103: Suggest using subshells over `cd foo; bar; cd ..` -- SC2102: Warn about duplicates in char ranges, e.g. `[10-15]` -- SC2101: Warn about named classes not inside a char range, e.g. `[:digit:]` -- SC2100/SC2099: Warn about bad math expressions like `i=i+5` -- SC2098/SC2097: Warn about `foo=bar echo $foo` -- SC2095: Warn when using `ssh`/`ffmpeg` in `while read` loops -- Better warnings for missing here doc tokens - -### Fixed -- Don't warn when single quoting variables with `ssh/perl/eval` -- `${!var}` is now counted as a variable reference - -### Removed -- Suggestions about using parameter expansion over basename -- The `jsoncheck` binary. Use `shellcheck -f json` instead. - - -## v0.2.0 - 2013-10-27 -### Added -- Suggest `./*` instead of `*` when passing globs to commands -- Suggest `pgrep` over `ps | grep` -- Warn about unicode quotes -- Warn about assigned but unused variables -- Inform about client side expansion when using `ssh` - -### Fixed -- CLI tool now uses exit codes and stderr canonically -- Parsing of extglobs containing empty patterns -- Parsing of bash style `eval foo=(bar)` -- Parsing of expansions in here documents -- Parsing of function names containing :+- -- Don't warn about `find|xargs` when using `-print0` - - -## v0.1.0 - 2013-07-23 -### Added -- First release diff --git a/Dockerfile.multi-arch b/Dockerfile.multi-arch deleted file mode 100644 index 24ed18c..0000000 --- a/Dockerfile.multi-arch +++ /dev/null @@ -1,24 +0,0 @@ -# Alpine image -FROM alpine:latest AS alpine -LABEL maintainer="Vidar Holen " -ARG tag - -# Put the right binary for each architecture into place for the -# multi-architecture docker image. -ARG url_base="https://github.com/koalaman/shellcheck/releases/download/" -RUN set -x; \ - arch="$(uname -m)"; \ - echo "arch is $arch"; \ - if [ "${arch}" = 'armv7l' ]; then \ - arch='armv6hf'; \ - fi; \ - tar_file="${tag}/shellcheck-${tag}.linux.${arch}.tar.xz"; \ - wget "${url_base}${tar_file}" -O - | tar -C /bin --strip-components=1 -xJf - "shellcheck-${tag}/shellcheck" && \ - ls -laF /bin/shellcheck - -# ShellCheck image -FROM scratch -LABEL maintainer="Vidar Holen " -WORKDIR /mnt -COPY --from=alpine /bin/shellcheck /bin/ -ENTRYPOINT ["/bin/shellcheck"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.md b/README.md deleted file mode 100644 index 9747e76..0000000 --- a/README.md +++ /dev/null @@ -1,564 +0,0 @@ -[![Build Status](https://github.com/koalaman/shellcheck/actions/workflows/build.yml/badge.svg)](https://github.com/koalaman/shellcheck/actions/workflows/build.yml) - - -# ShellCheck - A shell script static analysis tool - -ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts: - -![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png) - -The goals of ShellCheck are - -* To point out and clarify typical beginner's syntax issues that cause a shell - to give cryptic error messages. - -* To point out and clarify typical intermediate level semantic problems that - cause a shell to behave strangely and counter-intuitively. - -* To point out subtle caveats, corner cases and pitfalls that may cause an - advanced user's otherwise working script to fail under future circumstances. - -See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify! - -## Table of Contents - -* [How to use](#how-to-use) - * [On the web](#on-the-web) - * [From your terminal](#from-your-terminal) - * [In your editor](#in-your-editor) - * [In your build or test suites](#in-your-build-or-test-suites) -* [Installing](#installing) -* [Compiling from source](#compiling-from-source) - * [Installing Cabal](#installing-cabal) - * [Compiling ShellCheck](#compiling-shellcheck) - * [Running tests](#running-tests) -* [Gallery of bad code](#gallery-of-bad-code) - * [Quoting](#quoting) - * [Conditionals](#conditionals) - * [Frequently misused commands](#frequently-misused-commands) - * [Common beginner's mistakes](#common-beginners-mistakes) - * [Style](#style) - * [Data and typing errors](#data-and-typing-errors) - * [Robustness](#robustness) - * [Portability](#portability) - * [Miscellaneous](#miscellaneous) -* [Testimonials](#testimonials) -* [Ignoring issues](#ignoring-issues) -* [Reporting bugs](#reporting-bugs) -* [Contributing](#contributing) -* [Copyright](#copyright) -* [Other Resources](#other-resources) - -## How to use - -There are a number of ways to use ShellCheck! - -### On the web - -Paste a shell script on for instant feedback. - -[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends! - -### From your terminal - -Run `shellcheck yourscript` in your terminal for instant output, as seen above. - -### In your editor - -You can see ShellCheck suggestions directly in a variety of editors. - -* Vim, through [ALE](https://github.com/w0rp/ale), [Neomake](https://github.com/neomake/neomake), or [Syntastic](https://github.com/scrooloose/syntastic): - -![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png). - -* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck): - -![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png). - -* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck). - -* Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar). - -* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck). - -* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats). - -### In your build or test suites - -While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites. -It makes canonical use of exit codes, so you can just add a `shellcheck` command as part of the process. - -For example, in a Makefile: - -```Makefile -check-scripts: - # Fail if any of these files have warnings - shellcheck myscripts/*.sh -``` - -or in a Travis CI `.travis.yml` file: - -```yaml -script: - # Fail if any of these files have warnings - - shellcheck myscripts/*.sh -``` - -Services and platforms that have ShellCheck pre-installed and ready to use: - -* [Travis CI](https://travis-ci.org/) -* [Codacy](https://www.codacy.com/) -* [Code Climate](https://codeclimate.com/) -* [Code Factor](https://www.codefactor.io/) -* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner) -* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck) -* [Github](https://github.com/features/actions) (only Linux) -* [Trunk Code Quality](https://trunk.io/code-quality) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml) -* [CodeRabbit](https://coderabbit.ai/) - -Most other services, including [GitLab](https://about.gitlab.com/), let you install -ShellCheck yourself, either through the system's package manager (see [Installing](#installing)), -or by downloading and unpacking a [binary release](#installing-a-pre-compiled-binary). - -It's a good idea to manually install a specific ShellCheck version regardless. This avoids -any surprise build breaks when a new version with new warnings is published. - -For customized filtering or reporting, ShellCheck can output simple JSON, CheckStyle compatible XML, -GCC compatible warnings as well as human readable text (with or without ANSI colors). See the -[Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation. - -## Installing - -The easiest way to install ShellCheck locally is through your package manager. - -On systems with Cabal (installs to `~/.cabal/bin`): - - cabal update - cabal install ShellCheck - -On systems with Stack (installs to `~/.local/bin`): - - stack update - stack install ShellCheck - -On Debian based distros: - - sudo apt install shellcheck - -On Arch Linux based distros: - - pacman -S shellcheck - -or get the dependency free [shellcheck-bin](https://aur.archlinux.org/packages/shellcheck-bin/) from the AUR. - -On Gentoo based distros: - - emerge --ask shellcheck - -On EPEL based distros: - - sudo yum -y install epel-release - sudo yum install ShellCheck - -On Fedora based distros: - - dnf install ShellCheck - -On FreeBSD: - - pkg install hs-ShellCheck - -On macOS (OS X) with Homebrew: - - brew install shellcheck - -Or with MacPorts: - - sudo port install shellcheck - -On OpenBSD: - - pkg_add shellcheck - -On openSUSE - - zypper in ShellCheck - -Or use OneClickInstall - - -On Solus: - - eopkg install shellcheck - -On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)): - -```cmd -C:\> choco install shellcheck -``` - -Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)): - -```cmd -C:\> winget install --id koalaman.shellcheck -``` - -Or Windows (via [scoop](http://scoop.sh)): - -```cmd -C:\> scoop install shellcheck -``` - -From [conda-forge](https://anaconda.org/conda-forge/shellcheck): - - conda install -c conda-forge shellcheck - -From Snap Store: - - snap install --channel=edge shellcheck - -From Docker Hub: - -```sh -docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript -# Or :v0.4.7 for that version, or :latest for daily builds -``` - -or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled. - -Using the [nix package manager](https://nixos.org/nix): -```sh -nix-env -iA nixpkgs.shellcheck -``` - -Using the [Flox package manager](https://flox.dev/) -```sh -flox install shellcheck -``` - -Alternatively, you can download pre-compiled binaries for the latest release here: - -* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) -* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) -* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) -* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz) -* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz) -* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) - -or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases -(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds). - -There are currently no official binaries for Apple Silicon, but third party builds are available via -[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases). - -Distro packages already come with a `man` page. If you are building from source, it can be installed with: - -```console -pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 -sudo mv shellcheck.1 /usr/share/man/man1 -``` - -### pre-commit - -To run ShellCheck via [pre-commit](https://pre-commit.com/), add the hook to your `.pre-commit-config.yaml`: - -``` -repos: -- repo: https://github.com/koalaman/shellcheck-precommit - rev: v0.7.2 - hooks: - - id: shellcheck -# args: ["--severity=warning"] # Optionally only show errors and warnings -``` - -### Travis CI - -Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. - -If you still want to do so in order to upgrade at your leisure or ensure you're -using the latest release, follow the steps below to install a binary version. - -### Installing a pre-compiled binary - -The pre-compiled binaries come in `tar.xz` files. To decompress them, make sure -`xz` is installed. -On Debian/Ubuntu/Mint, you can `apt install xz-utils`. -On Redhat/Fedora/CentOS, `yum -y install xz`. - -A simple installer may do something like: - -```bash -scversion="stable" # or "v0.4.7", or "latest" -wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${scversion?}/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv -cp "shellcheck-${scversion}/shellcheck" /usr/bin/ -shellcheck --version -``` - -## Compiling from source - -This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile. - -### Installing Cabal - -ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`). - -On macOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source. - - $ brew install cabal-install - -On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from - -Verify that `cabal` is installed and update its dependency list with - - $ cabal update - -### Compiling ShellCheck - -`git clone` this repository, and `cd` to the ShellCheck source directory to build/install: - - $ cabal install - -This will compile ShellCheck and install it to your `~/.cabal/bin` directory. - -Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`): - -```sh -export PATH="$HOME/.cabal/bin:$PATH" -``` - -Log out and in again, and verify that your PATH is set up correctly: - -```sh -$ which shellcheck -~/.cabal/bin/shellcheck -``` - -On native Windows, the `PATH` should already be set up, but the system -may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE, -make sure to use a TrueType font, not a Raster font, and set the active -codepage to UTF-8 (65001) with `chcp`: - -```cmd -chcp 65001 -``` - -In Powershell ISE, you may need to additionally update the output encoding: - -```powershell -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -``` - -### Running tests - -To run the unit test suite: - - $ cabal test - -## Gallery of bad code - -So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues. - -### Quoting - -ShellCheck can recognize several types of incorrect quoting: - -```sh -echo $1 # Unquoted variables -find . -name *.ogg # Unquoted find/grep patterns -rm "~/my file.txt" # Quoted tilde expansion -v='--verbose="true"'; cmd $v # Literal quotes in variables -for f in "*.ogg" # Incorrectly quoted 'for' loops -touch $@ # Unquoted $@ -echo 'Don't forget to restart!' # Singlequote closed by apostrophe -echo 'Don\'t try this at home' # Attempting to escape ' in '' -echo 'Path is $PATH' # Variables in single quotes -trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap -unset var[i] # Array index treated as glob -``` - -### Conditionals - -ShellCheck can recognize many types of incorrect test statements. - -```sh -[[ n != 0 ]] # Constant test expressions -[[ -e *.mpg ]] # Existence checks of globs -[[ $foo==0 ]] # Always true due to missing spaces -[[ -n "$foo " ]] # Always true due to literals -[[ $foo =~ "fo+" ]] # Quoted regex in =~ -[ foo =~ re ] # Unsupported [ ] operators -[ $1 -eq "shellcheck" ] # Numerical comparison of strings -[ $n && $m ] # && in [ .. ] -[ grep -q foo file ] # Command without $(..) -[[ "$$file" == *.jpg ]] # Comparisons that can't succeed -(( 1 -lt 2 )) # Using test operators in ((..)) -[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping -``` - -### Frequently misused commands - -ShellCheck can recognize instances where commands are used incorrectly: - -```sh -grep '*foo*' file # Globs in regex contexts -find . -exec foo {} && bar {} \; # Prematurely terminated find -exec -sudo echo 'Var=42' > /etc/profile # Redirecting sudo -time --format=%s sleep 10 # Passing time(1) flags to time builtin -while read h; do ssh "$h" uptime # Commands eating while loop input -alias archive='mv $1 /backup' # Defining aliases with arguments -tr -cd '[a-zA-Z0-9]' # [] around ranges in tr -exec foo; echo "Done!" # Misused 'exec' -find -name \*.bak -o -name \*~ -delete # Implicit precedence in find -# find . -exec foo > bar \; # Redirections in find -f() { whoami; }; sudo f # External use of internal functions -``` - -### Common beginner's mistakes - -ShellCheck recognizes many common beginner's syntax errors: - -```sh -var = 42 # Spaces around = in assignments -$foo=42 # $ in assignments -for $var in *; do ... # $ in for loop variables -var$n="Hello" # Wrong indirect assignment -echo ${var$n} # Wrong indirect reference -var=(1, 2, 3) # Comma separated arrays -array=( [index] = value ) # Incorrect index initialization -echo $var[14] # Missing {} in array references -echo "Argument 10 is $10" # Positional parameter misreference -if $(myfunction); then ..; fi # Wrapping commands in $() -else if othercondition; then .. # Using 'else if' -f; f() { echo "hello world; } # Using function before definition -[ false ] # 'false' being true -if ( -f file ) # Using (..) instead of test -``` - -### Style - -ShellCheck can make suggestions to improve style: - -```sh -[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead -a >> log; b >> log; c >> log # Use a redirection block instead -echo "The time is `date`" # Use $() instead -cd dir; process *; cd ..; # Use subshells instead -echo $[1+2] # Use standard $((..)) instead of old $[] -echo $(($RANDOM % 6)) # Don't use $ on variables in $((..)) -echo "$(date)" # Useless use of echo -cat file | grep foo # Useless use of cat -``` - -### Data and typing errors - -ShellCheck can recognize issues related to data and typing: - -```sh -args="$@" # Assigning arrays to strings -files=(foo bar); echo "$files" # Referencing arrays as strings -declare -A arr=(foo bar) # Associative arrays without index -printf "%s\n" "Arguments: $@." # Concatenating strings and arrays -[[ $# > 2 ]] # Comparing numbers as strings -var=World; echo "Hello " var # Unused lowercase variables -echo "Hello $name" # Unassigned lowercase variables -cmd | read bar; echo $bar # Assignments in subshells -cat foo | cp bar # Piping to commands that don't read -printf '%s: %s\n' foo # Mismatches in printf argument count -eval "${array[@]}" # Lost word boundaries in array eval -for i in "${x[@]}"; do ${x[$i]} # Using array value as key -``` - -### Robustness - -ShellCheck can make suggestions for improving the robustness of a script: - -```sh -rm -rf "$STEAMROOT/"* # Catastrophic rm -touch ./-l; ls * # Globs that could become options -find . -exec sh -c 'a && b {}' \; # Find -exec shell injection -printf "Hello $name" # Variables in printf format -for f in $(ls *.txt); do # Iterating over ls output -export MYVAR=$(cmd) # Masked exit codes -case $version in 2.*) :;; 2.6.*) # Shadowed case branches -``` - -### Portability - -ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`: - -```sh -echo {1..$n} # Works in ksh, but not bash/dash/sh -echo {1..10} # Works in ksh and bash, but not dash/sh -echo -n 42 # Works in ksh, bash and dash, undefined in sh -expr match str regex # Unportable alias for `expr str : regex` -trap 'exit 42' sigint # Unportable signal spec -cmd &> file # Unportable redirection operator -read foo < /dev/tcp/host/22 # Unportable intercepted files -foo-bar() { ..; } # Undefined/unsupported function name -[ $UID = 0 ] # Variable undefined in dash/sh -local var=value # local is undefined in sh -time sleep 1 | sleep 5 # Undefined uses of 'time' -``` - -### Miscellaneous - -ShellCheck recognizes a menagerie of other issues: - -```sh -PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\] -PATH="$PATH:~/bin" # Literal tilde in $PATH -rm “file” # Unicode quotes -echo "Hello world" # Carriage return / DOS line endings -echo hello \ # Trailing spaces after \ -var=42 echo $var # Expansion of inlined environment -!# bin/bash -x -e # Common shebang errors -echo $((n/180*100)) # Unnecessary loss of precision -ls *[:digit:].txt # Bad character class globs -sed 's/foo/bar/' file > file # Redirecting to input -var2=$var2 # Variable assigned to itself -[ x$var = xval ] # Antiquated x-comparisons -ls() { ls -l "$@"; } # Infinitely recursive wrapper -alias ls='ls -l'; ls foo # Alias used before it takes effect -for x; do for x; do # Nested loop uses same variable -while getopts "a" f; do case $f in "b") # Unhandled getopts flags -``` - -## Testimonials - -> At first you're like "shellcheck is awesome" but then you're like "wtf are we still using bash" - -Alexander Tarasikov, -[via Twitter](https://twitter.com/astarasikov/status/568825996532707330) - -## Ignoring issues - -Issues can be ignored via environmental variable, command line, individually or globally within a file: - - - -## Reporting bugs - -Please use the GitHub issue tracker for any bugs or feature suggestions: - - - -## Contributing - -Please submit patches to code or documentation as GitHub pull requests! Check -out the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the -ShellCheck Wiki. - -Contributions must be licensed under the GNU GPLv3. -The contributor retains the copyright. - -## Copyright - -ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE). - -Copyright 2012-2019, [Vidar 'koala_man' Holen](https://github.com/koalaman/) and contributors. - -Happy ShellChecking! - -## Other Resources - -* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221). -* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)! diff --git a/ShellCheck.cabal b/ShellCheck.cabal deleted file mode 100644 index 6e21526..0000000 --- a/ShellCheck.cabal +++ /dev/null @@ -1,146 +0,0 @@ -Name: ShellCheck -Version: 0.11.0 -Synopsis: Shell script analysis tool -License: GPL-3 -License-file: LICENSE -Category: Static Analysis -Author: Vidar Holen -Maintainer: vidar@vidarholen.net -Homepage: https://www.shellcheck.net/ -Build-Type: Simple -Cabal-Version: 1.18 -Bug-reports: https://github.com/koalaman/shellcheck/issues -Description: - The goals of ShellCheck are: - . - * To point out and clarify typical beginner's syntax issues, - that causes a shell to give cryptic error messages. - . - * To point out and clarify typical intermediate level semantic problems, - that causes a shell to behave strangely and counter-intuitively. - . - * To point out subtle caveats, corner cases and pitfalls, that may cause an - advanced user's otherwise working script to fail under future circumstances. - -Extra-Doc-Files: - README.md - CHANGELOG.md -Extra-Source-Files: - -- documentation - shellcheck.1.md - -- A script to build the man page using pandoc - manpage - -- convenience script for stripping tests - striptests - -- tests - test/shellcheck.hs - -source-repository head - type: git - location: git://github.com/koalaman/shellcheck.git - -library - hs-source-dirs: src - if impl(ghc < 8.0) - build-depends: - semigroups - build-depends: - -- The lower bounds are based on GHC 7.10.3 - -- The upper bounds are based on GHC 9.12.1 - aeson >= 1.4.0 && < 2.3, - array >= 0.5.1 && < 0.6, - base >= 4.8.0.0 && < 5, - bytestring >= 0.10.6 && < 0.13, - containers >= 0.5.6 && < 0.9, - deepseq >= 1.4.1 && < 1.6, - Diff >= 0.4.0 && < 1.1, - fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), - filepath >= 1.4.0 && < 1.6, - mtl >= 2.2.2 && < 2.4, - parsec >= 3.1.14 && < 3.2, - QuickCheck >= 2.14.2 && < 2.17, - regex-tdfa >= 1.2.0 && < 1.4, - transformers >= 0.4.2 && < 0.7, - - -- getXdgDirectory from 1.2.3.0 - directory >= 1.2.3 && < 1.4, - - -- When cabal supports it, move this to setup-depends: - process - exposed-modules: - ShellCheck.AST - ShellCheck.ASTLib - ShellCheck.Analytics - ShellCheck.Analyzer - ShellCheck.AnalyzerLib - ShellCheck.CFG - ShellCheck.CFGAnalysis - ShellCheck.Checker - ShellCheck.Checks.Commands - ShellCheck.Checks.ControlFlow - ShellCheck.Checks.Custom - ShellCheck.Checks.ShellSupport - ShellCheck.Data - ShellCheck.Debug - ShellCheck.Fixer - ShellCheck.Formatter.Format - ShellCheck.Formatter.CheckStyle - ShellCheck.Formatter.Diff - ShellCheck.Formatter.GCC - ShellCheck.Formatter.JSON - ShellCheck.Formatter.JSON1 - ShellCheck.Formatter.TTY - ShellCheck.Formatter.Quiet - ShellCheck.Interface - ShellCheck.Parser - ShellCheck.Prelude - ShellCheck.Regex - other-modules: - Paths_ShellCheck - default-language: Haskell98 - -executable shellcheck - if impl(ghc < 8.0) - build-depends: - semigroups - build-depends: - aeson, - array, - base, - bytestring, - containers, - deepseq, - Diff, - directory, - fgl, - mtl, - filepath, - parsec, - QuickCheck, - regex-tdfa, - transformers, - ShellCheck - default-language: Haskell98 - main-is: shellcheck.hs - -test-suite test-shellcheck - type: exitcode-stdio-1.0 - build-depends: - aeson, - array, - base, - bytestring, - containers, - deepseq, - Diff, - directory, - fgl, - filepath, - mtl, - parsec, - QuickCheck, - regex-tdfa, - transformers, - ShellCheck - default-language: Haskell98 - main-is: test/shellcheck.hs diff --git a/builders/README.md b/builders/README.md deleted file mode 100644 index 31e8607..0000000 --- a/builders/README.md +++ /dev/null @@ -1,17 +0,0 @@ -This directory contains Dockerfiles for all builds. - -A build image will: - -* Run on Linux x86\_64 with vanilla Docker (no exceptions) -* Not contain any software that would restrict easy modification or copying -* Take a `cabal sdist` style tar.gz of the ShellCheck directory on stdin -* Output a tar.gz of artifacts on stdout, in a directory named for the arch - -This makes it simple to build any release without exotic hardware or software. - -An image can be built and tagged using `build_builder`, -and run on a source tarball using `run_builder`. - -Tip: Are you developing an image that relies on QEmu usermode emulation? - It's easy to accidentally depend on binfmt\_misc on the host OS. - Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing. diff --git a/builders/build_builder b/builders/build_builder deleted file mode 100755 index b34b996..0000000 --- a/builders/build_builder +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -if [ $# -eq 0 ] -then - echo >&2 "No build image directories specified" - echo >&2 "Example: $0 build/*/" - exit 1 -fi - -for dir -do - ( cd "$dir" && docker build -t "$(cat tag)" . ) || exit 1 -done diff --git a/builders/darwin.aarch64/Dockerfile b/builders/darwin.aarch64/Dockerfile deleted file mode 100644 index 95c1cbe..0000000 --- a/builders/darwin.aarch64/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -FROM ghcr.io/shepherdjerred/macos-cross-compiler@sha256:7d40c5e179d5d15453cf2a6b1bba3392bb1448b8257ee6b86021fc905c59dad6 - -ENV TARGET=aarch64-apple-darwin22 -ENV TARGETNAME=darwin.aarch64 - -# Build dependencies -USER root -ENV DEBIAN_FRONTEND=noninteractive -ENV LC_ALL=C.utf8 - -# Install basic deps -RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static - -# Install a more suitable host compiler -WORKDIR /host-ghc -RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin -RUN curl -L 'https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-x86_64-deb10-linux.tar.xz' | tar xJ --strip-components=1 -RUN ./configure && make install - -# Build GHC. We have to use an old version because cross-compilation across OS has since broken. -WORKDIR /ghc -RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1 -RUN apt-get install -y llvm-12 -RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" -RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" -RUN make install - -# Due to an apparent cabal bug, we specify our options directly to cabal -# It won't reuse caches if ghc-options are specified in ~/.cabal/config -ENV CABALOPTS="--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0" - -# Prebuild the dependencies -RUN cabal update -RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck - -# Copy the build script -COPY build /usr/bin - -WORKDIR /scratch -ENTRYPOINT ["/usr/bin/build"] diff --git a/builders/darwin.aarch64/build b/builders/darwin.aarch64/build deleted file mode 100755 index ff522ff..0000000 --- a/builders/darwin.aarch64/build +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -set -xe -{ - tar xzv --strip-components=1 - chmod +x striptests && ./striptests - mkdir "$TARGETNAME" - ( IFS=';'; cabal build $CABALOPTS ) - find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - # Stripping invalidates the code signature and the build image does - # not appear to have anything similar to the 'codesign' tool. - # "$TARGET-strip" "$TARGETNAME/shellcheck" - ls -l "$TARGETNAME" - file "$TARGETNAME/shellcheck" | grep "Mach-O 64-bit arm64 executable" -} >&2 -tar czv "$TARGETNAME" diff --git a/builders/darwin.aarch64/tag b/builders/darwin.aarch64/tag deleted file mode 100644 index ae93ef3..0000000 --- a/builders/darwin.aarch64/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-darwin-aarch64 diff --git a/builders/darwin.x86_64/Dockerfile b/builders/darwin.x86_64/Dockerfile deleted file mode 100644 index ceef155..0000000 --- a/builders/darwin.x86_64/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76 - -ENV TARGET=x86_64-apple-darwin18 -ENV TARGETNAME=darwin.x86_64 - -# Build dependencies -USER root -ENV DEBIAN_FRONTEND=noninteractive -RUN sed -e 's/focal/kinetic/g' -e 's/archive\|security/old-releases/' -i /etc/apt/sources.list -RUN apt-get update -RUN apt-get dist-upgrade -y -RUN apt-get install -y ghc automake autoconf llvm curl alex happy - -# Build GHC -WORKDIR /ghc -RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1 -RUN ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" -RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" -RUN make install -RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin - -# Due to an apparent cabal bug, we specify our options directly to cabal -# It won't reuse caches if ghc-options are specified in ~/.cabal/config -ENV CABALOPTS="--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg" - -# Prebuild the dependencies -RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck - -# Copy the build script -COPY build /usr/bin - -WORKDIR /scratch -ENTRYPOINT ["/usr/bin/build"] diff --git a/builders/darwin.x86_64/build b/builders/darwin.x86_64/build deleted file mode 100755 index 058cece..0000000 --- a/builders/darwin.x86_64/build +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -set -xe -{ - tar xzv --strip-components=1 - chmod +x striptests && ./striptests - mkdir "$TARGETNAME" - ( IFS=';'; cabal build $CABALOPTS ) - find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - "$TARGET-strip" -Sx "$TARGETNAME/shellcheck" - ls -l "$TARGETNAME" -} >&2 -tar czv "$TARGETNAME" diff --git a/builders/darwin.x86_64/tag b/builders/darwin.x86_64/tag deleted file mode 100644 index 237a65c..0000000 --- a/builders/darwin.x86_64/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-darwin-x86_64 diff --git a/builders/linux.aarch64/Dockerfile b/builders/linux.aarch64/Dockerfile deleted file mode 100644 index e783bf7..0000000 --- a/builders/linux.aarch64/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM ubuntu:25.04 - -ENV TARGET=aarch64-linux-gnu -ENV TARGETNAME=linux.aarch64 - -# Build dependencies -USER root -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y llvm-20 "gcc-$TARGET" "g++-$TARGET" ghc alex happy automake autoconf build-essential curl qemu-user-static -RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-linux-alpine3_20.tar.xz" | tar xJv -C /usr/local/bin && cabal update - -# Build GHC -WORKDIR /ghc -RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-src.tar.xz" | tar xJ --strip-components=1 -RUN ./boot.source && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" -# GHC fails to build if it can't encode non-ascii -ENV LC_CTYPE=C.utf8 -# We have to do a binary-dist instead of a direct install, otherwise the targest won't have -# cross compilation prefixes in /usr/local/lib/aarch64-linux-gnu-ghc-*/lib/settings -RUN ./hadrian/build --flavour=quickest --bignum=native -V -j --prefix=/usr/local install -# Hadrian just outputs "gcc" as the name of gcc, without accounting for $TARGET. Manually fix up the paths: -RUN sed -e 's/"\(gcc\|g++\|ld\)"/"'"$TARGET"'-\1"/g' -i /usr/local/lib/$TARGET-ghc-*/lib/settings - -# Due to an apparent cabal bug, we specify our options directly to cabal -# It won't reuse caches if ghc-options are specified in ~/.cabal/config -ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-compiler=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native" - -# Prebuild the dependencies -RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck - -# Copy the build script -COPY build /usr/bin - -WORKDIR /scratch -ENTRYPOINT ["/usr/bin/build"] diff --git a/builders/linux.aarch64/build b/builders/linux.aarch64/build deleted file mode 100755 index c68f7b2..0000000 --- a/builders/linux.aarch64/build +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -set -xe -{ - tar xzv --strip-components=1 - chmod +x striptests && ./striptests - mkdir "$TARGETNAME" - ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) - find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - "$TARGET-strip" -s "$TARGETNAME/shellcheck" - ls -l "$TARGETNAME" - "qemu-${TARGET%%-*}-static" "$TARGETNAME/shellcheck" --version -} >&2 -tar czv "$TARGETNAME" diff --git a/builders/linux.aarch64/tag b/builders/linux.aarch64/tag deleted file mode 100644 index 6788e14..0000000 --- a/builders/linux.aarch64/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-linux-aarch64 diff --git a/builders/linux.armv6hf/Dockerfile b/builders/linux.armv6hf/Dockerfile deleted file mode 100644 index 70a1148..0000000 --- a/builders/linux.armv6hf/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -# This Docker file uses a custom QEmu fork with patches to follow execve -# to build all of ShellCheck emulated. - -FROM ubuntu:25.04 - -ENV TARGETNAME linux.armv6hf - -# Build QEmu with execve follow support -USER root -ENV DEBIAN_FRONTEND noninteractive -RUN apt-get update -RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap -WORKDIR /qemu -RUN git clone --depth 1 https://github.com/koalaman/qemu . -RUN ./configure --static --disable-werror && cd build && ninja qemu-arm -ENV QEMU_EXECVE 1 - -# Convenience utility -COPY scutil /bin/scutil -COPY scutil /chroot/bin/scutil -RUN chmod +x /bin/scutil /chroot/bin/scutil - -# Set up an armv6 userspace -WORKDIR / -RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian -RUN cp /qemu/build/qemu-arm /chroot/bin/qemu -RUN scutil emu /debootstrap/debootstrap --second-stage - -# Install deps in the chroot -RUN scutil emu apt-get update -RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install -RUN scutil emu cabal update - -# Finally we can build the current dependencies. This takes hours. -ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections" -# Generated with `cabal freeze --constraint 'hashable -arch-native'` -COPY cabal.project.freeze /chroot/etc -RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS - -# Copy the build script -COPY build /chroot/bin -ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"] diff --git a/builders/linux.armv6hf/build b/builders/linux.armv6hf/build deleted file mode 100755 index 1d496ae..0000000 --- a/builders/linux.armv6hf/build +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -set -xe -mkdir /scratch && cd /scratch -{ - tar xzv --strip-components=1 - cp /etc/cabal.project.freeze . - chmod +x striptests && ./striptests - mkdir "$TARGETNAME" - # This script does not cabal update because compiling anything new is slow - ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) - find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - strip -s "$TARGETNAME/shellcheck" - ls -l "$TARGETNAME" - "$TARGETNAME/shellcheck" --version -} >&2 -tar czv "$TARGETNAME" diff --git a/builders/linux.armv6hf/cabal.project.freeze b/builders/linux.armv6hf/cabal.project.freeze deleted file mode 100644 index 43388ec..0000000 --- a/builders/linux.armv6hf/cabal.project.freeze +++ /dev/null @@ -1,105 +0,0 @@ -active-repositories: hackage.haskell.org:merge -constraints: any.Diff ==1.0.2, - any.OneTuple ==0.4.2, - any.QuickCheck ==2.16.0.0, - QuickCheck -old-random +templatehaskell, - any.StateVar ==1.2.2, - any.aeson ==2.2.3.0, - aeson +ordered-keymap, - any.ansi-terminal ==1.1.3, - ansi-terminal -example, - any.ansi-terminal-types ==1.1.3, - any.array ==0.5.4.0, - any.assoc ==1.1.1, - assoc -tagged, - any.base ==4.15.1.0, - any.base-orphans ==0.9.3, - any.bifunctors ==5.6.2, - bifunctors +tagged, - any.binary ==0.8.8.0, - any.bytestring ==0.10.12.1, - any.character-ps ==0.1, - any.colour ==2.3.6, - any.comonad ==5.0.9, - comonad +containers +distributive +indexed-traversable, - any.containers ==0.6.4.1, - any.contravariant ==1.5.5, - contravariant +semigroups +statevar +tagged, - any.data-array-byte ==0.1.0.1, - any.data-fix ==0.3.4, - any.deepseq ==1.4.5.0, - any.directory ==1.3.6.2, - any.distributive ==0.6.2.1, - distributive +semigroups +tagged, - any.dlist ==1.0, - dlist -werror, - any.exceptions ==0.10.4, - any.fgl ==5.8.3.0, - fgl +containers042, - any.filepath ==1.4.2.1, - any.foldable1-classes-compat ==0.1.2, - foldable1-classes-compat +tagged, - any.generically ==0.1.1, - any.ghc-bignum ==1.1, - any.ghc-boot-th ==9.0.2, - any.ghc-prim ==0.7.0, - any.hashable ==1.4.7.0, - hashable -arch-native +integer-gmp -random-initial-seed, - any.indexed-traversable ==0.1.4, - any.indexed-traversable-instances ==0.1.2, - any.integer-conversion ==0.1.1, - any.integer-logarithms ==1.0.4, - integer-logarithms -check-bounds +integer-gmp, - any.mtl ==2.2.2, - any.network-uri ==2.6.4.2, - any.optparse-applicative ==0.19.0.0, - optparse-applicative +process, - any.parsec ==3.1.14.0, - any.pretty ==1.1.3.6, - any.prettyprinter ==1.7.1, - prettyprinter -buildreadme +text, - any.prettyprinter-ansi-terminal ==1.1.3, - any.primitive ==0.9.1.0, - any.process ==1.6.13.2, - any.random ==1.3.1, - any.regex-base ==0.94.0.3, - any.regex-tdfa ==1.3.2.4, - regex-tdfa +doctest -force-o2, - any.rts ==1.0.2, - any.scientific ==0.3.8.0, - scientific -integer-simple, - any.semialign ==1.3.1, - semialign +semigroupoids, - any.semigroupoids ==6.0.1, - semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers, - any.splitmix ==0.1.3.1, - splitmix -optimised-mixer, - any.stm ==2.5.0.0, - any.strict ==0.5.1, - any.tagged ==0.8.9, - tagged +deepseq +transformers, - any.tasty ==1.5.3, - tasty +unix, - any.template-haskell ==2.17.0.0, - any.text ==1.2.5.0, - any.text-iso8601 ==0.1.1, - any.text-short ==0.1.6, - text-short -asserts, - any.th-abstraction ==0.7.1.0, - any.th-compat ==0.1.6, - any.these ==1.2.1, - any.time ==1.9.3, - any.time-compat ==1.9.8, - any.transformers ==0.5.6.2, - any.transformers-compat ==0.7.2, - transformers-compat -five +five-three -four +generic-deriving +mtl -three -two, - any.unbounded-delays ==0.1.1.1, - any.unix ==2.7.2.2, - any.unordered-containers ==0.2.20, - unordered-containers -debug, - any.uuid-types ==1.0.6, - any.vector ==0.13.2.0, - vector +boundschecks -internalchecks -unsafechecks -wall, - any.vector-stream ==0.1.0.1, - any.witherable ==0.5 -index-state: hackage.haskell.org 2025-07-22T18:12:16Z diff --git a/builders/linux.armv6hf/scutil b/builders/linux.armv6hf/scutil deleted file mode 100644 index 96d5216..0000000 --- a/builders/linux.armv6hf/scutil +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/dash -# Various ShellCheck build utility functions - -# Generally set a ulimit to avoid QEmu using too much memory -ulimit -v "$((10*1024*1024))" -# If we happen to invoke or run under QEmu, make sure to follow execve. -# This requires a patched QEmu. -export QEMU_EXECVE=1 - -# Retry a command until it succeeds -# Usage: scutil retry 3 mycmd -retry() { - n="$1" - ret=1 - shift - while [ "$n" -gt 0 ] - do - "$@" - ret=$? - [ "$ret" = 0 ] && break - n=$((n-1)) - done - return "$ret" -} - -# Install all dependencies from a freeze file -# Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install -install_from_freeze() { - linefeed=$(printf '\nx') - linefeed=${linefeed%x} - flags=$( - sed 's/constraints:/&\n /' "$1" | - grep -vw -e rts -e base -e ghc | - sed -n -e 's/^ *\([^,]*\).*/\1/p' | - sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e') - shift - # shellcheck disable=SC2086 - ( IFS=$linefeed; set -x; "$@" $flags ) -} - -# Run a command under emulation. -# This assumes the correct emulator is named 'qemu' and the chroot is /chroot -# Usage: scutil emu echo "Hello World" -emu() { - chroot /chroot /bin/qemu /usr/bin/env "$@" -} - -"$@" diff --git a/builders/linux.armv6hf/tag b/builders/linux.armv6hf/tag deleted file mode 100644 index 9172c5c..0000000 --- a/builders/linux.armv6hf/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-linux-armv6hf diff --git a/builders/linux.riscv64/Dockerfile b/builders/linux.riscv64/Dockerfile deleted file mode 100644 index b7bd961..0000000 --- a/builders/linux.riscv64/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM ubuntu:25.04 - -ENV TARGETNAME=linux.riscv64 -ENV TARGET=riscv64-linux-gnu - -# Build dependencies -USER root -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y llvm-20 "gcc-$TARGET" "g++-$TARGET" ghc alex happy automake autoconf build-essential curl qemu-user-static -RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-linux-alpine3_20.tar.xz" | tar xJv -C /usr/local/bin && cabal update - -# Build GHC -WORKDIR /ghc -RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-src.tar.xz" | tar xJ --strip-components=1 -RUN ./boot.source && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" -# GHC fails to build if it can't encode non-ascii -ENV LC_CTYPE=C.utf8 -# We have to do a binary-dist instead of a direct install, otherwise the targest won't have -# cross compilation prefixes in /usr/local/lib/aarch64-linux-gnu-ghc-*/lib/settings -RUN ./hadrian/build --flavour=quickest --bignum=native -V -j --prefix=/usr/local install -# Hadrian just outputs "gcc" as the name of gcc, without accounting for $TARGET. Manually fix up the paths: -RUN sed -e 's/"\(gcc\|g++\|ld\)"/"'"$TARGET"'-\1"/g' -i /usr/local/lib/$TARGET-ghc-*/lib/settings - -# Due to an apparent cabal bug, we specify our options directly to cabal -# It won't reuse caches if ghc-options are specified in ~/.cabal/config -ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-compiler=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native" - -# Prebuild the dependencies -RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck - -# Copy the build script -COPY build /usr/bin - -WORKDIR /scratch -ENTRYPOINT ["/usr/bin/build"] diff --git a/builders/linux.riscv64/build b/builders/linux.riscv64/build deleted file mode 100755 index c68f7b2..0000000 --- a/builders/linux.riscv64/build +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -set -xe -{ - tar xzv --strip-components=1 - chmod +x striptests && ./striptests - mkdir "$TARGETNAME" - ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) - find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - "$TARGET-strip" -s "$TARGETNAME/shellcheck" - ls -l "$TARGETNAME" - "qemu-${TARGET%%-*}-static" "$TARGETNAME/shellcheck" --version -} >&2 -tar czv "$TARGETNAME" diff --git a/builders/linux.riscv64/tag b/builders/linux.riscv64/tag deleted file mode 100644 index 901eaaa..0000000 --- a/builders/linux.riscv64/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-linux-riscv64 diff --git a/builders/linux.x86_64/Dockerfile b/builders/linux.x86_64/Dockerfile deleted file mode 100644 index 33aea13..0000000 --- a/builders/linux.x86_64/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM alpine:3.22 -# alpine:3.16 (GHC 9.0.1): 5.8 megabytes (certs expired) -# alpine:3.17 (GHC 9.0.2): 15.0 megabytes (certs expired) -# alpine:3.18 (GHC 9.4.4): 29.0 megabytes (certs expired) -# alpine:3.19 (GHC 9.4.7): 29.0 megabytes (certs expired) -# alpine:3.20 (GHC 9.8.2): 16.0 megabytes -# alpine:3.21 (GHC 9.8.2): 16.0 megabytes -# alpine:3.22 (GHC 9.8.2): 16.0 megabytes - -ENV TARGETNAME=linux.x86_64 - -# Install GHC and cabal -USER root -RUN apk add ghc cabal g++ libffi-dev curl bash gmp gmp-static - -# Cabal has failed to cache if options are not specified on the command line, -# so do that explicitly. -ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections" - -# Verify that we have the certificates in place to successfully update cabal -RUN cabal update && rm -rf ~/.cabal - -# Other archs pre-build dependencies here, but this one doesn't to detect ecosystem movement -RUN true - -# Copy the build script -COPY build /usr/bin - -WORKDIR /scratch -ENTRYPOINT ["/usr/bin/build"] diff --git a/builders/linux.x86_64/build b/builders/linux.x86_64/build deleted file mode 100755 index 099f127..0000000 --- a/builders/linux.x86_64/build +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -set -xe -{ - tar xzv --strip-components=1 - chmod +x striptests && ./striptests - mkdir "$TARGETNAME" - cabal update - ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) - find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - strip -s "$TARGETNAME/shellcheck" - ls -l "$TARGETNAME" - "$TARGETNAME/shellcheck" --version -} >&2 -tar czv "$TARGETNAME" diff --git a/builders/linux.x86_64/tag b/builders/linux.x86_64/tag deleted file mode 100644 index f0224de..0000000 --- a/builders/linux.x86_64/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-linux-x86_64 diff --git a/builders/run_builder b/builders/run_builder deleted file mode 100755 index d6de27b..0000000 --- a/builders/run_builder +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -if [ $# -lt 2 ] -then - echo >&2 "This script builds a source archive (as produced by cabal sdist)" - echo >&2 "Usage: $0 sourcefile.tar.gz builddir..." - exit 1 -fi - -file=$(realpath "$1") -shift - -if [ ! -e "$file" ] -then - echo >&2 "$file does not exist" - exit 1 -fi - -set -ex -o pipefail - -for dir -do - tagfile="$dir/tag" - if [ ! -e "$tagfile" ] - then - echo >&2 "$tagfile does not exist" - exit 2 - fi - - docker run -i "$(< "$tagfile")" < "$file" | tar xz -done diff --git a/builders/windows.x86_64/Dockerfile b/builders/windows.x86_64/Dockerfile deleted file mode 100644 index 366cbc1..0000000 --- a/builders/windows.x86_64/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM ubuntu:25.04 - -ENV TARGETNAME=windows.x86_64 - -# We don't need wine32, even though it complains -USER root -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y curl busybox wine winbind xz-utils - -# Fetch Windows version, will be available under z:\haskell -WORKDIR /haskell -# 9.12.2 produces a 37M binary -# 9.0.2 produces a 28M binary -# 8.10.4 produces a 16M binary -# We don't want to be stuck on old versions forever though, so just go with the latest version -RUN curl -L "https://downloads.haskell.org/~ghc/9.12.2/ghc-9.12.2-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1 - -# Fetch dependencies -WORKDIR /haskell/bin -RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.16.0.0/cabal-install-3.16.0.0-x86_64-windows.zip" | busybox unzip - -RUN curl -L "https://curl.se/windows/dl-8.15.0_2/curl-8.15.0_2-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* . -RUN wine /haskell/bin/cabal.exe update -ENV WINEPATH=/haskell/bin:/haskell/mingw/bin - -# None of these actually seem to have an effect on GHC on Windows anymore, -# but we'll leave them in place anyways. -ENV CABALOPTS="--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections" - -# Precompile some deps to speed up later builds -RUN IFS=';' && wine /haskell/bin/cabal.exe install --lib --dependencies-only $CABALOPTS ShellCheck - -COPY build /usr/bin -WORKDIR /scratch -ENTRYPOINT ["/usr/bin/build"] diff --git a/builders/windows.x86_64/build b/builders/windows.x86_64/build deleted file mode 100755 index 22e5b42..0000000 --- a/builders/windows.x86_64/build +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -cabal() { - wine /haskell/bin/cabal.exe "$@" -} - -set -xe -{ - tar xzv --strip-components=1 - chmod +x striptests && ./striptests - mkdir "$TARGETNAME" - ( IFS=';'; cabal build $CABALOPTS ) - find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \; - ls -l "$TARGETNAME" - wine "/haskell/mingw/bin/strip.exe" -s "$TARGETNAME/shellcheck.exe" - ls -l "$TARGETNAME" - wine "$TARGETNAME/shellcheck.exe" --version -} >&2 -tar czv "$TARGETNAME" diff --git a/builders/windows.x86_64/tag b/builders/windows.x86_64/tag deleted file mode 100644 index a85921b..0000000 --- a/builders/windows.x86_64/tag +++ /dev/null @@ -1 +0,0 @@ -koalaman/scbuilder-windows-x86_64 diff --git a/doc/emacs-flycheck.png b/doc/emacs-flycheck.png deleted file mode 100644 index 98d9211..0000000 Binary files a/doc/emacs-flycheck.png and /dev/null differ diff --git a/doc/shellcheck_logo.svg b/doc/shellcheck_logo.svg deleted file mode 100644 index 836aa63..0000000 --- a/doc/shellcheck_logo.svg +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/terminal.png b/doc/terminal.png deleted file mode 100644 index 9ce2a8d..0000000 Binary files a/doc/terminal.png and /dev/null differ diff --git a/doc/vim-syntastic.png b/doc/vim-syntastic.png deleted file mode 100644 index 59ee722..0000000 Binary files a/doc/vim-syntastic.png and /dev/null differ diff --git a/manpage b/manpage deleted file mode 100755 index 0898092..0000000 --- a/manpage +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -echo >&2 "Generating man page using pandoc" -pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 || exit -echo >&2 "Done. You can read it with: man ./shellcheck.1" diff --git a/nextnumber b/nextnumber deleted file mode 100755 index 3bdf10a..0000000 --- a/nextnumber +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -# TODO: Find a less trashy way to get the next available error code -if ! shopt -s globstar -then - echo "Error: This script depends on Bash 4." >&2 - exit 1 -fi - -for i in 1 2 3 -do - last=$(grep -hv "^prop" ./**/*.hs | grep -Ewo "${i}[0-9]{3}" | sort -n | tail -n 1) - echo "Next ${i}xxx: $((last+1))" -done diff --git a/quickrun b/quickrun deleted file mode 100755 index e0e0547..0000000 --- a/quickrun +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -# quickrun runs ShellCheck in an interpreted mode. -# This allows testing changes without recompiling. - -path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1) -if [ -z "$path" ] -then - echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once." - exit 1 -fi -path="${path%/*}" - -exec runghc -isrc -i"$path" shellcheck.hs "$@" diff --git a/quicktest b/quicktest deleted file mode 100755 index 6a1cf61..0000000 --- a/quicktest +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# quicktest runs the ShellCheck unit tests in an interpreted mode. -# This allows running tests without compiling, which can be faster. -# 'cabal test' remains the source of truth. - -path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1) -if [ -z "$path" ] -then - echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once." - exit 1 -fi -path="${path%/*}" - - -( -var=$(echo 'main' | ghci -isrc -i"$path" test/shellcheck.hs 2>&1 | tee /dev/stderr) -if [[ $var == *ExitSuccess* ]] -then - exit 0 -else - grep -C 3 -e "Fail" -e "Tracing" <<< "$var" - exit 1 -fi -) 2>&1 diff --git a/setgitversion b/setgitversion deleted file mode 100755 index 3afad61..0000000 --- a/setgitversion +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -xe -# This script hardcodes the `git describe` version as ShellCheck's version number. -# This is done to allow shellcheck --version to differ from the cabal version when -# building git snapshots. - -file="src/ShellCheck/Data.hs" -test -e "$file" -tmp=$(mktemp) -version=$(git describe) -sed -e "s/=.*VERSIONSTRING.*/= \"$version\" -- VERSIONSTRING, DO NOT SUBMIT/" "$file" > "$tmp" -mv "$tmp" "$file" diff --git a/shellcheck.1.md b/shellcheck.1.md deleted file mode 100644 index abe4c22..0000000 --- a/shellcheck.1.md +++ /dev/null @@ -1,406 +0,0 @@ -% SHELLCHECK(1) Shell script analysis tool - -# NAME - -shellcheck - Shell script analysis tool - -# SYNOPSIS - -**shellcheck** [*OPTIONS*...] *FILES*... - -# DESCRIPTION - -ShellCheck is a static analysis and linting tool for sh/bash scripts. It's -mainly focused on handling typical beginner and intermediate level syntax -errors and pitfalls where the shell just gives a cryptic error message or -strange behavior, but it also reports on a few more advanced issues where -corner cases can cause delayed failures. - -ShellCheck gives shell specific advice. Consider this line: - - (( area = 3.14*r*r )) - -+ For scripts starting with `#!/bin/sh` (or when using `-s sh`), ShellCheck -will warn that `(( .. ))` is not POSIX compliant (similar to checkbashisms). - -+ For scripts starting with `#!/bin/bash` (or using `-s bash`), ShellCheck -will warn that decimals are not supported. - -+ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will -not warn at all, as `ksh` supports decimals in arithmetic contexts. - -# OPTIONS - -**-a**,\ **--check-sourced** - -: Emit warnings in sourced files. Normally, `shellcheck` will only warn - about issues in the specified files. With this option, any issues in - sourced files will also be reported. - -**-C**[*WHEN*],\ **--color**[=*WHEN*] - -: For TTY output, enable colors *always*, *never* or *auto*. The default - is *auto*. **--color** without an argument is equivalent to - **--color=always**. - -**-i**\ *CODE1*[,*CODE2*...],\ **--include=***CODE1*[,*CODE2*...] - -: Explicitly include only the specified codes in the report. Subsequent **-i** - options are cumulative, but all the codes can be specified at once, - comma-separated as a single argument. Include options override any provided - exclude options. - -**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...] - -: Explicitly exclude the specified codes from the report. Subsequent **-e** - options are cumulative, but all the codes can be specified at once, - comma-separated as a single argument. - -**--extended-analysis=true/false** - -: Enable/disable Dataflow Analysis to identify more issues (default true). If - ShellCheck uses too much CPU/RAM when checking scripts with several - thousand lines of code, extended analysis can be disabled with this flag - or a directive. This flag overrides directives and rc files. - -**-f** *FORMAT*, **--format=***FORMAT* - -: Specify the output format of shellcheck, which prints its results in the - standard output. Subsequent **-f** options are ignored, see **FORMATS** - below for more information. - -**--list-optional** - -: Output a list of known optional checks. These can be enabled with **-o** - flags or **enable** directives. - -**--norc** - -: Don't try to look for .shellcheckrc configuration files. - -**--rcfile** *RCFILE* - -: Prefer the specified configuration file over searching for one - in the default locations. - -**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...] - -: Enable optional checks. The special name *all* enables all of them. - Subsequent **-o** options accumulate. This is equivalent to specifying - **enable** directives. - -**-P**\ *SOURCEPATH*,\ **--source-path=***SOURCEPATH* - -: Specify paths to search for sourced files, separated by `:` on Unix and - `;` on Windows. This is equivalent to specifying `search-path` - directives. - -**-s**\ *shell*,\ **--shell=***shell* - -: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*, - and *busybox*. - The default is to deduce the shell from the file's `shell` directive, - shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to - POSIX `sh` (not the system's), and will warn of portability issues. - -**-S**\ *SEVERITY*,\ **--severity=***severity* - -: Specify minimum severity of errors to consider. Valid values in order of - severity are *error*, *warning*, *info* and *style*. - The default is *style*. - -**-V**,\ **--version** - -: Print version information and exit. - -**-W** *NUM*,\ **--wiki-link-count=NUM** - -: For TTY output, show *NUM* wiki links to more information about mentioned - warnings. Set to 0 to disable them entirely. - -**-x**,\ **--external-sources** - -: Follow `source` statements even when the file is not specified as input. - By default, `shellcheck` will only follow files specified on the command - line (plus `/dev/null`). This option allows following any file the script - may `source`. - - This option may also be enabled using `external-sources=true` in - `.shellcheckrc`. This flag takes precedence. - -**FILES...** - -: One or more script files to check, or "-" for standard input. - - -# FORMATS - -**tty** - -: Plain text, human readable output. This is the default. - -**gcc** - -: GCC compatible output. Useful for editors that support compiling and - showing syntax errors. - - For example, in Vim, `:set makeprg=shellcheck\ -f\ gcc\ %` will allow - using `:make` to check the script, and `:cnext` to jump to the next error. - - ::: : - -**checkstyle** - -: Checkstyle compatible XML output. Supported directly or through plugins - by many IDEs and build monitoring systems. - - - - - - ... - - ... - - -**diff** - -: Auto-fixes in unified diff format. Can be piped to `git apply` or `patch -p1` - to automatically apply fixes. - - --- a/test.sh - +++ b/test.sh - @@ -2,6 +2,6 @@ - ## Example of a broken script. - for f in $(ls *.m3u) - do - - grep -qi hq.*mp3 $f \ - + grep -qi hq.*mp3 "$f" \ - && echo -e 'Playlist $f contains a HQ file in mp3 format' - done - - -**json1** - -: Json is a popular serialization format that is more suitable for web - applications. ShellCheck's json is compact and contains only the bare - minimum. Tabs are counted as 1 character. - - { - comments: [ - { - "file": "filename", - "line": lineNumber, - "column": columnNumber, - "level": "severitylevel", - "code": errorCode, - "message": "warning message" - }, - ... - ] - } - -**json** - -: This is a legacy version of the **json1** format. It's a raw array of - comments, and all offsets have a tab stop of 8. - -**quiet** - -: Suppress all normal output. Exit with zero if no issues are found, - otherwise exit with one. Stops processing after the first issue. - - -# DIRECTIVES - -ShellCheck directives can be specified as comments in the shell script. -If they appear before the first command, they are considered file-wide. -Otherwise, they apply to the immediately following command or block: - - # shellcheck key=value key=value - command-or-structure - -For example, to suppress SC2035 about using `./*.jpg`: - - # shellcheck disable=SC2035 - echo "Files: " *.jpg - -To tell ShellCheck where to look for an otherwise dynamically determined file: - - # shellcheck source=./lib.sh - source "$(find_install_dir)/lib.sh" - -Here a shell brace group is used to suppress a warning on multiple lines: - - # shellcheck disable=SC2016 - { - echo 'Modifying $PATH' - echo 'PATH=foo:$PATH' >> ~/.bashrc - } - -Valid keys are: - -**disable** -: Disables a comma separated list of error codes for the following command. - The command can be a simple command like `echo foo`, or a compound command - like a function definition, subshell block or loop. A range can be - be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx. - All warnings can be disabled with `disable=all`. - -**enable** -: Enable an optional check by name, as listed with **--list-optional**. - Only file-wide `enable` directives are considered. - -**extended-analysis** -: Set to true/false to enable/disable dataflow analysis. Specifying - `# shellcheck extended-analysis=false` in particularly large (2000+ line) - auto-generated scripts will reduce ShellCheck's resource usage at the - expense of certain checks. Extended analysis is enabled by default. - -**external-sources** -: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open - arbitrary files from 'source' statements (the way most tools do). - - This option defaults to `false` only due to ShellCheck's origin as a - remote service for checking untrusted scripts. It can safely be enabled - for normal development. - -**source** -: Overrides the filename included by a `source`/`.` statement. This can be - used to tell shellcheck where to look for a file whose name is determined - at runtime, or to skip a source by telling it to use `/dev/null`. - -**source-path** -: Add a directory to the search path for `source`/`.` statements (by default, - only ShellCheck's working directory is included). Absolute paths will also - be rooted in these paths. The special path `SCRIPTDIR` can be used to - specify the currently checked script's directory, as in - `source-path=SCRIPTDIR` or `source-path=SCRIPTDIR/../libs`. Multiple - paths accumulate, and `-P` takes precedence over them. - -**shell** -: Overrides the shell detected from the shebang. This is useful for - files meant to be included (and thus lacking a shebang), or possibly - as a more targeted alternative to 'disable=SC2039'. - -# RC FILES - -Unless `--norc` is used, ShellCheck will look for a file `.shellcheckrc` or -`shellcheckrc` in the script's directory and each parent directory. If found, -it will read `key=value` pairs from it and treat them as file-wide directives. - -Here is an example `.shellcheckrc`: - - # Look for 'source'd files relative to the checked script, - # and also look for absolute paths in /mnt/chroot - source-path=SCRIPTDIR - source-path=/mnt/chroot - - # Since 0.9.0, values can be quoted with '' or "" to allow spaces - source-path="My Documents/scripts" - - # Allow opening any 'source'd file, even if not specified as input - external-sources=true - - # Turn on warnings for unquoted variables with safe values - enable=quote-safe-variables - - # Turn on warnings for unassigned uppercase variables - enable=check-unassigned-uppercase - - # Allow [ ! -z foo ] instead of suggesting -n - disable=SC2236 - -If no `.shellcheckrc` is found in any of the parent directories, ShellCheck -will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME` -(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on -Windows. Only the first file found will be used. - -Note for Snap users: the Snap sandbox disallows access to hidden files. -Use `shellcheckrc` without the dot instead. - -Note for Docker users: ShellCheck will only be able to look for files that -are mounted in the container, so `~/.shellcheckrc` will not be read. - - -# ENVIRONMENT VARIABLES - -The environment variable `SHELLCHECK_OPTS` can be set with default flags: - - export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016' - -Its value will be split on spaces and prepended to the command line on each -invocation. - -# RETURN VALUES - -ShellCheck uses the following exit codes: - -+ 0: All files successfully scanned with no issues. -+ 1: All files successfully scanned with some issues. -+ 2: Some files could not be processed (e.g. file not found). -+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag). -+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter). - -# LOCALE - -This version of ShellCheck is only available in English. All files are -leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid -sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for -locales where encoding is unspecified (such as the `C` locale). - -Windows users seeing `commitBuffer: invalid argument (invalid character)` -should set their terminal to use UTF-8 with `chcp 65001`. - -# KNOWN INCOMPATIBILITIES - -(If nothing in this section makes sense, you are unlikely to be affected by it) - -To avoid confusing and misguided suggestions, ShellCheck requires function -bodies to be either `{ brace groups; }` or `( subshells )`, and function names -containing `[]*=!` are only recognized after a `function` keyword. - -The following unconventional function definitions are identical in Bash, -but ShellCheck only recognizes the latter. - - [x!=y] () [[ $1 ]] - function [x!=y] () { [[ $1 ]]; } - -Shells without the `function` keyword do not allow these characters in function -names to begin with. Function names containing `{}` are not supported at all. - -Further, if ShellCheck sees `[x!=y]` it will assume this is an invalid -comparison. To invoke the above function, quote the command as in `'[x!=y]'`, -or to retain the same globbing behavior, use `command [x!=y]`. - -ShellCheck imposes additional restrictions on the `[` command to help diagnose -common invalid uses. While `[ $x= 1 ]` is defined in POSIX, ShellCheck will -assume it was intended as the much more likely comparison `[ "$x" = 1 ]` and -fail accordingly. For unconventional or dynamic uses of the `[` command, use -`test` or `\[` instead. - -# REPORTING BUGS - -Bugs and issues can be reported on GitHub: - -https://github.com/koalaman/shellcheck/issues - -# AUTHORS - -ShellCheck is developed and maintained by Vidar Holen, with assistance from a -long list of wonderful contributors. - -# COPYRIGHT - -Copyright 2012-2025, Vidar Holen and contributors. -Licensed under the GNU General Public License version 3 or later, -see https://gnu.org/licenses/gpl.html - -# SEE ALSO - -sh(1), bash(1), dash(1), ksh(1) diff --git a/shellcheck.hs b/shellcheck.hs deleted file mode 100644 index def3654..0000000 --- a/shellcheck.hs +++ /dev/null @@ -1,635 +0,0 @@ -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -import qualified ShellCheck.Analyzer -import ShellCheck.Checker -import ShellCheck.Data -import ShellCheck.Interface -import ShellCheck.Regex - -import qualified ShellCheck.Formatter.CheckStyle -import ShellCheck.Formatter.Format -import qualified ShellCheck.Formatter.Diff -import qualified ShellCheck.Formatter.GCC -import qualified ShellCheck.Formatter.JSON -import qualified ShellCheck.Formatter.JSON1 -import qualified ShellCheck.Formatter.TTY -import qualified ShellCheck.Formatter.Quiet - -import Control.Exception -import Control.Monad -import Control.Monad.IO.Class -import Control.Monad.Trans.Class -import Control.Monad.Except -import Data.Bits -import Data.Char -import Data.Either -import Data.Functor -import Data.IORef -import Data.List -import qualified Data.Map as Map -import Data.Maybe -import Data.Monoid -import Data.Semigroup (Semigroup (..)) -import Prelude hiding (catch) -import System.Console.GetOpt -import System.Directory -import System.Environment -import System.Exit -import System.FilePath -import System.IO - -data Flag = Flag String String -data Status = - NoProblems - | SomeProblems - | SupportFailure - | SyntaxFailure - | RuntimeException - deriving (Ord, Eq, Show) - -instance Semigroup Status where - (<>) = max - -instance Monoid Status where - mempty = NoProblems - mappend = (Data.Semigroup.<>) - -data Options = Options { - checkSpec :: CheckSpec, - externalSources :: Bool, - sourcePaths :: [FilePath], - formatterOptions :: FormatterOptions, - minSeverity :: Severity, - rcfile :: Maybe FilePath -} - -defaultOptions = Options { - checkSpec = emptyCheckSpec, - externalSources = False, - sourcePaths = [], - formatterOptions = newFormatterOptions { - foColorOption = ColorAuto - }, - minSeverity = StyleC, - rcfile = Nothing -} - -usageHeader = "Usage: shellcheck [OPTIONS...] FILES..." -options = [ - Option "a" ["check-sourced"] - (NoArg $ Flag "sourced" "false") "Include warnings from sourced files", - Option "C" ["color"] - (OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN") - "Use color (auto, always, never)", - Option "i" ["include"] - (ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings", - Option "e" ["exclude"] - (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings", - Option "" ["extended-analysis"] - (ReqArg (Flag "extended-analysis") "bool") "Perform dataflow analysis (default true)", - Option "f" ["format"] - (ReqArg (Flag "format") "FORMAT") $ - "Output format (" ++ formatList ++ ")", - Option "" ["list-optional"] - (NoArg $ Flag "list-optional" "true") "List checks disabled by default", - Option "" ["norc"] - (NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files", - Option "" ["rcfile"] - (ReqArg (Flag "rcfile") "RCFILE") - "Prefer the specified configuration file over searching for one", - Option "o" ["enable"] - (ReqArg (Flag "enable") "check1,check2..") - "List of optional checks to enable (or 'all')", - Option "P" ["source-path"] - (ReqArg (Flag "source-path") "SOURCEPATHS") - "Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)", - Option "s" ["shell"] - (ReqArg (Flag "shell") "SHELLNAME") - "Specify dialect (sh, bash, dash, ksh, busybox)", - Option "S" ["severity"] - (ReqArg (Flag "severity") "SEVERITY") - "Minimum severity of errors to consider (error, warning, info, style)", - Option "V" ["version"] - (NoArg $ Flag "version" "true") "Print version information", - Option "W" ["wiki-link-count"] - (ReqArg (Flag "wiki-link-count") "NUM") - "The number of wiki links to show, when applicable", - Option "x" ["external-sources"] - (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES", - Option "" ["help"] - (NoArg $ Flag "help" "true") "Show this usage summary and exit" - ] -getUsageInfo = usageInfo usageHeader options - -printErr = lift . hPutStrLn stderr - -parseArguments :: [String] -> ExceptT Status IO ([Flag], [FilePath]) -parseArguments argv = - case getOpt Permute options argv of - (opts, files, []) -> return (opts, files) - (_, _, errors) -> do - printErr $ concat errors ++ "\n" ++ getUsageInfo - throwError SyntaxFailure - -formats :: FormatterOptions -> Map.Map String (IO Formatter) -formats options = Map.fromList [ - ("checkstyle", ShellCheck.Formatter.CheckStyle.format), - ("diff", ShellCheck.Formatter.Diff.format options), - ("gcc", ShellCheck.Formatter.GCC.format), - ("json", ShellCheck.Formatter.JSON.format), - ("json1", ShellCheck.Formatter.JSON1.format), - ("tty", ShellCheck.Formatter.TTY.format options), - ("quiet", ShellCheck.Formatter.Quiet.format options) - ] - -formatList = intercalate ", " names - where - names = Map.keys $ formats (formatterOptions defaultOptions) - -getOption [] _ = Nothing -getOption (Flag var val:_) name | name == var = return val -getOption (_:rest) flag = getOption rest flag - -getOptions options name = - map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options - -split char str = - split' str [] - where - split' (a:rest) element = - if a == char - then reverse element : split' rest [] - else split' rest (a:element) - split' [] element = [reverse element] - -toStatus = fmap (either id id) . runExceptT - -getEnvArgs = do - opts <- getEnv "SHELLCHECK_OPTS" `catch` cantWaitForLookupEnv - return . filter (not . null) $ opts `splitOn` mkRegex " +" - where - cantWaitForLookupEnv :: IOException -> IO String - cantWaitForLookupEnv = const $ return "" - -main = do - params <- getArgs - envOpts <- getEnvArgs - let args = envOpts ++ params - status <- toStatus $ do - (flags, files) <- parseArguments args - process flags files - exitWith $ statusToCode status - -statusToCode status = - case status of - NoProblems -> ExitSuccess - SomeProblems -> ExitFailure 1 - SyntaxFailure -> ExitFailure 3 - SupportFailure -> ExitFailure 4 - RuntimeException -> ExitFailure 2 - -process :: [Flag] -> [FilePath] -> ExceptT Status IO Status -process flags files = do - options <- foldM (flip parseOption) defaultOptions flags - verifyFiles files - let format = fromMaybe "tty" $ getOption flags "format" - let formatters = formats $ formatterOptions options - formatter <- - case Map.lookup format formatters of - Nothing -> do - printErr $ "Unknown format " ++ format - printErr "Supported formats:" - mapM_ (printErr . write) $ Map.keys formatters - throwError SupportFailure - where write s = " " ++ s - Just f -> ExceptT $ fmap Right f - sys <- lift $ ioInterface options files - lift $ runFormatter sys formatter options files - -runFormatter :: SystemInterface IO -> Formatter -> Options -> [FilePath] - -> IO Status -runFormatter sys format options files = do - header format - result <- foldM f NoProblems files - footer format - return result - where - f :: Status -> FilePath -> IO Status - f status file = do - newStatus <- process file `catch` handler file - return $! status `mappend` newStatus - handler :: FilePath -> IOException -> IO Status - handler file e = reportFailure file (show e) - reportFailure file str = do - onFailure format file str - return RuntimeException - - process :: FilePath -> IO Status - process filename = do - input <- siReadFile sys Nothing filename - either (reportFailure filename) check input - where - check contents = do - let checkspec = (checkSpec options) { - csFilename = filename, - csScript = contents - } - result <- checkScript sys checkspec - onResult format result sys - return $ - if null (crComments result) - then NoProblems - else SomeProblems - -parseEnum name value list = - case lookup value list of - Just value -> return value - Nothing -> do - printErr $ "Unknown value for --" ++ name ++ ". " ++ - "Valid options are: " ++ (intercalate ", " $ map fst list) - throwError SupportFailure - -parseColorOption value = - parseEnum "color" value [ - ("auto", ColorAuto), - ("always", ColorAlways), - ("never", ColorNever) - ] - -parseSeverityOption value = - parseEnum "severity" value [ - ("error", ErrorC), - ("warning", WarningC), - ("info", InfoC), - ("style", StyleC) - ] - -parseOption flag options = - case flag of - Flag "shell" str -> - fromMaybe (die $ "Unknown shell: " ++ str) $ do - shell <- shellForExecutable str - return $ return options { - checkSpec = (checkSpec options) { - csShellTypeOverride = Just shell - } - } - - Flag "exclude" str -> do - new <- mapM parseNum $ filter (not . null) $ split ',' str - let old = csExcludedWarnings . checkSpec $ options - return options { - checkSpec = (checkSpec options) { - csExcludedWarnings = new ++ old - } - } - - Flag "include" str -> do - new <- mapM parseNum $ filter (not . null) $ split ',' str - let old = csIncludedWarnings . checkSpec $ options - return options { - checkSpec = (checkSpec options) { - csIncludedWarnings = - if null new - then old - else Just new `mappend` old - } - } - - Flag "version" _ -> do - liftIO printVersion - throwError NoProblems - - Flag "list-optional" _ -> do - liftIO printOptional - throwError NoProblems - - Flag "help" _ -> do - liftIO $ putStrLn getUsageInfo - throwError NoProblems - - Flag "externals" _ -> - return options { - externalSources = True - } - - Flag "color" color -> do - option <- parseColorOption color - return options { - formatterOptions = (formatterOptions options) { - foColorOption = option - } - } - - Flag "source-path" str -> do - let paths = splitSearchPath str - return options { - sourcePaths = (sourcePaths options) ++ paths - } - - Flag "sourced" _ -> - return options { - checkSpec = (checkSpec options) { - csCheckSourced = True - } - } - - Flag "severity" severity -> do - option <- parseSeverityOption severity - return options { - checkSpec = (checkSpec options) { - csMinSeverity = option - } - } - - Flag "wiki-link-count" countString -> do - count <- parseNum countString - return options { - formatterOptions = (formatterOptions options) { - foWikiLinkCount = count - } - } - - Flag "norc" _ -> - return options { - checkSpec = (checkSpec options) { - csIgnoreRC = True - } - } - - Flag "rcfile" str -> do - return options { - rcfile = Just str - } - - Flag "enable" value -> - let cs = checkSpec options in return options { - checkSpec = cs { - csOptionalChecks = (csOptionalChecks cs) ++ split ',' value - } - } - - Flag "extended-analysis" str -> do - value <- parseBool str - return options { - checkSpec = (checkSpec options) { - csExtendedAnalysis = Just value - } - } - - -- This flag is handled specially in 'process' - Flag "format" _ -> return options - - Flag str _ -> do - printErr $ "Internal error for --" ++ str ++ ". Please file a bug :(" - return options - where - die s = do - printErr s - throwError SupportFailure - parseNum ('S':'C':str) = parseNum str - parseNum num = do - unless (all isDigit num) $ do - printErr $ "Invalid number: " ++ num - throwError SyntaxFailure - return (Prelude.read num :: Integer) - - parseBool str = do - case str of - "true" -> return True - "false" -> return False - _ -> do - printErr $ "Invalid boolean, expected true/false: " ++ str - throwError SyntaxFailure - -ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO) -ioInterface options files = do - inputs <- mapM normalize files - cache <- newIORef emptyCache - configCache <- newIORef ("", Nothing) - return (newSystemInterface :: SystemInterface IO) { - siReadFile = get cache inputs, - siFindSource = findSourceFile inputs (sourcePaths options), - siGetConfig = getConfig configCache - } - where - emptyCache :: Map.Map FilePath String - emptyCache = Map.empty - - get cache inputs rcSuggestsExternal file = do - map <- readIORef cache - case Map.lookup file map of - Just x -> return $ Right x - Nothing -> fetch cache inputs rcSuggestsExternal file - - fetch cache inputs rcSuggestsExternal file = do - ok <- allowable rcSuggestsExternal inputs file - if ok - then (do - (contents, shouldCache) <- inputFile file - when shouldCache $ - modifyIORef cache $ Map.insert file contents - return $ Right contents - ) `catch` handler - else - if rcSuggestsExternal == Just False - then return $ Left (file ++ " was not specified as input, and external files were disabled via directive.") - else return $ Left (file ++ " was not specified as input (see shellcheck -x).") - where - handler :: IOException -> IO (Either ErrorMessage String) - handler ex = return . Left $ show ex - - allowable rcSuggestsExternal inputs x = - if fromMaybe (externalSources options) rcSuggestsExternal - then return True - else do - path <- normalize x - return $ path `elem` inputs - - normalize x = - canonicalizePath x `catch` fallback x - where - fallback :: FilePath -> IOException -> IO FilePath - fallback path _ = return path - - - -- Returns the name and contents of .shellcheckrc for the given file - getConfig cache filename = - case rcfile options of - Just file -> do - -- We have a specified rcfile. Ignore normal rcfile resolution. - (path, result) <- readIORef cache - if path == "/" - then return result - else do - result <- readConfig file - when (isNothing result) $ - hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file - writeIORef cache ("/", result) - return result - - Nothing -> do - path <- normalize filename - let dir = takeDirectory path - (previousPath, result) <- readIORef cache - if dir == previousPath - then return result - else do - paths <- getConfigPaths dir - result <- findConfig paths - writeIORef cache (dir, result) - return result - - findConfig paths = - case paths of - (file:rest) -> do - contents <- readConfig file - if isJust contents - then return contents - else findConfig rest - [] -> return Nothing - - -- Get a list of candidate filenames. This includes .shellcheckrc - -- in all parent directories, plus the user's home dir and xdg dir. - -- The dot is optional for Windows and Snap users. - getConfigPaths dir = do - let next = takeDirectory dir - rest <- if next /= dir - then getConfigPaths next - else defaultPaths `catch` - ((const $ return []) :: IOException -> IO [FilePath]) - return $ (dir ".shellcheckrc") : (dir "shellcheckrc") : rest - - defaultPaths = do - home <- getAppUserDataDirectory "shellcheckrc" - xdg <- getXdgDirectory XdgConfig "shellcheckrc" - return [home, xdg] - - readConfig file = do - exists <- doesFileExist file - if exists - then do - (contents, _) <- inputFile file `catch` handler file - return $ Just (file, contents) - else - return Nothing - where - handler :: FilePath -> IOException -> IO (String, Bool) - handler file err = do - hPutStrLn stderr $ file ++ ": " ++ show err - return ("", True) - - andM a b arg = do - first <- a arg - if not first then return False else b arg - - findM p = foldr go (pure Nothing) - where - go x acc = do - b <- p x - if b then pure (Just x) else acc - - findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original = - if isAbsolute original - then - let (_, relative) = splitDrive original - in find relative original - else - find original original - where - find filename deflt = do - sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $ - (adjustPath filename):(map (( filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation) - case sources of - Nothing -> return deflt - Just first -> return first - scriptdir = dropFileName currentScript - adjustPath str = - case (splitDirectories str) of - ("SCRIPTDIR":rest) -> joinPath (scriptdir:rest) - _ -> str - -inputFile file = do - (handle, shouldCache) <- - if file == "-" - then return (stdin, True) - else do - h <- openBinaryFile file ReadMode - reopenable <- hIsSeekable h - return (h, not reopenable) - - hSetBinaryMode handle True - contents <- decodeString <$> hGetContents handle -- closes handle - - seq (length contents) $ - return (contents, shouldCache) - --- Decode a char8 string into a utf8 string, with fallback on --- ISO-8859-1. This avoids depending on additional libraries. -decodeString = decode - where - decode [] = [] - decode (c:rest) | isAscii c = c : decode rest - decode (c:rest) = - let num = (fromIntegral $ ord c) :: Int - next = case num of - _ | num >= 0xF8 -> Nothing - | num >= 0xF0 -> construct (num .&. 0x07) 3 rest - | num >= 0xE0 -> construct (num .&. 0x0F) 2 rest - | num >= 0xC0 -> construct (num .&. 0x1F) 1 rest - | True -> Nothing - in - case next of - Just (n, remainder) -> chr n : decode remainder - Nothing -> c : decode rest - - construct x 0 rest = do - guard $ x <= 0x10FFFF - return (x, rest) - construct x n (c:rest) = - let num = (fromIntegral $ ord c) :: Int in - if num >= 0x80 && num <= 0xBF - then construct ((x `shiftL` 6) .|. (num .&. 0x3f)) (n-1) rest - else Nothing - construct _ _ _ = Nothing - - -verifyFiles files = - when (null files) $ do - printErr "No files specified.\n" - printErr $ usageInfo usageHeader options - throwError SyntaxFailure - -printVersion = do - putStrLn "ShellCheck - shell script analysis tool" - putStrLn $ "version: " ++ shellcheckVersion - putStrLn "license: GNU General Public License, version 3" - putStrLn "website: https://www.shellcheck.net" - -printOptional = do - mapM f list - where - list = sortOn cdName ShellCheck.Analyzer.optionalChecks - f item = do - putStrLn $ "name: " ++ cdName item - putStrLn $ "desc: " ++ cdDescription item - putStrLn $ "example: " ++ cdPositive item - putStrLn $ "fix: " ++ cdNegative item - putStrLn "" diff --git a/shpell.hs b/shpell.hs new file mode 100644 index 0000000..dec310c --- /dev/null +++ b/shpell.hs @@ -0,0 +1,934 @@ +{-# LANGUAGE NoMonomorphismRestriction #-} + +-- Shpell Check, by Vidar 'koala_man' Holen +-- Sorry about the code. It was a week's worth of hacking. + +import Text.Parsec +import Text.Parsec.Pos (initialPos) +import Debug.Trace +import Control.Monad +import Data.Char +import Data.List (isInfixOf, partition, sortBy, intercalate) +import qualified Control.Monad.State as Ms +import Data.Maybe +import Prelude hiding (readList) +import System.IO + + + +backslash = char '\\' +linefeed = char '\n' +singleQuote = char '\'' +doubleQuote = char '"' +variableStart = upper <|> lower <|> oneOf "_" +variableChars = upper <|> lower <|> digit <|> oneOf "_" +specialVariable = oneOf "@*#?-$!" +tokenDelimiter = oneOf "&|;<> \t\n" +quotable = oneOf "#|&;<>()$`\\ \"'\t\n" +doubleQuotable = oneOf "\"$`" +whitespace = oneOf " \t\n" +linewhitespace = oneOf " \t" + +spacing = do + x <- many (many1 linewhitespace <|> (try $ string "\\\n")) + optional readComment + return $ concat x + +allspacing = do + spacing + x <- option False ((linefeed <|> carriageReturn) >> return True) + when x allspacing + +carriageReturn = do + parseNote ErrorC "Literal carriage return. Run script through tr -d '\\r' " + char '\r' + + +--------- Message/position annotation on top of user state +data Annotated a = Annotated SourcePos [Note] a deriving (Show, Eq) +data Note = ParseNote SourcePos Severity String | Note Severity String deriving (Show, Eq) +data MessageStack = StackNode Note MessageStack | StackMark String SourcePos MessageStack | StackEmpty +data ParseProblem = ParseProblem SourcePos Severity String deriving (Show, Eq) +data OutputNote = OutputNote SourcePos Severity String deriving (Show, Eq) +data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord) + +instance Functor Annotated where + fmap f (Annotated p n a) = Annotated p n (f a) + +markStack msg = do + pos <- getPosition + modifyState (StackMark msg pos) + +getMessages r (StackMark _ _ s) = (r, s) +getMessages r (StackNode n s) = getMessages (n:r) s +popStack = do + f <- getState + let (notes, stack) = getMessages [] f + putState stack + return notes + +-- Store potential parse problems outside of parsec +parseProblem level msg = do + pos <- getPosition + parseProblemAt pos level msg + +parseProblemAt pos level msg = do + Ms.modify ((ParseProblem pos level msg):) + +pushNote n = modifyState (StackNode n) + +parseNote l a = do + pos <- getPosition + parseNoteAt pos l a + +parseNoteAt pos l a = pushNote $ ParseNote pos l a + + +annotated msg parser = do + pos <- getPosition + markStack msg + result <- parser + messages <- popStack + return $ Annotated pos messages result + +dropAnnotation (Annotated _ _ s) = s +blankAnnotation pos t = Annotated pos [] t + +merge (Annotated pos messages result) = do + mapM pushNote messages + return result + +merging p = p >>= merge + +getOutputNotes (Annotated p notes _) = map (makeOutputNote p) notes + +makeOutputNote _ (ParseNote p l s) = OutputNote p l s +makeOutputNote p (Note l s) = OutputNote p l s + +--------- Convenient combinators + +thenSkip main follow = do + r <- main + optional follow + return r + +disregard x = x >> return () + +reluctantlyTill p end = do -- parse p until end <|> eof matches ahead + (lookAhead ((disregard $ try end) <|> eof) >> return []) <|> do + x <- p + more <- reluctantlyTill p end + return $ x:more + <|> return [] + +reluctantlyTill1 p end = do + notFollowedBy end + x <- p + more <- reluctantlyTill p end + return $ x:more + +attempting rest branch = do + ((try branch) >> rest) <|> rest + +wasIncluded p = option False (p >> return True) + +-- Horrifying AST +data Token = T_AND_IF | T_OR_IF | T_DSEMI | T_Semi | T_DLESS | T_DGREAT | T_LESSAND | T_GREATAND | T_LESSGREAT | T_DLESSDASH | T_CLOBBER | T_If | T_Then | T_Else | T_Elif | T_Fi | T_Do | T_Done | T_Case | T_Esac | T_While | T_Until | T_For | T_Lbrace | T_Rbrace | T_Lparen | T_Rparen | T_Bang | T_In | T_NEWLINE | T_EOF | T_Less | T_Greater | T_SingleQuoted String | T_Literal String | T_NormalWord [Annotated Token] | T_DoubleQuoted [Annotated Token] | T_DollarExpansion [Token] | T_DollarBraced String | T_DollarVariable String | T_DollarArithmetic String | T_BraceExpansion String | T_IoFile Token Token | T_HereDoc Bool Bool String | T_HereString Token | T_FdRedirect String Token | T_Assignment String Token | T_Redirecting [Annotated Token] Token | T_SimpleCommand [Annotated Token] [Annotated Token] | T_Pipeline [Annotated Token] | T_Banged Token | T_AndIf (Annotated Token) (Annotated Token) | T_OrIf (Annotated Token) (Annotated Token) | T_Backgrounded Token | T_IfExpression [([Token],[Token])] [Token] | T_Subshell [Token] | T_BraceGroup [Token] | T_WhileExpression [Token] [Token] | T_UntilExpression [Token] [Token] | T_ForIn String [Token] [Token] | T_CaseExpression Token [([Token],[Token])] |T_Function String Token | T_Command (Annotated Token) | T_Script [Token] + deriving (Show) + +extractNotes' list = modifyFlag ((++) $ concatMap getOutputNotes list) >> return () +extractNotes (T_NormalWord list) = extractNotes' list +extractNotes (T_DoubleQuoted list) = extractNotes' list +extractNotes (T_Redirecting list f) = extractNotes' list +extractNotes (T_Pipeline list) = extractNotes' list +extractNotes (T_Command list) = extractNotes' [list] +extractNotes (T_SimpleCommand list1 list2) = do + extractNotes' list1 + extractNotes' list2 +extractNotes t = return () + + +postMessage level s = Ms.modify $ \(x, l) -> (x, Note level s : l) +warn s = postMessage WarningC s +inform s = postMessage InfoC s +style s = postMessage StyleC s + + +putFlag v = modifyFlag (const v) >> return () +getFlag = modifyFlag id +modifyFlag f = do + Ms.modify $ \(x, l) -> (f x, l) + v <- Ms.get + return $ fst v + + +analyzeScopes f i = mapM (analyzeScope f i) +analyzeScope f i (Annotated pos notes t) = do + v <- getFlag + let (ret, (flag, list)) = Ms.runState (analyze f i t) (v, []) + putFlag flag + return $ Annotated pos (notes++list) ret + +analyze f i s@(T_NormalWord list) = do + f s + a <- analyzeScopes f i list + return . i $ T_NormalWord a + +analyze f i s@(T_DoubleQuoted list) = do + f s + a <- analyzeScopes f i list + return . i $ T_DoubleQuoted a + +analyze f i s@(T_DollarExpansion l) = do + f s + nl <- mapM (analyze f i) l + return . i $ T_DollarExpansion nl + +analyze f i s@(T_IoFile op file) = do + f s + a <- analyze f i op + b <- analyze f i file + return . i $ T_IoFile a b + +analyze f i s@(T_HereString word) = do + f s + a <- analyze f i word + return . i $ T_HereString a + +analyze f i s@(T_FdRedirect v t) = do + f s + a <- analyze f i t + return . i $ T_FdRedirect v a + +analyze f i s@(T_Assignment v t) = do + f s + a <- analyze f i t + return . i $ T_Assignment v a + +analyze f i s@(T_Redirecting redirs cmd) = do + f s + newRedirs <- analyzeScopes f i redirs + newCmd <- analyze f i $ cmd + return . i $ (T_Redirecting newRedirs newCmd) + +analyze f i s@(T_SimpleCommand vars cmds) = do + f s + a <- analyzeScopes f i vars + b <- analyzeScopes f i cmds + return . i $ T_SimpleCommand a b + +analyze f i s@(T_Pipeline l) = do + f s + a <- analyzeScopes f i l + return . i $ T_Pipeline a + +analyze f i s@(T_Banged l) = do + f s + a <- analyze f i l + return . i $ T_Banged a + +analyze f i s@(T_AndIf t u) = do + f s + a <- analyzeScope f i t + b <- analyzeScope f i u + return . i $ T_AndIf a b + +analyze f i s@(T_OrIf t u) = do + f s + a <- analyzeScope f i t + b <- analyzeScope f i u + return . i $ T_OrIf a b + +analyze f i s@(T_Backgrounded l) = do + f s + a <- analyze f i l + return . i $ T_Backgrounded a + +analyze f i s@(T_IfExpression conditions elses) = do + f s + newConds <- mapM (\(c, t) -> do + x <- mapM (analyze f i) c + y <- mapM (analyze f i) t + return (x, y) + ) conditions + newElses <- mapM (analyze f i) elses + return . i $ T_IfExpression newConds newElses + +analyze f i s@(T_Subshell l) = do + f s + a <- mapM (analyze f i) l + return . i $ T_Subshell a + +analyze f i s@(T_BraceGroup l) = do + f s + a <- mapM (analyze f i) l + return . i $ T_BraceGroup a + +analyze f i s@(T_WhileExpression c l) = do + f s + a <- mapM (analyze f i) c + b <- mapM (analyze f i) l + return . i $ T_WhileExpression a b + +analyze f i s@(T_UntilExpression c l) = do + f s + a <- mapM (analyze f i) c + b <- mapM (analyze f i) l + return . i $ T_UntilExpression a b + +analyze f i s@(T_ForIn v w l) = do + f s + a <- mapM (analyze f i) w + b <- mapM (analyze f i) l + return . i $ T_ForIn v a b + +analyze f i s@(T_CaseExpression word cases) = do + f s + newWord <- analyze f i word + newCases <- mapM (\(c, t) -> do + x <- mapM (analyze f i) c + y <- mapM (analyze f i) t + return (x, y) + ) cases + return . i $ T_CaseExpression newWord newCases + +analyze f i s@(T_Script l) = do + f s + a <- mapM (analyze f i) l + return . i $ T_Script a + +analyze f i s@(T_Function name body) = do + f s + a <- analyze f i body + return . i $ T_Function name a + +analyze f i s@(T_Command c) = do + f s + a <- analyzeScope f i c + return . i $ T_Command a + +analyze f i t = do + f t + return . i $ t + +doAnalysis f t = fst $ Ms.runState (analyze f id t) ((), []) +explore f d t = fst . snd $ Ms.runState (analyze f id t) (d, []) +transform i t = fst $ Ms.runState (analyze (const $ return ()) i t) ((), []) + +findNotes t = explore extractNotes [] t +sortNotes l = sortBy compareNotes l +compareNotes (OutputNote pos1 level1 _) (OutputNote pos2 level2 _) = compare (pos1, level1) (pos2, level2) +findParseNotes l = map (\(ParseProblem p level s) -> OutputNote p level s) l +-- T_UntilExpression [Token] [Token] | T_ForIn String [Token] [Token] + +getNotes s = + case rp readScript s of + (Right x, p) -> sortNotes $ (findNotes $ doAllAnalysis x) ++ (findParseNotes p) + (Left _, p) -> sortNotes $ (OutputNote (initialPos "-") ErrorC "Parsing failed"):(findParseNotes p) + +readComment = do + char '#' + anyChar `reluctantlyTill` linefeed + +readNormalWord = do + x <- many1 readNormalWordPart + return $ T_NormalWord x + +readNormalWordPart = readSingleQuoted <|> readDoubleQuoted <|> readDollar <|> readBraced <|> readBackTicked <|> (annotated "normal literal" $ readNormalLiteral) + +readSingleQuoted = annotated "single quoted string" $ do + singleQuote + s <- readSingleQuotedPart `reluctantlyTill` singleQuote + singleQuote "End single quoted string" + + let string = concat s + return (T_SingleQuoted string) `attempting` do + x <- lookAhead anyChar + when (isAlpha x && isAlpha (last string)) $ parseProblem WarningC "This apostrophe terminated the single quoted string." + +readSingleQuotedLiteral = do + singleQuote + strs <- many1 readSingleQuotedPart + singleQuote + return $ concat strs + +readSingleQuotedPart = + readSingleEscaped + <|> anyChar `reluctantlyTill1` (singleQuote <|> backslash) + +readBackTicked = annotated "backtick expansion" $ do + parseNote StyleC "`..` style expansion is deprecated, use $(..) instead if you want my help" + pos <- getPosition + char '`' + f <- readGenericLiteral (char '`') + char '`' `attempting` (eof >> parseProblemAt pos ErrorC "Can't find terminating backtick for this one") + return $ T_Literal f + + +readDoubleQuoted = annotated "double quoted string" $ do + doubleQuote + x <- many doubleQuotedPart + doubleQuote "End double quoted" + return $ T_DoubleQuoted x + +doubleQuotedPart = readDoubleLiteral <|> readDollar <|> readBackTicked + +readDoubleQuotedLiteral = do + doubleQuote + x <- readDoubleLiteral + doubleQuote + return $ dropAnnotation x + +readDoubleLiteral = annotated "double literal" $ do + s <- many1 readDoubleLiteralPart + return $ T_Literal (concat s) + +readDoubleLiteralPart = do + x <- (readDoubleEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` doubleQuotable + return $ concat x + +readNormalLiteral = do + s <- many1 readNormalLiteralPart + return $ T_Literal (concat s) + +readNormalLiteralPart = do + readNormalEscaped <|> (anyChar `reluctantlyTill1` quotable) + +readNormalEscaped = do + backslash + pos <- getPosition + do + next <- (quotable <|> oneOf "?*[]") + return $ if next == '\n' then "" else [next] + <|> + do + next <- anyChar "No character after \\" + parseNoteAt pos WarningC $ "This character doesn't need escaping here, the \\ is ignored" + return [next] + +readSingleEscaped = do + s <- backslash + let attempt level p msg = do { try $ parseNote level msg; x <- p; return [s,x]; } + + do { + x <- singleQuote; + parseProblem InfoC "Are you trying to escape a single quote? echo 'You'\\''re doing it wrong'."; + return [s,x]; + } + <|> attempt InfoC linefeed "You don't break lines with \\ in single quotes, it results in literal backslash-linefeed." + <|> do + x <- anyChar + return [s,x] + + +readDoubleEscaped = do + bs <- backslash + (linefeed >> return "") + <|> (doubleQuotable >>= return . return) + <|> (anyChar >>= (return . \x -> [bs, x])) + + +readGenericLiteral endExp = do + strings <- many (readGenericEscaped <|> anyChar `reluctantlyTill1` endExp) + return $ concat strings + +readGenericLiteral1 endExp = do + strings <- many1 (readGenericEscaped <|> anyChar `reluctantlyTill1` endExp) + return $ concat strings + +readGenericEscaped = do + backslash + x <- anyChar + return $ if x == '\n' then [] else [x] + +readBraced = annotated "{1,2..3} expression" $ try $ do + let strip (T_Literal s) = return ("\"" ++ s ++ "\"") + char '{' + str <- many1 ((readDoubleQuotedLiteral >>= (strip )) <|> readGenericLiteral1 (oneOf "}" <|> whitespace)) + char '}' + return $ T_BraceExpansion $ concat str + +readDollar = readDollarArithmetic <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable <|> readDollarLonely + + +readParenLiteralHack = do + strs <- ((anyChar >>= \x -> return [x]) <|> readParenHack) `reluctantlyTill1` (string "))") + return $ concat strs + +readParenHack = do + char '(' + x <- many anyChar + char ')' + return $ "(" ++ x ++ ")" + +readDollarArithmetic = annotated "$(( )) expression" $ do + try (string "$((") + -- TODO + str <- readParenLiteralHack + string "))" + return (T_DollarArithmetic str) + +readDollarBraced = annotated "${ } expression" $ do + try (string "${") + -- TODO + str <- readGenericLiteral (char '}') + char '}' "matching }" + return $ (T_DollarBraced str) + +readDollarExpansion = annotated "$( )" $ do + try (string "$(") + cmds <- readCompoundList + char ')' + return $ (T_DollarExpansion cmds) + +readDollarVariable = annotated "$variable" $ do + let singleCharred p = do + n <- p + return (T_DollarVariable [n]) `attempting` do + pos <- getPosition + num <- lookAhead $ many1 p + parseNoteAt pos ErrorC $ "$" ++ (n:num) ++ " is equivalent to ${" ++ [n] ++ "}"++ num + + let positional = singleCharred digit + let special = singleCharred specialVariable + + let regular = do + name <- readVariableName + return $ T_DollarVariable (name) + + char '$' + positional <|> special <|> regular + +readVariableName = do + f <- variableStart + rest <- many variableChars + return (f:rest) + +readDollarLonely = annotated "lonely $" $ do + parseNote ErrorC "$ is not used specially and should therefore be escaped" + char '$' + return $ T_Literal "$" + +readHereDoc = annotated "here document" $ do + let stripLiteral (T_Literal x) = x + stripLiteral (T_SingleQuoted x) = x + try $ string "<<" + dashed <- (char '-' >> return True) <|> return False + tokenPosition <- getPosition + spacing + (quoted, endToken) <- (readNormalLiteral >>= (\x -> return (False, stripLiteral x)) ) + <|> (readDoubleQuotedLiteral >>= return . (\x -> (True, stripLiteral x))) + <|> (readSingleQuotedLiteral >>= return . (\x -> (True, x))) + spacing + + hereInfo <- anyChar `reluctantlyTill` (linefeed >> spacing >> (string endToken) >> (disregard whitespace <|> eof)) + + do + linefeed + spaces <- spacing + verifyHereDoc dashed quoted spaces hereInfo + token <- string endToken + return $ T_FdRedirect "" $ T_HereDoc dashed quoted hereInfo + `attempting` (eof >> debugHereDoc tokenPosition endToken hereInfo) + +verifyHereDoc dashed quoted spacing hereInfo = do + when (not dashed && spacing /= "") $ parseNote ErrorC "When using << instead of <<-, the end tokens can't be indented" + when (dashed && filter (/= '\t') spacing /= "" ) $ parseNote ErrorC "When using <<-, you can only indent with tabs" + return () + +debugHereDoc pos endToken doc = + if endToken `isInfixOf` doc + then parseProblemAt pos ErrorC (endToken ++ " was part of the here document, but not by itself at the start of the line") + else if (map toLower endToken) `isInfixOf` (map toLower doc) + then parseProblemAt pos ErrorC (endToken ++ " appears in the here document, but with different case") + else parseProblemAt pos ErrorC ("Couldn't find end token `" ++ endToken ++ "' in the here document ") + + +readFilename = readNormalWord +readIoFileOp = choice [g_LESSAND, g_GREATAND, g_DGREAT, g_LESSGREAT, g_CLOBBER, string "<" >> return T_Less, string ">" >> return T_Greater ] +readIoFile = do + op <- readIoFileOp + spacing + file <- readFilename + return $ T_FdRedirect "" $ T_IoFile op file +readIoNumber = try $ do + x <- many1 digit + lookAhead readIoFileOp + return x +readIoNumberRedirect = annotated "fd io redirect" $ do + n <- readIoNumber + op <- merging readHereString <|> merging readHereDoc <|> readIoFile + let actualOp = case op of T_FdRedirect "" x -> x + spacing + return $ T_FdRedirect n actualOp + +readIoRedirect = annotated "io redirect" $ choice [ merging readIoNumberRedirect, merging readHereString, merging readHereDoc, readIoFile ] `thenSkip` spacing + +readRedirectList = many1 readIoRedirect + +readHereString = annotated "here string" $ do + try $ string "<<<" + spacing + word <- readNormalWord + return $ T_FdRedirect "" $ T_HereString word + +readNewlineList = many1 ((newline <|> carriageReturn) `thenSkip` spacing) +readLineBreak = optional readNewlineList + +readSeparatorOp = do + notFollowedBy (g_AND_IF <|> g_DSEMI) + f <- char ';' <|> char '&' + spacing + return f + +readSequentialSep = (disregard $ g_Semi >> readLineBreak) <|> (disregard readNewlineList) +readSeparator = + do + separator <- readSeparatorOp + readLineBreak + return separator + <|> + do + readNewlineList + return '\n' + +makeSimpleCommand tokens = + let (assignment, rest) = partition (\x -> case dropAnnotation x of T_Assignment _ _ -> True; _ -> False) tokens + in let (redirections, rest2) = partition (\x -> case dropAnnotation x of T_FdRedirect _ _ -> True; _ -> False) rest + in T_Redirecting redirections $ T_SimpleCommand assignment rest2 + +readSimpleCommand = annotated "simple command" $ do + prefix <- option [] readCmdPrefix + cmd <- option [] $ do { f <- annotated "command name" readCmdName; return [f]; } + when (null prefix && null cmd) $ fail "No command" + if null cmd + then return $ makeSimpleCommand prefix + else do + suffix <- option [] readCmdSuffix + return $ makeSimpleCommand (prefix ++ cmd ++ suffix) + +readPipeline = annotated "Pipeline" $ do + notFollowedBy $ try readKeyword + do + g_Bang `thenSkip` spacing + pipe <- readPipeSequence + return $ T_Banged pipe + <|> do + readPipeSequence + +readAndOr = (flip (>>=)) (return . T_Command) $ chainr1 readPipeline $ do + pos <- getPosition + op <- g_AND_IF <|> g_OR_IF + readLineBreak + return $ \a b -> + blankAnnotation pos $ + case op of T_AND_IF -> T_AndIf a b + T_OR_IF -> T_OrIf a b + +readTerm = do + m <- readAndOr + readTerm' m + +readTerm' current = + do + sep <- readSeparator + more <- (option T_EOF $ readAndOr) + case more of T_EOF -> return [transformWithSeparator sep current] + _ -> do + list <- readTerm' more + return $ (transformWithSeparator sep current : list) + <|> + return [current] + +transformWithSeparator '&' = T_Backgrounded +transformWithSeparator _ = id + + +readPipeSequence = do + list <- readCommand `sepBy1` (readPipe `thenSkip` (spacing >> readLineBreak)) + spacing + return $ T_Pipeline list + +readPipe = do + notFollowedBy g_OR_IF + char '|' `thenSkip` spacing + +readCommand = (readCompoundCommand <|> readSimpleCommand) + +readCmdName = do + f <- readNormalWord + spacing + return f + +readCmdWord = do + f <- readNormalWord + spacing + return f + +readIfClause = annotated "if statement" $ do + (condition, action) <- readIfPart + elifs <- many readElifPart + elses <- option [] readElsePart + g_Fi + return $ T_IfExpression ((condition, action):elifs) elses + +readIfPart = do + g_If + allspacing + condition <- readTerm + g_Then + allspacing + action <- readTerm + return (condition, action) + +readElifPart = do + g_Elif + allspacing + condition <- readTerm + g_Then + allspacing + action <- readTerm + return (condition, action) + +readElsePart = do + g_Else + allspacing + readTerm + +readSubshell = annotated "subshell group" $ do + char '(' + allspacing + list <- readCompoundList + allspacing + char ')' + return $ T_Subshell list + +readBraceGroup = annotated "brace group" $ do + char '{' + allspacing + list <- readTerm + allspacing + char '}' + return $ T_BraceGroup list + +readWhileClause = annotated "while loop" $ do + g_While + condition <- readTerm + statements <- readDoGroup + return $ T_WhileExpression condition statements + +readUntilClause = annotated "until loop" $ do + g_Until + condition <- readTerm + statements <- readDoGroup + return $ T_UntilExpression condition statements + +readDoGroup = do + pos <- getPosition + g_Do + allspacing + (eof >> return []) <|> + do + commands <- readCompoundList + disregard g_Done <|> eof -- stunted support + return commands + <|> do + parseProblemAt pos ErrorC "Can't find the 'done' for this 'do'" + fail "No done" + +readForClause = annotated "for loop" $ do + g_For + spacing + name <- readVariableName + allspacing + values <- readInClause <|> (readSequentialSep >> return []) + group <- readDoGroup <|> (allspacing >> eof >> return []) -- stunted support + return $ T_ForIn name values group + +readInClause = do + g_In + things <- (readCmdWord) `reluctantlyTill` + (disregard (g_Semi) <|> disregard linefeed <|> disregard g_Do) + + do { + lookAhead (g_Do); + parseNote ErrorC "You need a line feed or semicolon before the 'do' (in Bash)"; + } <|> do { + optional $ g_Semi; + disregard allspacing; + } + + return things + +readCaseClause = annotated "case statement" $ do + g_Case + word <- readNormalWord + spacing + g_In + readLineBreak + list <- readCaseList + g_Esac + return $ T_CaseExpression word list + +readCaseList = many readCaseItem + +readCaseItem = do + notFollowedBy g_Esac + optional g_Lparen + spacing + pattern <- readPattern + g_Rparen + readLineBreak + list <- ((lookAhead g_DSEMI >> return []) <|> readCompoundList) + (g_DSEMI <|> lookAhead (readLineBreak >> g_Esac)) + readLineBreak + return (pattern, list) + +readFunctionDefinition = annotated "function definition" $ do + name <- try readFunctionSignature + allspacing + (disregard (lookAhead g_Lbrace) <|> parseProblem ErrorC "Expected a { to open the function definition") + group <- merging readBraceGroup + return $ T_Function name group + + +readFunctionSignature = do + (optional $ try (string "function " >> parseNote StyleC "Don't use 'function' in front of function definitions")) + name <- readVariableName + spacing + g_Lparen + g_Rparen + return name + + +readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing) + + +readCompoundCommand = annotated "compound command" $ do + cmd <- merging $ choice [ readBraceGroup, readSubshell, readWhileClause, readUntilClause, readIfClause, readForClause, readCaseClause, readFunctionDefinition] + spacing + redirs <- many readIoRedirect + return $ T_Redirecting redirs $ cmd + + +readCompoundList = readTerm + +readCmdPrefix = many1 (readIoRedirect <|> readAssignmentWord) +readCmdSuffix = many1 (readIoRedirect <|> annotated "normal word" readCmdWord) + +readAssignmentWord = annotated "assignment" $ try $ do + optional (char '$' >> parseNote ErrorC "Don't use $ on the left side of assignments") + variable <- readVariableName + space <- spacing + pos <- getPosition + char '=' + space2 <- spacing + value <- readNormalWord + spacing + when (space ++ space2 /= "") $ parseNoteAt pos ErrorC "Don't put spaces around the = in assignments" + return $ T_Assignment variable value + + +tryToken s t = try (string s >> spacing >> return t) +tryWordToken s t = tryParseWordToken (string s) t `thenSkip` spacing +tryParseWordToken parser t = try (parser >> (lookAhead (eof <|> disregard whitespace))) >> return t + +g_AND_IF = tryToken "&&" T_AND_IF +g_OR_IF = tryToken "||" T_OR_IF +g_DSEMI = tryToken ";;" T_DSEMI +g_DLESS = tryToken "<<" T_DLESS +g_DGREAT = tryToken ">>" T_DGREAT +g_LESSAND = tryToken "<&" T_LESSAND +g_GREATAND = tryToken ">&" T_GREATAND +g_LESSGREAT = tryToken "<>" T_LESSGREAT +g_DLESSDASH = tryToken "<<-" T_DLESSDASH +g_CLOBBER = tryToken ">|" T_CLOBBER +g_OPERATOR = g_AND_IF <|> g_OR_IF <|> g_DSEMI <|> g_DLESSDASH <|> g_DLESS <|> g_DGREAT <|> g_LESSAND <|> g_GREATAND <|> g_LESSGREAT + +g_If = tryWordToken "if" T_If +g_Then = tryWordToken "then" T_Then +g_Else = tryWordToken "else" T_Else +g_Elif = tryWordToken "elif" T_Elif +g_Fi = tryWordToken "fi" T_Fi +g_Do = tryWordToken "do" T_Do +g_Done = tryWordToken "done" T_Done +g_Case = tryWordToken "case" T_Case +g_Esac = tryWordToken "esac" T_Esac +g_While = tryWordToken "while" T_While +g_Until = tryWordToken "until" T_Until +g_For = tryWordToken "for" T_For +g_In = tryWordToken "in" T_In +g_Lbrace = tryWordToken "{" T_Lbrace +g_Rbrace = tryWordToken "}" T_Rbrace + +g_Lparen = tryToken "(" T_Lparen +g_Rparen = tryToken ")" T_Rparen +g_Bang = tryToken "!" T_Bang + +g_Semi = do + notFollowedBy g_DSEMI + tryToken ";" T_Semi + +readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ] + +ifParse p t f = do + (lookAhead (try p) >> t) <|> f + +wtf = do + x <- many anyChar + parseProblem ErrorC x + +readScript = do + do { + allspacing; + commands <- readTerm; + eof <|> (parseProblem WarningC "Stopping here, because I can't parse this command"); + return $ T_Script commands; + } <|> do { + parseProblem WarningC "Couldn't read any commands"; + wtf; + return T_EOF; + } + +shpell s = rp readScript s +rp p s = Ms.runState (runParserT p StackEmpty "-" s) [] + +-------- Destructively simplify AST + +simplify (T_Redirecting [] t) = t +simplify (T_Pipeline [x]) = dropAnnotation x +simplify (T_NormalWord [x]) = dropAnnotation x +simplify t = t + +-------- Analytics +doAllAnalysis t = foldl (\v f -> doAnalysis f v) t checks + +getAst s = case rp readScript s of (Right parsed, _) -> parsed +getAst2 s = case rp readScript s of (Right parsed, _) -> transform simplify parsed +lol (Right x, _) = x + +deadSimple (T_NormalWord l) = [concat (concatMap (deadSimple . dropAnnotation) l)] +deadSimple (T_DoubleQuoted l) = ["\"" ++(concat (concatMap (deadSimple . dropAnnotation) l)) ++ "\""] +deadSimple (T_SingleQuoted s) = [s] +deadSimple (T_DollarVariable _) = ["${VAR}"] +deadSimple (T_DollarBraced _) = ["${VAR}"] +deadSimple (T_DollarArithmetic _) = ["${VAR}"] +deadSimple (T_DollarExpansion _) = ["${VAR}"] +deadSimple (T_Literal x) = [x] +deadSimple (T_SimpleCommand vars words) = concatMap (deadSimple . dropAnnotation) words +deadSimple (T_Redirecting _ foo) = deadSimple foo +deadSimple _ = [] + + +checks = [checkUuoc] +checkUuoc (T_Pipeline ((Annotated _ _ x):_:_)) = case (deadSimple x) of ["cat", _] -> style "UUOC: Instead of 'cat a | b', use 'b < a'" + _ -> return () +checkUuoc _ = return () + + +main = do + s <- getContents +-- case rp readScript s of (Right parsed, _) -> putStrLn . show $ transform simplify parsed +-- (Left x, y) -> putStrLn $ "Can't parse: " ++ (show (x,y)) + + mapM (putStrLn . show) $ getNotes s diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml deleted file mode 100644 index 83ac1e7..0000000 --- a/snap/snapcraft.yaml +++ /dev/null @@ -1,57 +0,0 @@ -name: shellcheck -summary: A shell script static analysis tool -description: | - ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh - shell scripts. - - The goals of ShellCheck are - - - To point out and clarify typical beginner's syntax issues that cause a - shell to give cryptic error messages. - - - To point out and clarify typical intermediate level semantic problems that - cause a shell to behave strangely and counter-intuitively. - - - To point out subtle caveats, corner cases and pitfalls that may cause an - advanced user's otherwise working script to fail under future - circumstances. - - By default ShellCheck can only check non-hidden files under /home, to make - ShellCheck be able to check files under /media and /run/media you must - connect it to the `removable-media` interface manually: - - # snap connect shellcheck:removable-media - -version: git -base: core24 -grade: stable -confinement: strict - -apps: - shellcheck: - command: usr/bin/shellcheck - plugs: [home, removable-media] - environment: - LANG: C.UTF-8 - -parts: - shellcheck: - plugin: dump - source: . - build-packages: - - cabal-install - override-build: | - # Give ourselves enough memory to build - fallocate -l 2G /tmp/swap - chmod 0600 /tmp/swap - mkswap /tmp/swap - if ! swapon /tmp/swap; then - echo "Could not enable swap file, continuing anyway" - rm /tmp/swap - fi - - cabal update - cabal install -j - - install -d "${CRAFT_PART_INSTALL}/usr/bin" - install --strip ~/.cabal/bin/shellcheck "${CRAFT_PART_INSTALL}/usr/bin" diff --git a/src/ShellCheck/AST.hs b/src/ShellCheck/AST.hs deleted file mode 100644 index b04abee..0000000 --- a/src/ShellCheck/AST.hs +++ /dev/null @@ -1,289 +0,0 @@ -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE DeriveGeneric, DeriveAnyClass, DeriveTraversable, PatternSynonyms #-} -module ShellCheck.AST where - -import GHC.Generics (Generic) -import Control.Monad.Identity -import Control.DeepSeq -import Text.Parsec -import qualified ShellCheck.Regex as Re -import Prelude hiding (id) - -newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData) - -data Quoted = Quoted | Unquoted deriving (Show, Eq) -data Dashed = Dashed | Undashed deriving (Show, Eq) -data Piped = Piped | Unpiped deriving (Show, Eq) -data AssignmentMode = Assign | Append deriving (Show, Eq) -newtype FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq) -newtype FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq) -data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq) - -newtype Root = Root Token -data Token = OuterToken Id (InnerToken Token) deriving (Show) - -data InnerToken t = - Inner_TA_Binary String t t - | Inner_TA_Assignment String t t - | Inner_TA_Variable String [t] - | Inner_TA_Expansion [t] - | Inner_TA_Sequence [t] - | Inner_TA_Parenthesis t - | Inner_TA_Trinary t t t - | Inner_TA_Unary String t - | Inner_TC_And ConditionType String t t - | Inner_TC_Binary ConditionType String t t - | Inner_TC_Group ConditionType t - | Inner_TC_Nullary ConditionType t - | Inner_TC_Or ConditionType String t t - | Inner_TC_Unary ConditionType String t - | Inner_TC_Empty ConditionType - | Inner_T_AND_IF - | Inner_T_AndIf t t - | Inner_T_Arithmetic t - | Inner_T_Array [t] - | Inner_T_IndexedElement [t] t - -- Store the index as string, and parse as arithmetic or string later - | Inner_T_UnparsedIndex SourcePos String - | Inner_T_Assignment AssignmentMode String [t] t - | Inner_T_Backgrounded t - | Inner_T_Backticked [t] - | Inner_T_Bang - | Inner_T_Banged t - | Inner_T_BraceExpansion [t] - | Inner_T_BraceGroup [t] - | Inner_T_CLOBBER - | Inner_T_Case - | Inner_T_CaseExpression t [(CaseType, [t], [t])] - | Inner_T_Condition ConditionType t - | Inner_T_DGREAT - | Inner_T_DLESS - | Inner_T_DLESSDASH - | Inner_T_DSEMI - | Inner_T_Do - | Inner_T_DollarArithmetic t - | Inner_T_DollarBraced Bool t - | Inner_T_DollarBracket t - | Inner_T_DollarDoubleQuoted [t] - | Inner_T_DollarExpansion [t] - | Inner_T_DollarSingleQuoted String - | Inner_T_DollarBraceCommandExpansion Piped [t] - | Inner_T_Done - | Inner_T_DoubleQuoted [t] - | Inner_T_EOF - | Inner_T_Elif - | Inner_T_Else - | Inner_T_Esac - | Inner_T_Extglob String [t] - | Inner_T_FdRedirect String t - | Inner_T_Fi - | Inner_T_For - | Inner_T_ForArithmetic t t t [t] - | Inner_T_ForIn String [t] [t] - | Inner_T_Function FunctionKeyword FunctionParentheses String t - | Inner_T_GREATAND - | Inner_T_Glob String - | Inner_T_Greater - | Inner_T_HereDoc Dashed Quoted String [t] - | Inner_T_HereString t - | Inner_T_If - | Inner_T_IfExpression [([t],[t])] [t] - | Inner_T_In - | Inner_T_IoFile t t - | Inner_T_IoDuplicate t String - | Inner_T_LESSAND - | Inner_T_LESSGREAT - | Inner_T_Lbrace - | Inner_T_Less - | Inner_T_Literal String - | Inner_T_Lparen - | Inner_T_NEWLINE - | Inner_T_NormalWord [t] - | Inner_T_OR_IF - | Inner_T_OrIf t t - | Inner_T_ParamSubSpecialChar String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz} - | Inner_T_Pipeline [t] [t] -- [Pipe separators] [Commands] - | Inner_T_ProcSub String [t] - | Inner_T_Rbrace - | Inner_T_Redirecting [t] t - | Inner_T_Rparen - | Inner_T_Script t [t] -- Shebang T_Literal, followed by script. - | Inner_T_Select - | Inner_T_SelectIn String [t] [t] - | Inner_T_Semi - | Inner_T_SimpleCommand [t] [t] - | Inner_T_SingleQuoted String - | Inner_T_Subshell [t] - | Inner_T_Then - | Inner_T_Until - | Inner_T_UntilExpression [t] [t] - | Inner_T_While - | Inner_T_WhileExpression [t] [t] - | Inner_T_Annotation [Annotation] t - | Inner_T_Pipe String - | Inner_T_CoProc (Maybe Token) t - | Inner_T_CoProcBody t - | Inner_T_Include t - | Inner_T_SourceCommand t t - | Inner_T_BatsTest String t - deriving (Show, Eq, Functor, Foldable, Traversable) - -data Annotation = - DisableComment Integer Integer -- [from, to) - | EnableComment String - | SourceOverride String - | ShellOverride String - | SourcePath String - | ExternalSources Bool - | ExtendedAnalysis Bool - deriving (Show, Eq) -data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) - -pattern T_AND_IF id = OuterToken id Inner_T_AND_IF -pattern T_Bang id = OuterToken id Inner_T_Bang -pattern T_Case id = OuterToken id Inner_T_Case -pattern TC_Empty id typ = OuterToken id (Inner_TC_Empty typ) -pattern T_CLOBBER id = OuterToken id Inner_T_CLOBBER -pattern T_DGREAT id = OuterToken id Inner_T_DGREAT -pattern T_DLESS id = OuterToken id Inner_T_DLESS -pattern T_DLESSDASH id = OuterToken id Inner_T_DLESSDASH -pattern T_Do id = OuterToken id Inner_T_Do -pattern T_DollarSingleQuoted id str = OuterToken id (Inner_T_DollarSingleQuoted str) -pattern T_Done id = OuterToken id Inner_T_Done -pattern T_DSEMI id = OuterToken id Inner_T_DSEMI -pattern T_Elif id = OuterToken id Inner_T_Elif -pattern T_Else id = OuterToken id Inner_T_Else -pattern T_EOF id = OuterToken id Inner_T_EOF -pattern T_Esac id = OuterToken id Inner_T_Esac -pattern T_Fi id = OuterToken id Inner_T_Fi -pattern T_For id = OuterToken id Inner_T_For -pattern T_Glob id str = OuterToken id (Inner_T_Glob str) -pattern T_GREATAND id = OuterToken id Inner_T_GREATAND -pattern T_Greater id = OuterToken id Inner_T_Greater -pattern T_If id = OuterToken id Inner_T_If -pattern T_In id = OuterToken id Inner_T_In -pattern T_Lbrace id = OuterToken id Inner_T_Lbrace -pattern T_Less id = OuterToken id Inner_T_Less -pattern T_LESSAND id = OuterToken id Inner_T_LESSAND -pattern T_LESSGREAT id = OuterToken id Inner_T_LESSGREAT -pattern T_Literal id str = OuterToken id (Inner_T_Literal str) -pattern T_Lparen id = OuterToken id Inner_T_Lparen -pattern T_NEWLINE id = OuterToken id Inner_T_NEWLINE -pattern T_OR_IF id = OuterToken id Inner_T_OR_IF -pattern T_ParamSubSpecialChar id str = OuterToken id (Inner_T_ParamSubSpecialChar str) -pattern T_Pipe id str = OuterToken id (Inner_T_Pipe str) -pattern T_Rbrace id = OuterToken id Inner_T_Rbrace -pattern T_Rparen id = OuterToken id Inner_T_Rparen -pattern T_Select id = OuterToken id Inner_T_Select -pattern T_Semi id = OuterToken id Inner_T_Semi -pattern T_SingleQuoted id str = OuterToken id (Inner_T_SingleQuoted str) -pattern T_Then id = OuterToken id Inner_T_Then -pattern T_UnparsedIndex id pos str = OuterToken id (Inner_T_UnparsedIndex pos str) -pattern T_Until id = OuterToken id Inner_T_Until -pattern T_While id = OuterToken id Inner_T_While -pattern TA_Assignment id op t1 t2 = OuterToken id (Inner_TA_Assignment op t1 t2) -pattern TA_Binary id op t1 t2 = OuterToken id (Inner_TA_Binary op t1 t2) -pattern TA_Expansion id t = OuterToken id (Inner_TA_Expansion t) -pattern T_AndIf id t u = OuterToken id (Inner_T_AndIf t u) -pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t) -pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c) -pattern T_Array id t = OuterToken id (Inner_T_Array t) -pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l) -pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t) -pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value) -pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3) -pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1) -pattern TA_Variable id str t = OuterToken id (Inner_TA_Variable str t) -pattern T_Backgrounded id l = OuterToken id (Inner_T_Backgrounded l) -pattern T_Backticked id list = OuterToken id (Inner_T_Backticked list) -pattern T_Banged id l = OuterToken id (Inner_T_Banged l) -pattern T_BatsTest id name t = OuterToken id (Inner_T_BatsTest name t) -pattern T_BraceExpansion id list = OuterToken id (Inner_T_BraceExpansion list) -pattern T_BraceGroup id l = OuterToken id (Inner_T_BraceGroup l) -pattern TC_And id typ str t1 t2 = OuterToken id (Inner_TC_And typ str t1 t2) -pattern T_CaseExpression id word cases = OuterToken id (Inner_T_CaseExpression word cases) -pattern TC_Binary id typ op lhs rhs = OuterToken id (Inner_TC_Binary typ op lhs rhs) -pattern TC_Group id typ token = OuterToken id (Inner_TC_Group typ token) -pattern TC_Nullary id typ token = OuterToken id (Inner_TC_Nullary typ token) -pattern T_Condition id typ token = OuterToken id (Inner_T_Condition typ token) -pattern T_CoProcBody id t = OuterToken id (Inner_T_CoProcBody t) -pattern T_CoProc id var body = OuterToken id (Inner_T_CoProc var body) -pattern TC_Or id typ str t1 t2 = OuterToken id (Inner_TC_Or typ str t1 t2) -pattern TC_Unary id typ op token = OuterToken id (Inner_TC_Unary typ op token) -pattern T_DollarArithmetic id c = OuterToken id (Inner_T_DollarArithmetic c) -pattern T_DollarBraceCommandExpansion id pipe list = OuterToken id (Inner_T_DollarBraceCommandExpansion pipe list) -pattern T_DollarBraced id braced op = OuterToken id (Inner_T_DollarBraced braced op) -pattern T_DollarBracket id c = OuterToken id (Inner_T_DollarBracket c) -pattern T_DollarDoubleQuoted id list = OuterToken id (Inner_T_DollarDoubleQuoted list) -pattern T_DollarExpansion id list = OuterToken id (Inner_T_DollarExpansion list) -pattern T_DoubleQuoted id list = OuterToken id (Inner_T_DoubleQuoted list) -pattern T_Extglob id str l = OuterToken id (Inner_T_Extglob str l) -pattern T_FdRedirect id v t = OuterToken id (Inner_T_FdRedirect v t) -pattern T_ForArithmetic id a b c group = OuterToken id (Inner_T_ForArithmetic a b c group) -pattern T_ForIn id v w l = OuterToken id (Inner_T_ForIn v w l) -pattern T_Function id a b name body = OuterToken id (Inner_T_Function a b name body) -pattern T_HereDoc id d q str l = OuterToken id (Inner_T_HereDoc d q str l) -pattern T_HereString id word = OuterToken id (Inner_T_HereString word) -pattern T_IfExpression id conditions elses = OuterToken id (Inner_T_IfExpression conditions elses) -pattern T_Include id script = OuterToken id (Inner_T_Include script) -pattern T_IndexedElement id indices t = OuterToken id (Inner_T_IndexedElement indices t) -pattern T_IoDuplicate id op num = OuterToken id (Inner_T_IoDuplicate op num) -pattern T_IoFile id op file = OuterToken id (Inner_T_IoFile op file) -pattern T_NormalWord id list = OuterToken id (Inner_T_NormalWord list) -pattern T_OrIf id t u = OuterToken id (Inner_T_OrIf t u) -pattern T_Pipeline id l1 l2 = OuterToken id (Inner_T_Pipeline l1 l2) -pattern T_ProcSub id typ l = OuterToken id (Inner_T_ProcSub typ l) -pattern T_Redirecting id redirs cmd = OuterToken id (Inner_T_Redirecting redirs cmd) -pattern T_Script id shebang list = OuterToken id (Inner_T_Script shebang list) -pattern T_SelectIn id v w l = OuterToken id (Inner_T_SelectIn v w l) -pattern T_SimpleCommand id vars cmds = OuterToken id (Inner_T_SimpleCommand vars cmds) -pattern T_SourceCommand id includer t_include = OuterToken id (Inner_T_SourceCommand includer t_include) -pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l) -pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l) -pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) - -{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parenthesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-} - -instance Eq Token where - OuterToken _ a == OuterToken _ b = a == b - -analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token -analyze f g i = - round - where - round t@(OuterToken id it) = do - f t - newIt <- traverse round it - g t - i (OuterToken id newIt) - -getId :: Token -> Id -getId (OuterToken id _) = id - -blank :: Monad m => Token -> m () -blank = const $ return () -doAnalysis :: Monad m => (Token -> m ()) -> Token -> m Token -doAnalysis f = analyze f blank return -doStackAnalysis :: Monad m => (Token -> m ()) -> (Token -> m ()) -> Token -> m Token -doStackAnalysis startToken endToken = analyze startToken endToken return -doTransform :: (Token -> Token) -> Token -> Token -doTransform i = runIdentity . analyze blank blank (return . i) - diff --git a/src/ShellCheck/ASTLib.hs b/src/ShellCheck/ASTLib.hs deleted file mode 100644 index 7ddebe4..0000000 --- a/src/ShellCheck/ASTLib.hs +++ /dev/null @@ -1,926 +0,0 @@ -{- - Copyright 2012-2021 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -module ShellCheck.ASTLib where - -import ShellCheck.AST -import ShellCheck.Prelude -import ShellCheck.Regex - -import Control.Monad.Writer -import Control.Monad -import Data.Char -import Data.Functor -import Data.Functor.Identity -import Data.List -import Data.Maybe -import qualified Data.List.NonEmpty as NE -import qualified Data.Map as Map -import Numeric (showHex) - -import Test.QuickCheck - -arguments (T_SimpleCommand _ _ (cmd:args)) = args - --- Is this a type of loop? -isLoop t = case t of - T_WhileExpression {} -> True - T_UntilExpression {} -> True - T_ForIn {} -> True - T_ForArithmetic {} -> True - T_SelectIn {} -> True - _ -> False - --- Will this split into multiple words when used as an argument? -willSplit x = - case x of - T_DollarBraced {} -> True - T_DollarExpansion {} -> True - T_Backticked {} -> True - T_BraceExpansion {} -> True - T_Glob {} -> True - T_Extglob {} -> True - T_DoubleQuoted _ l -> any willBecomeMultipleArgs l - T_NormalWord _ l -> any willSplit l - _ -> False - -isGlob t = case t of - T_Extglob {} -> True - T_Glob {} -> True - T_NormalWord _ l -> any isGlob l || hasSplitRange l - _ -> False - where - -- foo[x${var}y] gets parsed as foo,[,x,$var,y], - -- so check if there's such an interval - hasSplitRange l = - let afterBracket = dropWhile (not . isHalfOpenRange) l - in any isClosingRange afterBracket - - isHalfOpenRange t = - case t of - T_Literal _ "[" -> True - _ -> False - - isClosingRange t = - case t of - T_Literal _ str -> ']' `elem` str - _ -> False - - --- Is this shell word a constant? -isConstant token = - case token of - -- This ignores some cases like ~"foo": - T_NormalWord _ (T_Literal _ ('~':_) : _) -> False - T_NormalWord _ l -> all isConstant l - T_DoubleQuoted _ l -> all isConstant l - T_SingleQuoted _ _ -> True - T_Literal _ _ -> True - _ -> False - --- Is this an empty literal? -isEmpty token = - case token of - T_NormalWord _ l -> all isEmpty l - T_DoubleQuoted _ l -> all isEmpty l - T_SingleQuoted _ "" -> True - T_Literal _ "" -> True - _ -> False - --- Quick&lazy oversimplification of commands, throwing away details --- and returning a list like ["find", ".", "-name", "${VAR}*" ]. -oversimplify token = - case token of - (T_NormalWord _ l) -> [concat (concatMap oversimplify l)] - (T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)] - (T_SingleQuoted _ s) -> [s] - (T_DollarBraced _ _ _) -> ["${VAR}"] - (T_DollarArithmetic _ _) -> ["${VAR}"] - (T_DollarExpansion _ _) -> ["${VAR}"] - (T_Backticked _ _) -> ["${VAR}"] - (T_Glob _ s) -> [s] - (T_Pipeline _ _ [x]) -> oversimplify x - (T_Literal _ x) -> [x] - (T_ParamSubSpecialChar _ x) -> [x] - (T_SimpleCommand _ vars words) -> concatMap oversimplify words - (T_Redirecting _ _ foo) -> oversimplify foo - (T_DollarSingleQuoted _ s) -> [s] - (T_Annotation _ _ s) -> oversimplify s - -- Workaround for let "foo = bar" parsing - (TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v - _ -> [] - - --- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar", --- each in a tuple of (token, stringFlag). Non-flag arguments are added with --- stringFlag == "". -getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) = - let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args - (flagArgs, rest) = break (stopCondition . snd) tokenAndText - in - concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest - where - flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ] - flag (x, '-':args) = map (\v -> (x, [v])) args - flag (x, _) = [ (x, "") ] -getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command" - --- Get all flags in a GNU way, up until -- -getAllFlags :: Token -> [(Token, String)] -getAllFlags = getFlagsUntil (== "--") --- Get all flags in a BSD way, up until first non-flag argument or -- -getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x)) - --- Check if a command has a flag. -hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd) - --- Is this token a word that starts with a dash? -isFlag token = - case getWordParts token of - T_Literal _ ('-':_) : _ -> True - _ -> False - --- Is this token a flag where the - is unquoted? -isUnquotedFlag token = - case getLeadingUnquotedString token of - Just ('-':_) -> True - _ -> False - --- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read` --- -re -d : -u 3 bar --- into --- Just [("r", (-re, -re)), ("e", (-re, -re)), ("d", (-d,:)), ("u", (-u,3)), ("", (bar,bar))] --- --- Each string flag maps to a tuple of (flag, argument), where argument=flag if it --- doesn't take a specific one. --- --- Any unrecognized flag will result in Nothing. The exception is if arbitraryLongOpts --- is set, in which case --anything will map to "anything". -getGnuOpts :: String -> [Token] -> Maybe [(String, (Token, Token))] -getGnuOpts str args = getOpts (True, False) str [] args - --- As above, except the first non-arg string will treat the rest as arguments -getBsdOpts :: String -> [Token] -> Maybe [(String, (Token, Token))] -getBsdOpts str args = getOpts (False, False) str [] args - --- Tests for this are in Commands.hs where it's more frequently used -getOpts :: - -- Behavioral config: gnu style, allow arbitrary long options - (Bool, Bool) - -- A getopts style string - -> String - -- List of long options and whether they take arguments - -> [(String, Bool)] - -- List of arguments (excluding command) - -> [Token] - -- List of flags to tuple of (optionToken, valueToken) - -> Maybe [(String, (Token, Token))] - -getOpts (gnu, arbitraryLongOpts) string longopts args = process args - where - flagList (c:':':rest) = ([c], True) : flagList rest - flagList (c:rest) = ([c], False) : flagList rest - flagList [] = longopts - flagMap = Map.fromList $ ("", False) : flagList string - - process [] = return [] - process (token:rest) = do - case getLiteralStringDef "\0" token of - "--" -> return $ listToArgs rest - '-':'-':word -> do - let (name, arg) = span (/= '=') word - needsArg <- - if arbitraryLongOpts - then return $ Map.findWithDefault False name flagMap - else Map.lookup name flagMap - - if needsArg && null arg - then - case rest of - (arg:rest2) -> do - more <- process rest2 - return $ (name, (token, arg)) : more - _ -> fail "Missing arg" - else do - more <- process rest - -- Consider splitting up token to get arg - return $ (name, (token, token)) : more - '-':opts -> shortToOpts opts token rest - arg -> - if gnu - then do - more <- process rest - return $ ("", (token, token)):more - else return $ listToArgs (token:rest) - - shortToOpts opts token args = - case opts of - c:rest -> do - needsArg <- Map.lookup [c] flagMap - case () of - _ | needsArg && null rest -> do - (next:restArgs) <- return args - more <- process restArgs - return $ ([c], (token, next)):more - _ | needsArg -> do - more <- process args - return $ ([c], (token, token)):more - _ -> do - more <- shortToOpts rest token args - return $ ([c], (token, token)):more - [] -> process args - - listToArgs = map (\x -> ("", (x, x))) - - --- Generic getOpts that doesn't rely on a format string, but may also be inaccurate. --- This provides a best guess interpretation instead of failing when new options are added. --- --- "--" is treated as end of arguments --- "--anything[=foo]" is treated as a long option without argument --- "-any" is treated as -a -n -y, with the next arg as an option to -y unless it starts with - --- anything else is an argument -getGenericOpts :: [Token] -> [(String, (Token, Token))] -getGenericOpts = process - where - process (token:rest) = - case getLiteralStringDef "\0" token of - "--" -> map (\c -> ("", (c,c))) rest - '-':'-':word -> (takeWhile (`notElem` "\0=") word, (token, token)) : process rest - '-':optString -> - let opts = takeWhile (/= '\0') optString - in - case rest of - next:_ | "-" `isPrefixOf` getLiteralStringDef "\0" next -> - map (\c -> ([c], (token, token))) opts ++ process rest - next:remainder -> - case reverse opts of - last:initial -> - map (\c -> ([c], (token, token))) (reverse initial) - ++ [([last], (token, next))] - ++ process remainder - [] -> process remainder - [] -> map (\c -> ([c], (token, token))) opts - _ -> ("", (token, token)) : process rest - process [] = [] - - --- Is this an expansion of multiple items of an array? -isArrayExpansion (T_DollarBraced _ _ l) = - let string = concat $ oversimplify l in - "@" `isPrefixOf` string || - not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string -isArrayExpansion _ = False - --- Is it possible that this arg becomes multiple args? -mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t - where - f quoted (T_DollarBraced _ _ l) = - let string = concat $ oversimplify l in - not quoted || "!" `isPrefixOf` string - f quoted (T_DoubleQuoted _ parts) = any (f True) parts - f quoted (T_NormalWord _ parts) = any (f quoted) parts - f _ _ = False - --- Is it certain that this word will becomes multiple words? -willBecomeMultipleArgs t = willConcatInAssignment t || f t - where - f T_Extglob {} = True - f T_Glob {} = True - f T_BraceExpansion {} = True - f (T_NormalWord _ parts) = any f parts - f _ = False - --- This does token cause implicit concatenation in assignments? -willConcatInAssignment token = - case token of - t@T_DollarBraced {} -> isArrayExpansion t - (T_DoubleQuoted _ parts) -> any willConcatInAssignment parts - (T_NormalWord _ parts) -> any willConcatInAssignment parts - _ -> False - --- Maybe get the literal string corresponding to this token -getLiteralString :: Token -> Maybe String -getLiteralString = getLiteralStringExt (const Nothing) - --- Definitely get a literal string, with a given default for all non-literals -getLiteralStringDef :: String -> Token -> String -getLiteralStringDef x = runIdentity . getLiteralStringExt (const $ return x) - --- Definitely get a literal string, skipping over all non-literals -onlyLiteralString :: Token -> String -onlyLiteralString = getLiteralStringDef "" - --- Maybe get a literal string, but only if it's an unquoted argument. -getUnquotedLiteral (T_NormalWord _ list) = - concat <$> mapM str list - where - str (T_Literal _ s) = return s - str _ = Nothing -getUnquotedLiteral _ = Nothing - -isQuotes t = - case t of - T_DoubleQuoted {} -> True - T_SingleQuoted {} -> True - _ -> False - --- Get the last unquoted T_Literal in a word like "${var}foo"THIS --- or nothing if the word does not end in an unquoted literal. -getTrailingUnquotedLiteral :: Token -> Maybe Token -getTrailingUnquotedLiteral t = - case t of - (T_NormalWord _ list@(_:_)) -> - from (last list) - _ -> Nothing - where - from t = - case t of - T_Literal {} -> return t - _ -> Nothing - --- Get the leading, unquoted, literal string of a token (if any). -getLeadingUnquotedString :: Token -> Maybe String -getLeadingUnquotedString t = - case t of - T_NormalWord _ ((T_Literal _ s) : rest) -> return $ s ++ from rest - _ -> Nothing - where - from ((T_Literal _ s):rest) = s ++ from rest - from _ = "" - --- Maybe get the literal string of this token and any globs in it. -getGlobOrLiteralString = getLiteralStringExt f - where - f (T_Glob _ str) = return str - f _ = Nothing - - -prop_getLiteralString1 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x01") == Just "\1" -prop_getLiteralString2 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xyz") == Just "\\xyz" -prop_getLiteralString3 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1") == Just "\x1" -prop_getLiteralString4 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1y") == Just "\x1y" -prop_getLiteralString5 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xy") == Just "\\xy" -prop_getLiteralString6 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x") == Just "\\x" -prop_getLiteralString7 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1x") == Just "\1x" -prop_getLiteralString8 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12x") == Just "\o12x" -prop_getLiteralString9 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123x") == Just "\o123x" -prop_getLiteralString10 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1234") == Just "\o123\&4" -prop_getLiteralString11 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1") == Just "\1" -prop_getLiteralString12 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12") == Just "\o12" -prop_getLiteralString13 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123") == Just "\o123" - --- Maybe get the literal value of a token, using a custom function --- to map unrecognized Tokens into strings. -getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String -getLiteralStringExt more = g - where - allInList = fmap concat . mapM g - g (T_DoubleQuoted _ l) = allInList l - g (T_DollarDoubleQuoted _ l) = allInList l - g (T_NormalWord _ l) = allInList l - g (TA_Expansion _ l) = allInList l - g (T_SingleQuoted _ s) = return s - g (T_Literal _ s) = return s - g (T_ParamSubSpecialChar _ s) = return s - g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s - g x = more x - - -- Bash style $'..' decoding - decodeEscapes ('\\':c:cs) = - case c of - 'a' -> '\a' : rest - 'b' -> '\b' : rest - 'e' -> '\x1B' : rest - 'f' -> '\f' : rest - 'n' -> '\n' : rest - 'r' -> '\r' : rest - 't' -> '\t' : rest - 'v' -> '\v' : rest - '\'' -> '\'' : rest - '"' -> '"' : rest - '\\' -> '\\' : rest - 'x' -> - case cs of - (x:y:more) | isHexDigit x && isHexDigit y -> - chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more - (x:more) | isHexDigit x -> - chr (digitToInt x) : decodeEscapes more - more -> '\\' : 'x' : decodeEscapes more - _ | isOctDigit c -> - let (digits, more) = spanMax isOctDigit 3 (c:cs) - num = (parseOct digits) `mod` 256 - in (chr num) : decodeEscapes more - _ -> '\\' : c : rest - where - rest = decodeEscapes cs - parseOct = f 0 - where - f n "" = n - f n (c:rest) = f (n * 8 + digitToInt c) rest - spanMax f n list = - let (first, second) = span f list - (prefix, suffix) = splitAt n first - in - (prefix, suffix ++ second) - decodeEscapes (c:cs) = c : decodeEscapes cs - decodeEscapes [] = [] - --- Is this token a string literal? -isLiteral t = isJust $ getLiteralString t - --- Is this token a string literal number? -isLiteralNumber t = fromMaybe False $ do - s <- getLiteralString t - guard $ all isDigit s - return True - --- Escape user data for messages. --- Messages generally avoid repeating user data, but sometimes it's helpful. -e4m = escapeForMessage -escapeForMessage :: String -> String -escapeForMessage str = concatMap f str - where - f '\\' = "\\\\" - f '\n' = "\\n" - f '\r' = "\\r" - f '\t' = "\\t" - f '\x1B' = "\\e" - f c = - if shouldEscape c - then - if ord c < 256 - then "\\x" ++ (pad0 2 $ toHex c) - else "\\U" ++ (pad0 4 $ toHex c) - else [c] - - shouldEscape c = - (not $ isPrint c) - || (not (isAscii c) && not (isLetter c)) - - pad0 :: Int -> String -> String - pad0 n s = - let l = length s in - if l < n - then (replicate (n-l) '0') ++ s - else s - toHex :: Char -> String - toHex c = map toUpper $ showHex (ord c) "" - --- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz] -getWordParts (T_NormalWord _ l) = concatMap getWordParts l -getWordParts (T_DoubleQuoted _ l) = l --- TA_Expansion is basically T_NormalWord for arithmetic expressions -getWordParts (TA_Expansion _ l) = concatMap getWordParts l -getWordParts other = [other] - --- Return a list of NormalWords that would result from brace expansion -braceExpand (T_NormalWord id list) = take 1000 $ do - items <- mapM part list - return $ T_NormalWord id items - where - part (T_BraceExpansion id items) = do - item <- items - braceExpand item - part x = return x - --- Maybe get a SimpleCommand from immediate wrappers like T_Redirections -getCommand t = - case t of - T_Redirecting _ _ w -> getCommand w - T_SimpleCommand _ _ (w:_) -> return t - T_Annotation _ _ t -> getCommand t - _ -> Nothing - --- Maybe get the command name string of a token representing a command -getCommandName :: Token -> Maybe String -getCommandName = fst . getCommandNameAndToken False - --- Maybe get the name+arguments of a command. -getCommandArgv t = do - (T_SimpleCommand _ _ args@(_:_)) <- getCommand t - return args - --- Get the command name token from a command, i.e. --- the token representing 'ls' in 'ls -la 2> foo'. --- If it can't be determined, return the original token. -getCommandTokenOrThis = snd . getCommandNameAndToken False - --- Given a command, get the string and token that represents the command name. --- If direct, return the actual command (e.g. exec in 'exec ls') --- If not, return the logical command (e.g. 'ls' in 'exec ls') - -getCommandNameAndToken :: Bool -> Token -> (Maybe String, Token) -getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do - cmd@(T_SimpleCommand _ _ (w:rest)) <- getCommand t - s <- getLiteralString w - return $ fromMaybe (Just s, w) $ do - guard $ not direct - actual <- getEffectiveCommandToken s cmd rest - return (getLiteralString actual, actual) - where - getEffectiveCommandToken str cmd args = - let - firstArg = do - arg <- listToMaybe args - guard . not $ isFlag arg - return arg - in - case str of - "busybox" -> firstArg - "builtin" -> firstArg - "command" -> firstArg - "run" -> firstArg -- Used by bats - "exec" -> do - opts <- getBsdOpts "cla:" args - (_, (t, _)) <- find (null . fst) opts - return t - _ -> fail "" - --- If a command substitution is a single command, get its name. --- $(date +%s) = Just "date" -getCommandNameFromExpansion :: Token -> Maybe String -getCommandNameFromExpansion t = - case t of - T_DollarExpansion _ [c] -> extract c - T_Backticked _ [c] -> extract c - T_DollarBraceCommandExpansion _ _ [c] -> extract c - _ -> Nothing - where - extract (T_Pipeline _ _ [cmd]) = getCommandName cmd - extract _ = Nothing - --- Get the basename of a token representing a command -getCommandBasename = fmap basename . getCommandName - -basename = reverse . takeWhile (/= '/') . reverse - -isAssignment t = - case t of - T_Redirecting _ _ w -> isAssignment w - T_SimpleCommand _ (w:_) [] -> True - T_Assignment {} -> True - T_Annotation _ _ w -> isAssignment w - _ -> False - -isOnlyRedirection t = - case t of - T_Pipeline _ _ [x] -> isOnlyRedirection x - T_Annotation _ _ w -> isOnlyRedirection w - T_Redirecting _ (_:_) c -> isOnlyRedirection c - T_SimpleCommand _ [] [] -> True - _ -> False - -isFunction t = case t of T_Function {} -> True; _ -> False - --- Bats tests are functions for the purpose of 'local' and such -isFunctionLike t = - case t of - T_Function {} -> True - T_BatsTest {} -> True - _ -> False - - -isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False - --- Get the lists of commands from tokens that contain them, such as --- the conditions and bodies of while loops or branches of if statements. -getCommandSequences :: Token -> [[Token]] -getCommandSequences t = - case t of - T_Script _ _ cmds -> [cmds] - T_BraceGroup _ cmds -> [cmds] - T_Subshell _ cmds -> [cmds] - T_WhileExpression _ cond cmds -> [cond, cmds] - T_UntilExpression _ cond cmds -> [cond, cmds] - T_ForIn _ _ _ cmds -> [cmds] - T_ForArithmetic _ _ _ _ cmds -> [cmds] - T_IfExpression _ thens elses -> (concatMap (\(a,b) -> [a,b]) thens) ++ [elses] - T_Annotation _ _ t -> getCommandSequences t - - T_DollarExpansion _ cmds -> [cmds] - T_DollarBraceCommandExpansion _ _ cmds -> [cmds] - T_Backticked _ cmds -> [cmds] - _ -> [] - --- Get a list of names of associative arrays -getAssociativeArrays t = - nub . execWriter $ doAnalysis f t - where - f :: Token -> Writer [String] () - f t@T_SimpleCommand {} = sequence_ $ do - name <- getCommandName t - let assocNames = ["declare","local","typeset"] - guard $ name `elem` assocNames - let flags = getAllFlags t - guard $ "A" `elem` map snd flags - let args = [arg | (arg, "") <- flags] - let names = mapMaybe (getLiteralStringExt nameAssignments) args - return $ tell names - f _ = return () - - nameAssignments t = - case t of - T_Assignment _ _ name _ _ -> return name - _ -> Nothing - --- A Pseudoglob is a wildcard pattern used for checking if a match can succeed. --- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which --- can be proven never to match. -data PseudoGlob = PGAny | PGMany | PGChar Char - deriving (Eq, Show) - --- Turn a word into a PG pattern, replacing all unknown/runtime values with --- PGMany. -wordToPseudoGlob :: Token -> [PseudoGlob] -wordToPseudoGlob = fromMaybe [PGMany] . wordToPseudoGlob' False - --- Turn a word into a PG pattern, but only if we can preserve --- exact semantics. -wordToExactPseudoGlob :: Token -> Maybe [PseudoGlob] -wordToExactPseudoGlob = wordToPseudoGlob' True - -wordToPseudoGlob' :: Bool -> Token -> Maybe [PseudoGlob] -wordToPseudoGlob' exact word = - simplifyPseudoGlob <$> toGlob word - where - toGlob :: Token -> Maybe [PseudoGlob] - toGlob word = - case word of - T_NormalWord _ (T_Literal _ ('~':str):rest) -> do - guard $ not exact - let this = (PGMany : (map PGChar $ dropWhile (/= '/') str)) - tail <- concat <$> (mapM f $ concatMap getWordParts rest) - return $ this ++ tail - _ -> concat <$> (mapM f $ getWordParts word) - - f x = case x of - T_Literal _ s -> return $ map PGChar s - T_SingleQuoted _ s -> return $ map PGChar s - T_Glob _ "?" -> return [PGAny] - T_Glob _ "*" -> return [PGMany] - T_Glob _ ('[':_) | not exact -> return [PGAny] - _ -> if exact then fail "" else return [PGMany] - - --- Reorder a PseudoGlob for more efficient matching, e.g. --- f?*?**g -> f??*g -simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob] -simplifyPseudoGlob = f - where - f [] = [] - f (x@(PGChar _) : rest ) = x : f rest - f list = - let (anys, rest) = span (\x -> x == PGMany || x == PGAny) list in - order anys ++ f rest - - order s = let (any, many) = partition (== PGAny) s in - any ++ take 1 many - --- Check whether the two patterns can ever overlap. -pseudoGlobsCanOverlap :: [PseudoGlob] -> [PseudoGlob] -> Bool -pseudoGlobsCanOverlap = matchable - where - matchable x@(xf:xs) y@(yf:ys) = - case (xf, yf) of - (PGMany, _) -> matchable x ys || matchable xs y - (_, PGMany) -> matchable x ys || matchable xs y - (PGAny, _) -> matchable xs ys - (_, PGAny) -> matchable xs ys - (_, _) -> xf == yf && matchable xs ys - - matchable [] [] = True - matchable (PGMany : rest) [] = matchable rest [] - matchable (_:_) [] = False - matchable [] r = matchable r [] - --- Check whether the first pattern always overlaps the second. -pseudoGlobIsSuperSetof :: [PseudoGlob] -> [PseudoGlob] -> Bool -pseudoGlobIsSuperSetof = matchable - where - matchable x@(xf:xs) y@(yf:ys) = - case (xf, yf) of - (PGMany, PGMany) -> matchable x ys - (PGMany, _) -> matchable x ys || matchable xs y - (_, PGMany) -> False - (PGAny, _) -> matchable xs ys - (_, PGAny) -> False - (_, _) -> xf == yf && matchable xs ys - - matchable [] [] = True - matchable (PGMany : rest) [] = matchable rest [] - matchable _ _ = False - -wordsCanBeEqual x y = pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y) - --- Is this an expansion that can be quoted, --- e.g. $(foo) `foo` $foo (but not {foo,})? -isQuoteableExpansion t = case t of - T_DollarBraced {} -> True - _ -> isCommandSubstitution t - -isCommandSubstitution t = case t of - T_DollarExpansion {} -> True - T_DollarBraceCommandExpansion {} -> True - T_Backticked {} -> True - _ -> False - --- Is this an expansion that results in a simple string? -isStringExpansion t = isCommandSubstitution t || case t of - T_DollarArithmetic {} -> True - T_DollarBraced {} -> not (isArrayExpansion t) - _ -> False - --- Is this a T_Annotation that ignores a specific code? -isAnnotationIgnoringCode code t = - case t of - T_Annotation _ anns _ -> any hasNum anns - _ -> False - where - hasNum (DisableComment from to) = code >= from && code < to - hasNum _ = False - -prop_executableFromShebang1 = executableFromShebang "/bin/sh" == "sh" -prop_executableFromShebang2 = executableFromShebang "/bin/bash" == "bash" -prop_executableFromShebang3 = executableFromShebang "/usr/bin/env ksh" == "ksh" -prop_executableFromShebang4 = executableFromShebang "/usr/bin/env -S foo=bar bash -x" == "bash" -prop_executableFromShebang5 = executableFromShebang "/usr/bin/env --split-string=bash -x" == "bash" -prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string=foo=bar bash -x" == "bash" -prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash" -prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash" -prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash" -prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh" -prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash" - --- Get the shell executable from a string like '/usr/bin/env bash' -executableFromShebang :: String -> String -executableFromShebang = shellFor - where - re = mkRegex "/env +(-S|--split-string=?)? *(.*)" - shellFor s | s `matches` re = - case matchRegex re s of - Just [flag, shell] -> fromEnvArgs (words shell) - _ -> "" - shellFor sb = - case words sb of - [] -> "" - [x] -> basename x - (first:second:args) | basename first == "busybox" -> - case basename second of - "sh" -> "busybox sh" - "ash" -> "busybox ash" - x -> x - (first:args) | basename first == "env" -> - fromEnvArgs args - (first:_) -> basename first - - fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args - basename s = reverse . takeWhile (/= '/') . reverse $ s - skipFlags = dropWhile ("-" `isPrefixOf`) - - --- Determining if a name is a variable -isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x -isVariableChar x = isVariableStartChar x || isDigit x -isSpecialVariableChar = (`elem` "*@#?-$!") -variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*" - -prop_isVariableName1 = isVariableName "_fo123" -prop_isVariableName2 = not $ isVariableName "4" -prop_isVariableName3 = not $ isVariableName "test: " -isVariableName (x:r) = isVariableStartChar x && all isVariableChar r -isVariableName _ = False - - --- Get the variable name from an expansion like ${var:-foo} -prop_getBracedReference1 = getBracedReference "foo" == "foo" -prop_getBracedReference2 = getBracedReference "#foo" == "foo" -prop_getBracedReference3 = getBracedReference "#" == "#" -prop_getBracedReference4 = getBracedReference "##" == "#" -prop_getBracedReference5 = getBracedReference "#!" == "!" -prop_getBracedReference6 = getBracedReference "!#" == "#" -prop_getBracedReference7 = getBracedReference "!foo#?" == "foo" -prop_getBracedReference8 = getBracedReference "foo-bar" == "foo" -prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo" -prop_getBracedReference10 = getBracedReference "foo: -1" == "foo" -prop_getBracedReference11 = getBracedReference "!os*" == "" -prop_getBracedReference11b = getBracedReference "!os@" == "" -prop_getBracedReference12 = getBracedReference "!os?bar**" == "" -prop_getBracedReference13 = getBracedReference "foo[bar]" == "foo" -getBracedReference s = fromMaybe s $ - nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s - where - noPrefix = dropPrefix s - dropPrefix (c:rest) | c `elem` "!#" = rest - dropPrefix cs = cs - takeName s = do - let name = takeWhile isVariableChar s - guard . not $ null name - return name - getSpecial (c:_) | isSpecialVariableChar c = return [c] - getSpecial _ = fail "empty or not special" - - nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*} - guard $ isVariableChar next -- e.g. ${!@} - first <- find (not . isVariableChar) rest - guard $ first `elem` "*?@" - return "" - nameExpansion _ = Nothing - --- Get the variable modifier like /a/b in ${var/a/b} -prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz" -prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo" -prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]" -prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q" -prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q" -getBracedModifier s = headOrDefault "" $ do - let var = getBracedReference s - a <- dropModifier s - dropPrefix var a - where - dropPrefix [] t = return t - dropPrefix (a:b) (c:d) | a == c = dropPrefix b d - dropPrefix _ _ = [] - - dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest] - dropModifier x = [x] - --- Get the variables from indices like ["x", "y"] in ${var[x+y+1]} -prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"] -getIndexReferences s = fromMaybe [] $ do - index:_ <- matchRegex re s - return $ matchAllStrings variableNameRegex index - where - re = mkRegex "(\\[.*\\])" - -prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"] -prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"] -prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"] -prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"] -getOffsetReferences mods = fromMaybe [] $ do --- if mods start with [, then drop until ] - _:offsets:_ <- matchRegex re mods - return $ matchAllStrings variableNameRegex offsets - where - re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)" - - --- Returns whether a token is a parameter expansion without any modifiers. --- True for $var ${var} $1 $# --- False for ${#var} ${var[x]} ${var:-0} -isUnmodifiedParameterExpansion t = - case t of - T_DollarBraced _ False _ -> True - T_DollarBraced _ _ list -> - let str = concat $ oversimplify list - in getBracedReference str == str - _ -> False - --- Return the referenced variable if (and only if) it's an unmodified parameter expansion. -getUnmodifiedParameterExpansion t = - case t of - T_DollarBraced _ _ list -> do - let str = concat $ oversimplify list - guard $ getBracedReference str == str - return str - _ -> Nothing - ---- A list of the element and all its parents up to the root node. -getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree) - -isClosingFileOp op = - case op of - T_IoDuplicate _ (T_GREATAND _) "-" -> True - T_IoDuplicate _ (T_LESSAND _) "-" -> True - _ -> False - -getEnableDirectives root = - case root of - T_Annotation _ list _ -> [s | EnableComment s <- list] - _ -> [] - -getExtendedAnalysisDirective :: Token -> Maybe Bool -getExtendedAnalysisDirective root = - case root of - T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list] - _ -> Nothing - -return [] -runTests = $quickCheckAll diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs deleted file mode 100644 index 373d495..0000000 --- a/src/ShellCheck/Analytics.hs +++ /dev/null @@ -1,5242 +0,0 @@ -{- - Copyright 2012-2024 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE PatternGuards #-} -module ShellCheck.Analytics (checker, optionalChecks, ShellCheck.Analytics.runTests) where - -import ShellCheck.AST -import ShellCheck.ASTLib -import ShellCheck.AnalyzerLib hiding (producesComments) -import ShellCheck.CFG -import qualified ShellCheck.CFGAnalysis as CF -import ShellCheck.Data -import ShellCheck.Parser -import ShellCheck.Prelude -import ShellCheck.Interface -import ShellCheck.Regex - -import Control.Arrow (first) -import Control.Monad -import Control.Monad.Identity -import Control.Monad.State -import Control.Monad.Writer hiding ((<>)) -import Control.Monad.Reader -import Data.Char -import Data.Functor -import Data.Function (on) -import Data.List -import Data.Maybe -import Data.Ord -import Data.Semigroup -import Debug.Trace -- STRIP -import qualified Data.List.NonEmpty as NE -import qualified Data.Map.Strict as Map -import qualified Data.Set as S -import Test.QuickCheck.All (forAllProperties) -import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) - --- Checks that are run on the AST root -treeChecks :: [Parameters -> Token -> [TokenComment]] -treeChecks = [ - nodeChecksToTreeCheck nodeChecks - ,subshellAssignmentCheck - ,checkQuotesInLiterals - ,checkShebangParameters - ,checkFunctionsUsedExternally - ,checkUnusedAssignments - ,checkUnpassedInFunctions - ,checkArrayWithoutIndex - ,checkShebang - ,checkUnassignedReferences - ,checkUncheckedCdPushdPopd - ,checkArrayAssignmentIndices - ,checkUseBeforeDefinition - ,checkAliasUsedInSameParsingUnit - ,checkArrayValueUsedAsIndex - ] - -checker spec params = mkChecker spec params treeChecks - -mkChecker spec params checks = - Checker { - perScript = \(Root root) -> do - tell $ concatMap (\f -> f params root) all, - perToken = const $ return () - } - where - all = checks ++ optionals - optionalKeys = asOptionalChecks spec - optionals = - if "all" `elem` optionalKeys - then map snd optionalTreeChecks - else mapMaybe (\c -> Map.lookup c optionalCheckMap) optionalKeys - - -checkList l t = concatMap (\f -> f t) l - --- Checks that are run on each node in the AST -runNodeAnalysis f p t = execWriter (doAnalysis (f p) t) - --- Perform multiple node checks in a single iteration over the tree -nodeChecksToTreeCheck checkList = - runNodeAnalysis - (\p t -> (mapM_ ((\ f -> f t) . (\ f -> f p)) - checkList)) - -nodeChecks :: [Parameters -> Token -> Writer [TokenComment] ()] -nodeChecks = [ - checkPipePitfalls - ,checkForInQuoted - ,checkForInLs - ,checkShorthandIf - ,checkDollarStar - ,checkUnquotedDollarAt - ,checkStderrRedirect - ,checkUnquotedN - ,checkNumberComparisons - ,checkSingleBracketOperators - ,checkDoubleBracketOperators - ,checkLiteralBreakingTest - ,checkConstantNullary - ,checkDivBeforeMult - ,checkArithmeticDeref - ,checkArithmeticBadOctal - ,checkComparisonAgainstGlob - ,checkCaseAgainstGlob - ,checkCommarrays - ,checkOrNeq - ,checkAndEq - ,checkEchoWc - ,checkConstantIfs - ,checkPipedAssignment - ,checkAssignAteCommand - ,checkUuoeVar - ,checkQuotedCondRegex - ,checkForInCat - ,checkFindExec - ,checkValidCondOps - ,checkGlobbedRegex - ,checkTestRedirects - ,checkBadParameterSubstitution - ,checkPS1Assignments - ,checkBackticks - ,checkInexplicablyUnquoted - ,checkTildeInQuotes - ,checkLonelyDotDash - ,checkSpuriousExec - ,checkSpuriousExpansion - ,checkDollarBrackets - ,checkSshHereDoc - ,checkGlobsAsOptions - ,checkWhileReadPitfalls - ,checkArithmeticOpCommand - ,checkCharRangeGlob - ,checkUnquotedExpansions - ,checkSingleQuotedVariables - ,checkRedirectToSame - ,checkPrefixAssignmentReference - ,checkLoopKeywordScope - ,checkCdAndBack - ,checkWrongArithmeticAssignment - ,checkConditionalAndOrs - ,checkFunctionDeclarations - ,checkStderrPipe - ,checkOverridingPath - ,checkArrayAsString - ,checkUnsupported - ,checkMultipleAppends - ,checkSuspiciousIFS - ,checkShouldUseGrepQ - ,checkTestArgumentSplitting - ,checkConcatenatedDollarAt - ,checkTildeInPath - ,checkReadWithoutR - ,checkLoopVariableReassignment - ,checkTrailingBracket - ,checkReturnAgainstZero - ,checkRedirectedNowhere - ,checkUnmatchableCases - ,checkSubshellAsTest - ,checkSplittingInArrays - ,checkRedirectionToNumber - ,checkGlobAsCommand - ,checkFlagAsCommand - ,checkEmptyCondition - ,checkPipeToNowhere - ,checkForLoopGlobVariables - ,checkSubshelledTests - ,checkRedirectionToCommand - ,checkDollarQuoteParen - ,checkUselessBang - ,checkTranslatedStringVariable - ,checkModifiedArithmeticInRedirection - ,checkBlatantRecursion - ,checkBadTestAndOr - ,checkAssignToSelf - ,checkEqualsInCommand - ,checkSecondArgIsComparison - ,checkComparisonWithLeadingX - ,checkCommandWithTrailingSymbol - ,checkUnquotedParameterExpansionPattern - ,checkBatsTestDoesNotUseNegation - ,checkCommandIsUnreachable - ,checkSpacefulnessCfg - ,checkOverwrittenExitCode - ,checkUnnecessaryArithmeticExpansionIndex - ,checkUnnecessaryParens - ,checkPlusEqualsNumber - ,checkExpansionWithRedirection - ,checkUnaryTestA - ] - -optionalChecks = map fst optionalTreeChecks - - -prop_verifyOptionalExamples = all check optionalTreeChecks - where - check (desc, check) = - verifyTree check (cdPositive desc) - && verifyNotTree check (cdNegative desc) - -optionalTreeChecks :: [(CheckDescription, (Parameters -> Token -> [TokenComment]))] -optionalTreeChecks = [ - (newCheckDescription { - cdName = "quote-safe-variables", - cdDescription = "Suggest quoting variables without metacharacters", - cdPositive = "var=hello; echo $var", - cdNegative = "var=hello; echo \"$var\"" - }, nodeChecksToTreeCheck [checkVerboseSpacefulnessCfg]) - - ,(newCheckDescription { - cdName = "avoid-nullary-conditions", - cdDescription = "Suggest explicitly using -n in `[ $var ]`", - cdPositive = "[ \"$var\" ]", - cdNegative = "[ -n \"$var\" ]" - }, nodeChecksToTreeCheck [checkNullaryExpansionTest]) - - ,(newCheckDescription { - cdName = "avoid-negated-conditions", - cdDescription = "Suggest removing unnecessary comparison negations", - cdPositive = "[ ! \"$var\" -eq 1 ]", - cdNegative = "[ \"$var\" -ne 1 ]" - }, nodeChecksToTreeCheck [checkUnnecessarilyInvertedTest]) - - ,(newCheckDescription { - cdName = "add-default-case", - cdDescription = "Suggest adding a default case in `case` statements", - cdPositive = "case $? in 0) echo 'Success';; esac", - cdNegative = "case $? in 0) echo 'Success';; *) echo 'Fail' ;; esac" - }, nodeChecksToTreeCheck [checkDefaultCase]) - - ,(newCheckDescription { - cdName = "require-variable-braces", - cdDescription = "Suggest putting braces around all variable references", - cdPositive = "var=hello; echo $var", - cdNegative = "var=hello; echo ${var}" - }, nodeChecksToTreeCheck [checkVariableBraces]) - - ,(newCheckDescription { - cdName = "check-unassigned-uppercase", - cdDescription = "Warn when uppercase variables are unassigned", - cdPositive = "echo $VAR", - cdNegative = "VAR=hello; echo $VAR" - }, checkUnassignedReferences' True) - - ,(newCheckDescription { - cdName = "require-double-brackets", - cdDescription = "Require [[ and warn about [ in Bash/Ksh", - cdPositive = "[ -e /etc/issue ]", - cdNegative = "[[ -e /etc/issue ]]" - }, checkRequireDoubleBracket) - - ,(newCheckDescription { - cdName = "check-set-e-suppressed", - cdDescription = "Notify when set -e is suppressed during function invocation", - cdPositive = "set -e; func() { cp *.txt ~/backup; rm *.txt; }; func && echo ok", - cdNegative = "set -e; func() { cp *.txt ~/backup; rm *.txt; }; func; echo ok" - }, checkSetESuppressed) - - ,(newCheckDescription { - cdName = "check-extra-masked-returns", - cdDescription = "Check for additional cases where exit codes are masked", - cdPositive = "rm -r \"$(get_chroot_dir)/home\"", - cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\"" - }, checkExtraMaskedReturns) - - ,(newCheckDescription { - cdName = "useless-use-of-cat", - cdDescription = "Check for Useless Use Of Cat (UUOC)", - cdPositive = "cat foo | grep bar", - cdNegative = "grep bar foo" - }, nodeChecksToTreeCheck [checkUuoc]) - ] - -optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment]) -optionalCheckMap = Map.fromList $ map item optionalTreeChecks - where - item (desc, check) = (cdName desc, check) - -wouldHaveBeenGlob s = '*' `elem` s - -verify :: (Parameters -> Token -> Writer [TokenComment] ()) -> String -> Bool -verify f s = checkNode f s == Just True - -verifyNot :: (Parameters -> Token -> Writer [TokenComment] ()) -> String -> Bool -verifyNot f s = checkNode f s == Just False - -verifyTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool -verifyTree f s = producesComments f s == Just True - -verifyNotTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool -verifyNotTree f s = producesComments f s == Just False - -checkCommand str f t@(T_SimpleCommand id _ (cmd:rest)) - | t `isCommand` str = f cmd rest -checkCommand _ _ _ = return () - -checkUnqualifiedCommand str f t@(T_SimpleCommand id _ (cmd:rest)) - | t `isUnqualifiedCommand` str = f cmd rest -checkUnqualifiedCommand _ _ _ = return () - -verifyCodes :: (Parameters -> Token -> Writer [TokenComment] ()) -> [Code] -> String -> Bool -verifyCodes f l s = codes == Just l - where - treeCheck = runNodeAnalysis f - comments = runAndGetComments treeCheck s - codes = map (cCode . tcComment) <$> comments - -checkNode f = producesComments (runNodeAnalysis f) -producesComments :: (Parameters -> Token -> [TokenComment]) -> String -> Maybe Bool -producesComments f s = not . null <$> runAndGetComments f s - -runAndGetComments f s = do - let pr = pScript s - root <- prRoot pr - let spec = defaultSpec pr - let params = makeParameters spec - return $ - filterByAnnotation spec params $ - f params root - --- Copied from https://wiki.haskell.org/Edit_distance -dist :: Eq a => [a] -> [a] -> Int -dist a b - = last (if lab == 0 then mainDiag - else if lab > 0 then lowers !! (lab - 1) - else{- < 0 -} uppers !! (-1 - lab)) - where mainDiag = oneDiag a b (head uppers) (-1 : head lowers) - uppers = eachDiag a b (mainDiag : uppers) -- upper diagonals - lowers = eachDiag b a (mainDiag : lowers) -- lower diagonals - eachDiag a [] diags = [] - eachDiag a (bch:bs) (lastDiag:diags) = oneDiag a bs nextDiag lastDiag : eachDiag a bs diags - where nextDiag = head (tail diags) - oneDiag a b diagAbove diagBelow = thisdiag - where doDiag [] b nw n w = [] - doDiag a [] nw n w = [] - doDiag (ach:as) (bch:bs) nw n w = me : doDiag as bs me (tail n) (tail w) - where me = if ach == bch then nw else 1 + min3 (head w) nw (head n) - firstelt = 1 + head diagBelow - thisdiag = firstelt : doDiag a b firstelt diagAbove (tail diagBelow) - lab = length a - length b - min3 x y z = if x < y then x else min y z - -hasFloatingPoint params = shellType params == Ksh - --- Checks whether the current parent path is part of a condition -isCondition (x NE.:| xs) = foldr go (const False) xs x - where - go _ _ T_BatsTest{} = True -- count anything in a @test as conditional - go parent go_rest child = - getId child `elem` map getId (getConditionChildren parent) || go_rest parent - getConditionChildren t = - case t of - T_AndIf _ left right -> [left] - T_OrIf id left right -> [left] - T_IfExpression id conditions elses -> concatMap (take 1 . reverse . fst) conditions - T_WhileExpression id c l -> take 1 . reverse $ c - T_UntilExpression id c l -> take 1 . reverse $ c - _ -> [] - --- helpers to build replacements -replaceStart id params n r = - let tp = tokenPositions params - (start, _) = tp Map.! id - new_end = start { - posColumn = posColumn start + n - } - depth = length $ getPath (parentMap params) (T_EOF id) - in - newReplacement { - repStartPos = start, - repEndPos = new_end, - repString = r, - repPrecedence = depth, - repInsertionPoint = InsertAfter - } -replaceEnd id params n r = - let tp = tokenPositions params - (_, end) = tp Map.! id - new_start = end { - posColumn = posColumn end - n - } - new_end = end { - posColumn = posColumn end - } - depth = length $ getPath (parentMap params) (T_EOF id) - in - newReplacement { - repStartPos = new_start, - repEndPos = new_end, - repString = r, - repPrecedence = depth, - repInsertionPoint = InsertBefore - } -replaceToken id params r = - let tp = tokenPositions params - (start, end) = tp Map.! id - depth = length $ getPath (parentMap params) (T_EOF id) - in - newReplacement { - repStartPos = start, - repEndPos = end, - repString = r, - repPrecedence = depth, - repInsertionPoint = InsertBefore - } - -surroundWith id params s = fixWith [replaceStart id params 0 s, replaceEnd id params 0 s] -fixWith fixes = newFix { fixReplacements = fixes } - -analyse f t = execState (doAnalysis f t) [] - --- Make a map from functions to definition IDs -functions t = Map.fromList $ analyse findFunctions t -findFunctions (T_Function id _ _ name _) - = modify ((name, id):) -findFunctions _ = return () - --- Make a map from aliases to definition IDs -aliases t = Map.fromList $ analyse findAliases t -findAliases t@(T_SimpleCommand _ _ (_:args)) - | t `isUnqualifiedCommand` "alias" = mapM_ getAlias args -findAliases _ = return () -getAlias arg = - let string = onlyLiteralString arg - in when ('=' `elem` string) $ - modify ((takeWhile (/= '=') string, getId arg):) - -prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)" -checkEchoWc _ (T_Pipeline id _ [a, b]) = - when (acmd == ["echo", "${VAR}"]) $ - case bcmd of - ["wc", "-c"] -> countMsg - ["wc", "-m"] -> countMsg - _ -> return () - where - acmd = oversimplify a - bcmd = oversimplify b - countMsg = style id 2000 "See if you can use ${#variable} instead." -checkEchoWc _ _ = return () - -prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo" -prop_checkPipedAssignment2 = verifyNot checkPipedAssignment "A=foo cmd | grep foo" -prop_checkPipedAssignment3 = verifyNot checkPipedAssignment "A=foo" -checkPipedAssignment _ (T_Pipeline _ _ (T_Redirecting _ _ (T_SimpleCommand id (_:_) []):_:_)) = - warn id 2036 "If you wanted to assign the output of the pipeline, use a=$(b | c) ." -checkPipedAssignment _ _ = return () - -prop_checkAssignAteCommand1 = verify checkAssignAteCommand "A=ls -l" -prop_checkAssignAteCommand2 = verify checkAssignAteCommand "A=ls --sort=$foo" -prop_checkAssignAteCommand3 = verify checkAssignAteCommand "A=cat foo | grep bar" -prop_checkAssignAteCommand4 = verifyNot checkAssignAteCommand "A=foo ls -l" -prop_checkAssignAteCommand5 = verify checkAssignAteCommand "PAGER=cat grep bar" -prop_checkAssignAteCommand6 = verifyNot checkAssignAteCommand "PAGER=\"cat\" grep bar" -prop_checkAssignAteCommand7 = verify checkAssignAteCommand "here=pwd" -checkAssignAteCommand _ (T_SimpleCommand id [T_Assignment _ _ _ _ assignmentTerm] list) = - -- Check if first word is intended as an argument (flag or glob). - if firstWordIsArg list - then - err id 2037 "To assign the output of a command, use var=$(cmd) ." - else - -- Check if it's a known, unquoted command name. - when (isCommonCommand $ getUnquotedLiteral assignmentTerm) $ - warn id 2209 "Use var=$(command) to assign output (or quote to assign string)." - where - isCommonCommand (Just s) = s `elem` commonCommands - isCommonCommand _ = False - firstWordIsArg (head:_) = isGlob head || isUnquotedFlag head - firstWordIsArg [] = False - -checkAssignAteCommand _ _ = return () - -prop_checkArithmeticOpCommand1 = verify checkArithmeticOpCommand "i=i + 1" -prop_checkArithmeticOpCommand2 = verify checkArithmeticOpCommand "foo=bar * 2" -prop_checkArithmeticOpCommand3 = verifyNot checkArithmeticOpCommand "foo + opts" -checkArithmeticOpCommand _ (T_SimpleCommand id [T_Assignment {}] (firstWord:_)) = - mapM_ check $ getGlobOrLiteralString firstWord - where - check op = - when (op `elem` ["+", "-", "*", "/"]) $ - warn (getId firstWord) 2099 $ - "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))" -checkArithmeticOpCommand _ _ = return () - -prop_checkWrongArit = verify checkWrongArithmeticAssignment "i=i+1" -prop_checkWrongArit2 = verify checkWrongArithmeticAssignment "n=2; i=n*2" -checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _ val] []) = - sequence_ $ do - str <- getNormalString val - var:op:_ <- matchRegex regex str - guard $ S.member var references - return . warn (getId val) 2100 $ - "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))" - where - regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$" - references = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params] - - getNormalString (T_NormalWord _ words) = do - parts <- mapM getLiterals words - return $ concat parts - getNormalString _ = Nothing - - getLiterals (T_Literal _ s) = return s - getLiterals (T_Glob _ s) = return s - getLiterals _ = Nothing -checkWrongArithmeticAssignment _ _ = return () - - -prop_checkUuoc1 = verify checkUuoc "cat foo | grep bar" -prop_checkUuoc2 = verifyNot checkUuoc "cat * | grep bar" -prop_checkUuoc3 = verify checkUuoc "cat \"$var\" | grep bar" -prop_checkUuoc3b = verifyNot checkUuoc "cat $var | grep bar" -prop_checkUuoc3c = verifyNot checkUuoc "cat \"${!var}\" | grep bar" -prop_checkUuoc4 = verifyNot checkUuoc "cat $var" -prop_checkUuoc5 = verifyNot checkUuoc "cat \"$@\"" -prop_checkUuoc6 = verifyNot checkUuoc "cat -n | grep bar" -checkUuoc _ (T_Pipeline _ _ (T_Redirecting _ _ cmd:_:_)) = - checkCommand "cat" (const f) cmd - where - f [word] | not (mayBecomeMultipleArgs word || isOption word) = - style (getId word) 2002 "Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead." - f _ = return () - isOption word = "-" `isPrefixOf` onlyLiteralString word -checkUuoc _ _ = return () - -prop_checkPipePitfalls3 = verify checkPipePitfalls "ls | grep -v mp3" -prop_checkPipePitfalls4 = verifyNot checkPipePitfalls "find . -print0 | xargs -0 foo" -prop_checkPipePitfalls5 = verifyNot checkPipePitfalls "ls -N | foo" -prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo" -prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | xargs foo" -prop_checkPipePitfalls8 = verify checkPipePitfalls "foo | grep bar | wc -l" -prop_checkPipePitfalls9 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -l" -prop_checkPipePitfalls10 = verifyNot checkPipePitfalls "foo | grep -o bar | wc" -prop_checkPipePitfalls11 = verifyNot checkPipePitfalls "foo | grep bar | wc" -prop_checkPipePitfalls12 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -c" -prop_checkPipePitfalls13 = verifyNot checkPipePitfalls "foo | grep bar | wc -c" -prop_checkPipePitfalls14 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -cmwL" -prop_checkPipePitfalls15 = verifyNot checkPipePitfalls "foo | grep bar | wc -cmwL" -prop_checkPipePitfalls16 = verifyNot checkPipePitfalls "foo | grep -r bar | wc -l" -prop_checkPipePitfalls17 = verifyNot checkPipePitfalls "foo | grep -l bar | wc -l" -prop_checkPipePitfalls18 = verifyNot checkPipePitfalls "foo | grep -L bar | wc -l" -prop_checkPipePitfalls19 = verifyNot checkPipePitfalls "foo | grep -A2 bar | wc -l" -prop_checkPipePitfalls20 = verifyNot checkPipePitfalls "foo | grep -B999 bar | wc -l" -prop_checkPipePitfalls21 = verifyNot checkPipePitfalls "foo | grep --after-context 999 bar | wc -l" -prop_checkPipePitfalls22 = verifyNot checkPipePitfalls "foo | grep -B 1 --after-context 999 bar | wc -l" -prop_checkPipePitfalls23 = verifyNot checkPipePitfalls "ps -o pid,args -p $(pgrep java) | grep -F net.shellcheck.Test" -checkPipePitfalls _ (T_Pipeline id _ commands) = do - for ["find", "xargs"] $ - \(find:xargs:_) -> - let args = oversimplify xargs ++ oversimplify find - in - unless (any ($ args) [ - hasShortParameter '0', - hasParameter "null", - hasParameter "print0", - hasParameter "printf" - ]) $ warn (getId find) 2038 - "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames." - - for ["ps", "grep"] $ - \(ps:grep:_) -> - let - psFlags = maybe [] (map snd . getAllFlags) $ getCommand ps - in - -- There are many ways to specify a pid: 1, -1, p 1, wup 1, -q 1, -p 1, --pid 1. - -- For simplicity we only deal with the most canonical looking flags: - unless (any (`elem` ["p", "pid", "q", "quick-pid"]) psFlags) $ - info (getId ps) 2009 "Consider using pgrep instead of grepping ps output." - - for ["grep", "wc"] $ - \(grep:wc:_) -> - let flagsGrep = maybe [] (map snd . getAllFlags) $ getCommand grep - flagsWc = maybe [] (map snd . getAllFlags) $ getCommand wc - in - unless (any (`elem` ["l", "files-with-matches", "L", "files-without-matches", "o", "only-matching", "r", "R", "recursive", "A", "after-context", "B", "before-context"]) flagsGrep - || any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc - || null flagsWc) $ - style (getId grep) 2126 "Consider using 'grep -c' instead of 'grep|wc -l'." - - didLs <- fmap or . sequence $ [ - for' ["ls", "grep"] $ - \x -> warn x 2010 "Don't use ls | grep. Use a glob or a for loop with a condition to allow non-alphanumeric filenames.", - for' ["ls", "xargs"] $ - \x -> warn x 2011 "Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames." - ] - unless didLs $ void $ - for ["ls", "?"] $ - \(ls:_) -> unless (hasShortParameter 'N' (oversimplify ls)) $ - info (getId ls) 2012 "Use find instead of ls to better handle non-alphanumeric filenames." - where - for l f = - let indices = indexOfSublists l (map (headOrDefault "" . oversimplify) commands) - in do - mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices - return . not . null $ indices - for' l f = for l (first f) - first func (x:_) = func (getId $ getCommandTokenOrThis x) - first _ _ = return () - hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x) - hasParameter string = - any (isPrefixOf string . dropWhile (== '-')) -checkPipePitfalls _ _ = return () - -indexOfSublists sub = f 0 - where - f _ [] = [] - f n a@(r:rest) = - let others = f (n+1) rest in - if match sub a - then n:others - else others - match ("?":r1) (_:r2) = match r1 r2 - match (x1:r1) (x2:r2) | x1 == x2 = match r1 r2 - match [] _ = True - match _ _ = False - - -prop_checkShebangParameters1 = verifyTree checkShebangParameters "#!/usr/bin/env bash -x\necho cow" -prop_checkShebangParameters2 = verifyNotTree checkShebangParameters "#! /bin/sh -l " -prop_checkShebangParameters3 = verifyNotTree checkShebangParameters "#!/usr/bin/env -S bash -x\necho cow" -prop_checkShebangParameters4 = verifyNotTree checkShebangParameters "#!/usr/bin/env --split-string bash -x\necho cow" -checkShebangParameters p (T_Annotation _ _ t) = checkShebangParameters p t -checkShebangParameters _ (T_Script _ (T_Literal id sb) _) = - [makeComment ErrorC id 2096 "On most OS, shebangs can only specify a single parameter." | isMultiWord] - where - isMultiWord = length (words sb) > 2 && not (sb `matches` re) - re = mkRegex "env +(-S|--split-string)" - -prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho cow" -prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l " -prop_checkShebang3 = verifyTree checkShebang "ls -l" -prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo" -prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash" -prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n" -prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n" -prop_checkShebang8 = verifyTree checkShebang "#!bin/sh\ntrue" -prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue" -prop_checkShebang10 = verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue" -prop_checkShebang11 = verifyTree checkShebang "#!/bin/sh/\ntrue" -prop_checkShebang12 = verifyTree checkShebang "#!/bin/sh/ -xe\ntrue" -prop_checkShebang13 = verifyNotTree checkShebang "#!/bin/busybox sh" -prop_checkShebang14 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=sh\n" -prop_checkShebang15 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=dash\n" -prop_checkShebang16 = verifyNotTree checkShebang "#!/bin/busybox ash" -prop_checkShebang17 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=dash\n" -prop_checkShebang18 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=sh\n" -checkShebang params (T_Annotation _ list t) = - if any isOverride list then [] else checkShebang params t - where - isOverride (ShellOverride _) = True - isOverride _ = False -checkShebang params (T_Script _ (T_Literal id sb) _) = execWriter $ do - unless (shellTypeSpecified params) $ do - when (null sb) $ - err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive." - when (executableFromShebang sb == "ash") $ - warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence." - unless (null sb) $ do - unless ("/" `isPrefixOf` sb) $ - err id 2239 "Ensure the shebang uses an absolute path to the interpreter." - when ("/" `isSuffixOf` head (words sb)) $ - err id 2246 "This shebang specifies a directory. Ensure the interpreter is a file." - - -prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done" -prop_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done" -prop_checkForInQuoted2a = verifyNot checkForInQuoted "for f in *.mp3; do echo foo; done" -prop_checkForInQuoted2b = verify checkForInQuoted "for f in \"*.mp3\"; do echo foo; done" -prop_checkForInQuoted3 = verify checkForInQuoted "for f in 'find /'; do true; done" -prop_checkForInQuoted4 = verify checkForInQuoted "for f in 1,2,3; do true; done" -prop_checkForInQuoted4a = verifyNot checkForInQuoted "for f in foo{1,2,3}; do true; done" -prop_checkForInQuoted5 = verify checkForInQuoted "for f in ls; do true; done" -prop_checkForInQuoted6 = verifyNot checkForInQuoted "for f in \"${!arr}\"; do true; done" -prop_checkForInQuoted7 = verify checkForInQuoted "for f in ls, grep, mv; do true; done" -prop_checkForInQuoted8 = verify checkForInQuoted "for f in 'ls', 'grep', 'mv'; do true; done" -prop_checkForInQuoted9 = verifyNot checkForInQuoted "for f in 'ls,' 'grep,' 'mv'; do true; done" -checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]] _) - | any willSplit list && not (mayBecomeMultipleArgs word) - || maybe False wouldHaveBeenGlob (getLiteralString word) = - err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once." -checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id _]] _) = - warn id 2041 "This is a literal string. To run as a command, use $(..) instead of '..' . " -checkForInQuoted _ (T_ForIn _ _ [single] _) - | maybe False (',' `elem`) $ getUnquotedLiteral single = - warn (getId single) 2042 "Use spaces, not commas, to separate loop elements." - | not (willSplit single || mayBecomeMultipleArgs single) = - warn (getId single) 2043 "This loop will only ever run once. Bad quoting or missing glob/expansion?" -checkForInQuoted params (T_ForIn _ _ multiple _) = - forM_ multiple $ \arg -> sequence_ $ do - suffix <- getTrailingUnquotedLiteral arg - string <- getLiteralString suffix - guard $ "," `isSuffixOf` string - return $ - warnWithFix (getId arg) 2258 - "The trailing comma is part of the value, not a separator. Delete or quote it." - (fixWith [replaceEnd (getId suffix) params 1 ""]) -checkForInQuoted _ _ = return () - -prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done" -prop_checkForInCat1a = verify checkForInCat "for f in `cat foo`; do stuff; done" -prop_checkForInCat2 = verify checkForInCat "for f in $(cat foo | grep lol); do stuff; done" -prop_checkForInCat2a = verify checkForInCat "for f in `cat foo | grep lol`; do stuff; done" -prop_checkForInCat3 = verifyNot checkForInCat "for f in $(cat foo | grep bar | wc -l); do stuff; done" -checkForInCat _ (T_ForIn _ f [T_NormalWord _ w] _) = mapM_ checkF w - where - checkF (T_DollarExpansion id [T_Pipeline _ _ r]) - | all isLineBased r = - info id 2013 "To read lines rather than words, pipe/redirect to a 'while read' loop." - checkF (T_Backticked id cmds) = checkF (T_DollarExpansion id cmds) - checkF _ = return () - isLineBased cmd = any (cmd `isCommand`) - ["grep", "fgrep", "egrep", "sed", "cat", "awk", "cut", "sort"] -checkForInCat _ _ = return () - -prop_checkForInLs = verify checkForInLs "for f in $(ls *.mp3); do mplayer \"$f\"; done" -prop_checkForInLs2 = verify checkForInLs "for f in `ls *.mp3`; do mplayer \"$f\"; done" -prop_checkForInLs3 = verify checkForInLs "for f in `find / -name '*.mp3'`; do mplayer \"$f\"; done" -checkForInLs _ = try - where - try (T_ForIn _ f [T_NormalWord _ [T_DollarExpansion id [x]]] _) = - check id f x - try (T_ForIn _ f [T_NormalWord _ [T_Backticked id [x]]] _) = - check id f x - try _ = return () - check id f x = - case oversimplify x of - ("ls":n) -> - let warntype = if any ("-" `isPrefixOf`) n then warn else err in - warntype id 2045 "Iterating over ls output is fragile. Use globs." - ("find":_) -> warn id 2044 "For loops over find output are fragile. Use find -exec or a while read loop." - _ -> return () - - -prop_checkFindExec1 = verify checkFindExec "find / -name '*.php' -exec rm {};" -prop_checkFindExec2 = verify checkFindExec "find / -exec touch {} && ls {} \\;" -prop_checkFindExec3 = verify checkFindExec "find / -execdir cat {} | grep lol +" -prop_checkFindExec4 = verifyNot checkFindExec "find / -name '*.php' -exec foo {} +" -prop_checkFindExec5 = verifyNot checkFindExec "find / -execdir bash -c 'a && b' \\;" -prop_checkFindExec6 = verify checkFindExec "find / -type d -execdir rm *.jpg \\;" -checkFindExec _ cmd@(T_SimpleCommand _ _ t@(h:r)) | cmd `isCommand` "find" = do - c <- broken r False - when c $ - let wordId = getId $ last t in - err wordId 2067 "Missing ';' or + terminating -exec. You can't use |/||/&&, and ';' has to be a separate, quoted argument." - - where - broken [] v = return v - broken (w:r) v = do - when v (mapM_ warnFor $ fromWord w) - case getLiteralString w of - Just "-exec" -> broken r True - Just "-execdir" -> broken r True - Just "+" -> broken r False - Just ";" -> broken r False - _ -> broken r v - - shouldWarn x = - case x of - T_DollarExpansion _ _ -> True - T_Backticked _ _ -> True - T_Glob _ _ -> True - T_Extglob {} -> True - _ -> False - - warnFor x = - when(shouldWarn x) $ - info (getId x) 2014 "This will expand once before find runs, not per file found." - - fromWord (T_NormalWord _ l) = l - fromWord _ = [] -checkFindExec _ _ = return () - - -prop_checkUnquotedExpansions1 = verify checkUnquotedExpansions "rm $(ls)" -prop_checkUnquotedExpansions1a = verify checkUnquotedExpansions "rm `ls`" -prop_checkUnquotedExpansions2 = verify checkUnquotedExpansions "rm foo$(date)" -prop_checkUnquotedExpansions3 = verify checkUnquotedExpansions "[ $(foo) == cow ]" -prop_checkUnquotedExpansions3a = verify checkUnquotedExpansions "[ ! $(foo) ]" -prop_checkUnquotedExpansions4 = verifyNot checkUnquotedExpansions "[[ $(foo) == cow ]]" -prop_checkUnquotedExpansions5 = verifyNot checkUnquotedExpansions "for f in $(cmd); do echo $f; done" -prop_checkUnquotedExpansions6 = verifyNot checkUnquotedExpansions "$(cmd)" -prop_checkUnquotedExpansions7 = verifyNot checkUnquotedExpansions "cat << foo\n$(ls)\nfoo" -prop_checkUnquotedExpansions8 = verifyNot checkUnquotedExpansions "set -- $(seq 1 4)" -prop_checkUnquotedExpansions9 = verifyNot checkUnquotedExpansions "echo foo `# inline comment`" -prop_checkUnquotedExpansions10 = verify checkUnquotedExpansions "#!/bin/sh\nexport var=$(val)" -prop_checkUnquotedExpansions11 = verifyNot checkUnquotedExpansions "ps -p $(pgrep foo)" -checkUnquotedExpansions params = - check - where - check t@(T_DollarExpansion _ c) = examine t c - check t@(T_Backticked _ c) = examine t c - check t@(T_DollarBraceCommandExpansion _ _ c) = examine t c - check _ = return () - tree = parentMap params - examine t contents = - unless (null contents || shouldBeSplit t || isQuoteFree (shellType params) tree t || usedAsCommandName tree t) $ - warn (getId t) 2046 "Quote this to prevent word splitting." - - shouldBeSplit t = - getCommandNameFromExpansion t `elem` [Just "seq", Just "pgrep"] - - -prop_checkRedirectToSame = verify checkRedirectToSame "cat foo > foo" -prop_checkRedirectToSame2 = verify checkRedirectToSame "cat lol | sed -e 's/a/b/g' > lol" -prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol" -prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null" -prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar" -prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo" -prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file" -prop_checkRedirectToSame8 = verifyNot checkRedirectToSame "while read -r line; do _=\"$fname\"; done <\"$fname\"" -prop_checkRedirectToSame9 = verifyNot checkRedirectToSame "while read -r line; do cat < \"$fname\"; done <\"$fname\"" -prop_checkRedirectToSame10 = verifyNot checkRedirectToSame "mapfile -t foo (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list - where - note x = makeComment InfoC x 2094 - "Make sure not to read and write the same file in the same pipeline." - checkOccurrences t@(T_NormalWord exceptId x) u@(T_NormalWord newId y) | - exceptId /= newId - && x == y - && not (isInput t && isInput u) - && not (isOutput t && isOutput u) - && not (special t) - && not (any isHarmlessCommand [t,u]) - && not (any containsAssignment [u]) = do - addComment $ note newId - addComment $ note exceptId - checkOccurrences _ _ = return () - getAllRedirs = concatMap (\t -> - case t of - T_Redirecting _ ls _ -> concatMap getRedirs ls - _ -> []) - getRedirs (T_FdRedirect _ _ (T_IoFile _ op file)) = - case op of T_Greater _ -> [file] - T_Less _ -> [file] - T_DGREAT _ -> [file] - _ -> [] - getRedirs _ = [] - special x = "/dev/" `isPrefixOf` concat (oversimplify x) - isInput t = - case NE.tail $ getPath (parentMap params) t of - T_IoFile _ op _:_ -> - case op of - T_Less _ -> True - _ -> False - _ -> False - isOutput t = - case NE.tail $ getPath (parentMap params) t of - T_IoFile _ op _:_ -> - case op of - T_Greater _ -> True - T_DGREAT _ -> True - _ -> False - _ -> False - isHarmlessCommand arg = fromMaybe False $ do - cmd <- getClosestCommand (parentMap params) arg - name <- getCommandBasename cmd - return $ name `elem` ["echo", "mapfile", "printf", "sponge"] - containsAssignment arg = fromMaybe False $ do - cmd <- getClosestCommand (parentMap params) arg - return $ isAssignment cmd - -checkRedirectToSame _ _ = return () - - -prop_checkShorthandIf = verify checkShorthandIf "[[ ! -z file ]] && scp file host || rm file" -prop_checkShorthandIf2 = verifyNot checkShorthandIf "[[ ! -z file ]] && { scp file host || echo 'Eek'; }" -prop_checkShorthandIf3 = verifyNot checkShorthandIf "foo && bar || echo baz" -prop_checkShorthandIf4 = verifyNot checkShorthandIf "foo && a=b || a=c" -prop_checkShorthandIf5 = verifyNot checkShorthandIf "foo && rm || printf b" -prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi" -prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done" -prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi" -prop_checkShorthandIf9 = verifyNot checkShorthandIf "foo && [ -x /file ] || bar" -prop_checkShorthandIf10 = verifyNot checkShorthandIf "foo && bar || true" -prop_checkShorthandIf11 = verify checkShorthandIf "foo && bar || false" -checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ b) (T_Pipeline _ _ t)) - | not (isOk t || inCondition) && not (isTestCommand b) = - info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true." - where - isOk [t] = isAssignment t || fromMaybe False (do - name <- getCommandBasename t - return $ name `elem` ["echo", "exit", "return", "printf", "true", ":"]) - isOk _ = False - inCondition = isCondition $ getPath (parentMap params) x -checkShorthandIf _ _ = return () - - -prop_checkDollarStar = verify checkDollarStar "for f in $*; do ..; done" -prop_checkDollarStar2 = verifyNot checkDollarStar "a=$*" -prop_checkDollarStar3 = verifyNot checkDollarStar "[[ $* = 'a b' ]]" -prop_checkDollarStar4 = verify checkDollarStar "for f in ${var[*]}; do ..; done" -prop_checkDollarStar5 = verify checkDollarStar "ls ${*//foo/bar}" -prop_checkDollarStar6 = verify checkDollarStar "ls ${var[*]%%.*}" -prop_checkDollarStar7 = verify checkDollarStar "ls ${*}" -prop_checkDollarStar8 = verifyNot checkDollarStar "ls ${#*}" -prop_checkDollarStar9 = verify checkDollarStar "ls ${arr[*]}" -prop_checkDollarStar10 = verifyNot checkDollarStar "ls ${#arr[*]}" -checkDollarStar p t@(T_NormalWord _ [T_DollarBraced id _ l]) - | not (isStrictlyQuoteFree (shellType p) (parentMap p) t) = do - let str = concat (oversimplify l) - when ("*" `isPrefixOf` str) $ - warn id 2048 "Use \"$@\" (with quotes) to prevent whitespace problems." - when ("[*]" `isPrefixOf` (getBracedModifier str) && isVariableChar (headOrDefault '!' str)) $ - warn id 2048 "Use \"${array[@]}\" (with quotes) to prevent whitespace problems." - -checkDollarStar _ _ = return () - - -prop_checkUnquotedDollarAt = verify checkUnquotedDollarAt "ls $@" -prop_checkUnquotedDollarAt1 = verifyNot checkUnquotedDollarAt "ls ${#@}" -prop_checkUnquotedDollarAt2 = verify checkUnquotedDollarAt "ls ${foo[@]}" -prop_checkUnquotedDollarAt3 = verifyNot checkUnquotedDollarAt "ls ${#foo[@]}" -prop_checkUnquotedDollarAt4 = verifyNot checkUnquotedDollarAt "ls \"$@\"" -prop_checkUnquotedDollarAt5 = verifyNot checkUnquotedDollarAt "ls ${foo/@/ at }" -prop_checkUnquotedDollarAt6 = verifyNot checkUnquotedDollarAt "a=$@" -prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done" -prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\"" -prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}" -prop_checkUnquotedDollarAt10 = verifyNot checkUnquotedDollarAt "echo ${@+\"$@\"}" -checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (shellType p) (parentMap p) word = - forM_ (find isArrayExpansion parts) $ \x -> - unless (isQuotedAlternativeReference x) $ - err (getId x) 2068 - "Double quote array expansions to avoid re-splitting elements." -checkUnquotedDollarAt _ _ = return () - -prop_checkConcatenatedDollarAt1 = verify checkConcatenatedDollarAt "echo \"foo$@\"" -prop_checkConcatenatedDollarAt2 = verify checkConcatenatedDollarAt "echo ${arr[@]}lol" -prop_checkConcatenatedDollarAt3 = verify checkConcatenatedDollarAt "echo $a$@" -prop_checkConcatenatedDollarAt4 = verifyNot checkConcatenatedDollarAt "echo $@" -prop_checkConcatenatedDollarAt5 = verifyNot checkConcatenatedDollarAt "echo \"${arr[@]}\"" -checkConcatenatedDollarAt p word@T_NormalWord {} - | not $ isQuoteFree (shellType p) (parentMap p) word - || null (drop 1 parts) = - mapM_ for array - where - parts = getWordParts word - array = find isArrayExpansion parts - for t = err (getId t) 2145 "Argument mixes string and array. Use * or separate argument." -checkConcatenatedDollarAt _ _ = return () - -prop_checkArrayAsString1 = verify checkArrayAsString "a=$@" -prop_checkArrayAsString2 = verify checkArrayAsString "a=\"${arr[@]}\"" -prop_checkArrayAsString3 = verify checkArrayAsString "a=*.png" -prop_checkArrayAsString4 = verify checkArrayAsString "a={1..10}" -prop_checkArrayAsString5 = verifyNot checkArrayAsString "a='*.gif'" -prop_checkArrayAsString6 = verifyNot checkArrayAsString "a=$*" -prop_checkArrayAsString7 = verifyNot checkArrayAsString "a=( $@ )" -checkArrayAsString _ (T_Assignment id _ _ _ word) = - if willConcatInAssignment word - then - warn (getId word) 2124 - "Assigning an array to a string! Assign as array, or use * instead of @ to concatenate." - else - when (willBecomeMultipleArgs word) $ - warn (getId word) 2125 - "Brace expansions and globs are literal in assignments. Quote it or use an array." -checkArrayAsString _ _ = return () - -prop_checkArrayWithoutIndex1 = verifyTree checkArrayWithoutIndex "foo=(a b); echo $foo" -prop_checkArrayWithoutIndex2 = verifyNotTree checkArrayWithoutIndex "foo='bar baz'; foo=($foo); echo ${foo[0]}" -prop_checkArrayWithoutIndex3 = verifyTree checkArrayWithoutIndex "coproc foo while true; do echo cow; done; echo $foo" -prop_checkArrayWithoutIndex4 = verifyTree checkArrayWithoutIndex "coproc tail -f log; echo $COPROC" -prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo $a" -prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS" -prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c" -prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;" -prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\"" -prop_checkArrayWithoutIndex10 = verifyTree checkArrayWithoutIndex "read -ra arr <<< 'foo bar'; echo \"$arr\"" -prop_checkArrayWithoutIndex11 = verifyNotTree checkArrayWithoutIndex "read -rpfoobar r; r=42" -checkArrayWithoutIndex params _ = - doVariableFlowAnalysis readF writeF defaultSet (variableFlow params) - where - defaultSet = S.fromList arrayVariables - readF _ (T_DollarBraced id _ token) _ = do - s <- get - return . maybeToList $ do - name <- getLiteralString token - guard $ S.member name s - return $ makeComment WarningC id 2128 - "Expanding an array without an index only gives the first element." - readF _ _ _ = return [] - - writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do - isArray <- gets (S.member name) - return $ if not isArray then [] else - case mode of - Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."] - Append -> [makeComment WarningC id 2179 "Use array+=(\"item\") to append items to an array."] - - writeF _ t name (DataArray _) = do - modify (S.insert name) - return [] - writeF _ expr name _ = do - if isIndexed expr - then modify (S.insert name) - else modify (S.delete name) - return [] - - isIndexed expr = - case expr of - T_Assignment _ _ _ (_:_) _ -> True - _ -> False - -prop_checkStderrRedirect = verify checkStderrRedirect "test 2>&1 > cow" -prop_checkStderrRedirect2 = verifyNot checkStderrRedirect "test > cow 2>&1" -prop_checkStderrRedirect3 = verifyNot checkStderrRedirect "test 2>&1 > file | grep stderr" -prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)" -prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)" -prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null" -prop_checkStderrRedirect7 = verifyNot checkStderrRedirect "{ cmd > file; } 2>&1" -checkStderrRedirect params redir@(T_Redirecting _ [ - T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"), - T_FdRedirect _ _ (T_IoFile _ op _) - ] _) = case op of - T_Greater _ -> error - T_DGREAT _ -> error - _ -> return () - where - usesOutput t = - case t of - (T_Pipeline _ _ list) -> length list > 1 && not (isParentOf (parentMap params) (last list) redir) - T_ProcSub {} -> True - T_DollarExpansion {} -> True - T_Backticked {} -> True - _ -> False - isCaptured = any usesOutput $ getPath (parentMap params) redir - - error = unless isCaptured $ - warn id 2069 "To redirect stdout+stderr, 2>&1 must be last (or use '{ cmd > file; } 2>&1' to clarify)." - -checkStderrRedirect _ _ = return () - -lt x = trace ("Tracing " ++ show x) x -- STRIP -ltt t = trace ("Tracing " ++ show t) -- STRIP - - -prop_checkSingleQuotedVariables = verify checkSingleQuotedVariables "echo '$foo'" -prop_checkSingleQuotedVariables2 = verify checkSingleQuotedVariables "echo 'lol$1.jpg'" -prop_checkSingleQuotedVariables3 = verifyNot checkSingleQuotedVariables "sed 's/foo$/bar/'" -prop_checkSingleQuotedVariables3a = verify checkSingleQuotedVariables "sed 's/${foo}/bar/'" -prop_checkSingleQuotedVariables3b = verify checkSingleQuotedVariables "sed 's/$(echo cow)/bar/'" -prop_checkSingleQuotedVariables3c = verify checkSingleQuotedVariables "sed 's/$((1+foo))/bar/'" -prop_checkSingleQuotedVariables4 = verifyNot checkSingleQuotedVariables "awk '{print $1}'" -prop_checkSingleQuotedVariables5 = verifyNot checkSingleQuotedVariables "trap 'echo $SECONDS' EXIT" -prop_checkSingleQuotedVariables6 = verifyNot checkSingleQuotedVariables "sed -n '$p'" -prop_checkSingleQuotedVariables6a = verify checkSingleQuotedVariables "sed -n '$pattern'" -prop_checkSingleQuotedVariables7 = verifyNot checkSingleQuotedVariables "PS1='$PWD \\$ '" -prop_checkSingleQuotedVariables8 = verify checkSingleQuotedVariables "find . -exec echo '$1' {} +" -prop_checkSingleQuotedVariables9 = verifyNot checkSingleQuotedVariables "find . -exec awk '{print $1}' {} \\;" -prop_checkSingleQuotedVariables10 = verify checkSingleQuotedVariables "echo '`pwd`'" -prop_checkSingleQuotedVariables11 = verifyNot checkSingleQuotedVariables "sed '${/lol/d}'" -prop_checkSingleQuotedVariables12 = verifyNot checkSingleQuotedVariables "eval 'echo $1'" -prop_checkSingleQuotedVariables13 = verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'" -prop_checkSingleQuotedVariables14 = verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]" -prop_checkSingleQuotedVariables15 = verifyNot checkSingleQuotedVariables "git filter-branch 'test $GIT_COMMIT'" -prop_checkSingleQuotedVariables16 = verify checkSingleQuotedVariables "git '$a'" -prop_checkSingleQuotedVariables17 = verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *" -prop_checkSingleQuotedVariables18 = verifyNot checkSingleQuotedVariables "echo '``'" -prop_checkSingleQuotedVariables19 = verifyNot checkSingleQuotedVariables "echo '```'" -prop_checkSingleQuotedVariables20 = verifyNot checkSingleQuotedVariables "mumps -run %XCMD 'W $O(^GLOBAL(5))'" -prop_checkSingleQuotedVariables21 = verifyNot checkSingleQuotedVariables "mumps -run LOOP%XCMD --xec 'W $O(^GLOBAL(6))'" -prop_checkSingleQuotedVariables22 = verifyNot checkSingleQuotedVariables "jq '$__loc__'" -prop_checkSingleQuotedVariables23 = verifyNot checkSingleQuotedVariables "command jq '$__loc__'" -prop_checkSingleQuotedVariables24 = verifyNot checkSingleQuotedVariables "exec jq '$__loc__'" -prop_checkSingleQuotedVariables25 = verifyNot checkSingleQuotedVariables "exec -c -a foo jq '$__loc__'" - - -checkSingleQuotedVariables params t@(T_SingleQuoted id s) = - when (s `matches` re) $ - if "sed" == commandName - then unless (s `matches` sedContra) showMessage - else unless isProbablyOk showMessage - where - parents = parentMap params - showMessage = info id 2016 - "Expressions don't expand in single quotes, use double quotes for that." - commandName = fromMaybe "" $ do - cmd <- getClosestCommand parents t - name <- getCommandBasename cmd - return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else if name == "mumps" then getMumpsCommand cmd else name - - isProbablyOk = - any isOkAssignment (NE.take 3 $ getPath parents t) - || commandName `elem` [ - "trap" - ,"sh" - ,"bash" - ,"ksh" - ,"zsh" - ,"ssh" - ,"eval" - ,"xprop" - ,"alias" - ,"sudo" -- covering "sudo sh" and such - ,"doas" -- same as sudo - ,"run0" -- same as sudo - ,"docker" -- like above - ,"podman" - ,"oc" - ,"dpkg-query" - ,"jq" -- could also check that user provides --arg - ,"rename" - ,"rg" - ,"unset" - ,"git filter-branch" - ,"mumps -run %XCMD" - ,"mumps -run LOOP%XCMD" - ] - || "awk" `isSuffixOf` commandName - || "perl" `isPrefixOf` commandName - - commonlyQuoted = ["PS1", "PS2", "PS3", "PS4", "PROMPT_COMMAND"] - isOkAssignment t = - case t of - T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted - TC_Unary _ _ "-v" _ -> True - _ -> False - - re = mkRegex "\\$[{(0-9a-zA-Z_]|`[^`]+`" - sedContra = mkRegex "\\$[{dpsaic]($|[^a-zA-Z])" - - getFindCommand (T_SimpleCommand _ _ words) = - let list = map getLiteralString words - cmd = dropWhile (\x -> x /= Just "-exec" && x /= Just "-execdir") list - in - case cmd of - (flag:cmd:rest) -> fromMaybe "find" cmd - _ -> "find" - getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd - getFindCommand _ = "find" - getGitCommand (T_SimpleCommand _ _ words) = - case map getLiteralString words of - Just "git":Just "filter-branch":_ -> "git filter-branch" - _ -> "git" - getGitCommand (T_Redirecting _ _ cmd) = getGitCommand cmd - getGitCommand _ = "git" - getMumpsCommand (T_SimpleCommand _ _ words) = - case map getLiteralString words of - Just "mumps":Just "-run":Just "%XCMD":_ -> "mumps -run %XCMD" - Just "mumps":Just "-run":Just "LOOP%XCMD":_ -> "mumps -run LOOP%XCMD" - _ -> "mumps" - getMumpsCommand (T_Redirecting _ _ cmd) = getMumpsCommand cmd - getMumpsCommand _ = "mumps" -checkSingleQuotedVariables _ _ = return () - - -prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi" -prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]" -prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow" -prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]" -prop_checkUnquotedN5 = verifyNot checkUnquotedN "[ -n \"$@\" ]" -checkUnquotedN _ (TC_Unary _ SingleBracket "-n" t) | willSplit t = - unless (any isArrayExpansion $ getWordParts t) $ -- There's SC2198 for these - err (getId t) 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]." -checkUnquotedN _ _ = return () - -prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]" -prop_checkNumberComparisons2 = verify checkNumberComparisons "[[ 0 >= $(cmd) ]]" -prop_checkNumberComparisons3 = verifyNot checkNumberComparisons "[[ $foo ]] > 3" -prop_checkNumberComparisons4 = verify checkNumberComparisons "[[ $foo > 2.72 ]]" -prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]" -prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]]" -prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]" -prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]" -prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]" -prop_checkNumberComparisons11 = verify checkNumberComparisons "[ $foo -eq 'N' ]" -prop_checkNumberComparisons12 = verify checkNumberComparisons "[ x$foo -gt x${N} ]" -prop_checkNumberComparisons13 = verify checkNumberComparisons "[ $foo > $bar ]" -prop_checkNumberComparisons14 = verifyNot checkNumberComparisons "[[ foo < bar ]]" -prop_checkNumberComparisons15 = verifyNot checkNumberComparisons "[ $foo '>' $bar ]" -prop_checkNumberComparisons16 = verify checkNumberComparisons "[ foo -eq 'y' ]" -prop_checkNumberComparisons17 = verify checkNumberComparisons "[[ 'foo' -eq 2 ]]" -prop_checkNumberComparisons18 = verify checkNumberComparisons "[[ foo -eq 2 ]]" -prop_checkNumberComparisons19 = verifyNot checkNumberComparisons "foo=1; [[ foo -eq 2 ]]" -prop_checkNumberComparisons20 = verify checkNumberComparisons "[[ 2 -eq / ]]" -prop_checkNumberComparisons21 = verify checkNumberComparisons "[[ foo -eq foo ]]" -prop_checkNumberComparisons22 = verify checkNumberComparisons "x=10; [[ $x > $z ]]" -prop_checkNumberComparisons23 = verify checkNumberComparisons "x=0; if [[ -n $def ]]; then x=$def; fi; while [ $x > $z ]; do lol; done" -prop_checkNumberComparisons24 = verify checkNumberComparisons "x=$RANDOM; [ $x > $z ]" -prop_checkNumberComparisons25 = verify checkNumberComparisons "[[ $((n++)) > $x ]]" - -checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do - if isNum lhs || isNum rhs - then do - when (isLtGt op) $ - err id 2071 $ - op ++ " is for string comparisons. Use " ++ eqv op ++ " instead." - when (isLeGe op && hasStringComparison) $ - err id 2071 $ op ++ " is not a valid operator. " ++ - "Use " ++ eqv op ++ " ." - else do - when (isLeGe op || isLtGt op) $ - mapM_ checkDecimals [lhs, rhs] - - when (isLeGe op && hasStringComparison) $ - err id 2122 $ op ++ " is not a valid operator. " ++ - "Use '! a " ++ esc ++ invert op ++ " b' instead." - - when (typ == SingleBracket && op `elem` ["<", ">"]) $ - case shellType params of - Sh -> return () -- These are unsupported and will be caught by bashism checks. - Dash -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting." - BusyboxSh -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting." - _ -> err id 2073 $ "Escape \\" ++ op ++ " to prevent it redirecting (or switch to [[ .. ]])." - - when (op `elem` arithmeticBinaryTestOps) $ do - mapM_ checkDecimals [lhs, rhs] - mapM_ checkString [lhs, rhs] - - - where - hasStringComparison = shellType params /= Sh - isLtGt = flip elem ["<", "\\<", ">", "\\>"] - isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="] - - checkDecimals hs = - when (isFraction hs && not (hasFloatingPoint params)) $ - err (getId hs) 2072 decimalError - decimalError = "Decimals are not supported. " ++ - "Either use integers only, or use bc or awk to compare." - - checkString t = - let - asString = getLiteralStringDef "\0" t - isVar = isVariableName asString - kind = if isVar then "a variable" else "an arithmetic expression" - fix = if isVar then "$var" else "$((expr))" - in - when (isNonNum t) $ - if typ == SingleBracket - then - err (getId t) 2170 $ - "Invalid number for " ++ op ++ ". Use " ++ seqv op ++ - " to compare as string (or use " ++ fix ++ - " to expand as " ++ kind ++ ")." - else - -- We should warn if any of the following holds: - -- The string is not a variable name - -- Any part of it is quoted - -- It's not a recognized variable name - when (not isVar || any isQuotes (getWordParts t) || asString `notElem` assignedVariables) $ - warn (getId t) 2309 $ - op ++ " treats this as " ++ kind ++ ". " ++ - "Use " ++ seqv op ++ " to compare as string (or expand explicitly with " ++ fix ++ ")." - - assignedVariables :: [String] - assignedVariables = mapMaybe f (variableFlow params) - where - f t = do - Assignment (_, _, name, _) <- return t - return name - - isNonNum t = not . all numChar $ onlyLiteralString t - numChar x = isDigit x || x `elem` "+-. " - - isNum t = - case getWordParts t of - [T_DollarArithmetic {}] -> True - [b@(T_DollarBraced id _ c)] -> - let - str = concat $ oversimplify c - var = getBracedReference str - in fromMaybe False $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id - value <- Map.lookup var $ CF.variablesInScope state - return $ CF.numericalStatus (CF.variableValue value) >= CF.NumericalStatusMaybe - _ -> - case oversimplify t of - [v] -> all isDigit v - _ -> False - - isFraction t = - case oversimplify t of - [v] -> isJust $ matchRegex floatRegex v - _ -> False - - eqv ('\\':s) = eqv s - eqv "<" = "-lt" - eqv ">" = "-gt" - eqv "<=" = "-le" - eqv ">=" = "-ge" - eqv _ = "the numerical equivalent" - - esc = if typ == SingleBracket then "\\" else "" - seqv "-ge" = "! a " ++ esc ++ "< b" - seqv "-gt" = esc ++ ">" - seqv "-le" = "! a " ++ esc ++ "> b" - seqv "-lt" = esc ++ "<" - seqv "-eq" = "=" - seqv "-ne" = "!=" - seqv _ = "the string equivalent" - - invert ('\\':s) = invert s - invert "<=" = ">" - invert ">=" = "<" - - floatRegex = mkRegex "^[-+]?[0-9]+\\.[0-9]+$" -checkNumberComparisons _ _ = return () - -prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =~ foo ]" -checkSingleBracketOperators params (TC_Binary id SingleBracket "=~" lhs rhs) - | shellType params `elem` [Bash, Ksh] = - err id 2074 $ "Can't use =~ in [ ]. Use [[..]] instead." -checkSingleBracketOperators _ _ = return () - -prop_checkDoubleBracketOperators1 = verify checkDoubleBracketOperators "[[ 3 \\< 4 ]]" -prop_checkDoubleBracketOperators3 = verifyNot checkDoubleBracketOperators "[[ foo < bar ]]" -checkDoubleBracketOperators _ x@(TC_Binary id typ op lhs rhs) - | typ == DoubleBracket && op `elem` ["\\<", "\\>"] = - err id 2075 $ "Escaping " ++ op ++" is required in [..], but invalid in [[..]]" -checkDoubleBracketOperators _ _ = return () - -prop_checkConditionalAndOrs1 = verify checkConditionalAndOrs "[ foo && bar ]" -prop_checkConditionalAndOrs2 = verify checkConditionalAndOrs "[[ foo -o bar ]]" -prop_checkConditionalAndOrs3 = verifyNot checkConditionalAndOrs "[[ foo || bar ]]" -prop_checkConditionalAndOrs4 = verify checkConditionalAndOrs "[ foo -a bar ]" -prop_checkConditionalAndOrs5 = verify checkConditionalAndOrs "[ -z 3 -o a = b ]" -checkConditionalAndOrs _ t = - case t of - (TC_And id SingleBracket "&&" _ _) -> - err id 2107 "Instead of [ a && b ], use [ a ] && [ b ]." - (TC_And id DoubleBracket "-a" _ _) -> - err id 2108 "In [[..]], use && instead of -a." - (TC_Or id SingleBracket "||" _ _) -> - err id 2109 "Instead of [ a || b ], use [ a ] || [ b ]." - (TC_Or id DoubleBracket "-o" _ _) -> - err id 2110 "In [[..]], use || instead of -o." - - (TC_And id SingleBracket "-a" _ _) -> - warn id 2166 "Prefer [ p ] && [ q ] as [ p -a q ] is not well defined." - (TC_Or id SingleBracket "-o" _ _) -> - warn id 2166 "Prefer [ p ] || [ q ] as [ p -o q ] is not well defined." - - _ -> return () - -prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]" -prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]" -prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]" -prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]" -prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]" -prop_checkQuotedCondRegex6 = verify checkQuotedCondRegex "[[ $foo =~ 'cow|bar' ]]" -checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) = - case rhs of - T_NormalWord id [T_DoubleQuoted _ _] -> error rhs - T_NormalWord id [T_SingleQuoted _ _] -> error rhs - _ -> return () - where - error t = - unless (isConstantNonRe t) $ - warn (getId t) 2076 - "Remove quotes from right-hand side of =~ to match as a regex rather than literally." - re = mkRegex "[][*.+()|]" - hasMetachars s = s `matches` re - isConstantNonRe t = fromMaybe False $ do - s <- getLiteralString t - return . not $ hasMetachars s -checkQuotedCondRegex _ _ = return () - -prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]" -prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]" -prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]" -prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]" -prop_checkGlobbedRegex5 = verifyNot checkGlobbedRegex "[[ $foo =~ \\* ]]" -prop_checkGlobbedRegex6 = verifyNot checkGlobbedRegex "[[ $foo =~ (o*) ]]" -prop_checkGlobbedRegex7 = verifyNot checkGlobbedRegex "[[ $foo =~ \\*foo ]]" -prop_checkGlobbedRegex8 = verifyNot checkGlobbedRegex "[[ $foo =~ x\\* ]]" -checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) - | isConfusedGlobRegex s = - warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead." - where s = concat $ oversimplify rhs -checkGlobbedRegex _ _ = return () - - -prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]" -prop_checkConstantIfs2a = verify checkConstantIfs "[ n -le 4 ]" -prop_checkConstantIfs2b = verifyNot checkConstantIfs "[[ n -le 4 ]]" -prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n != 2 ]]" -prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]" -prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]" -prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]" -prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]" -prop_checkConstantIfs8 = verifyNot checkConstantIfs "[[ ~foo == '~foo' ]]" -prop_checkConstantIfs9 = verify checkConstantIfs "[[ *.png == [a-z] ]]" -prop_checkConstantIfs10 = verifyNot checkConstantIfs "[[ ~me == ~+ ]]" -prop_checkConstantIfs11 = verifyNot checkConstantIfs "[[ ~ == ~+ ]]" -prop_checkConstantIfs12 = verify checkConstantIfs "[[ '~' == x ]]" -checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic = - if isConstant lhs && isConstant rhs - then warn id 2050 "This expression is constant. Did you forget the $ on a variable?" - else checkUnmatchable id op lhs rhs - where - isDynamic = - op `elem` arithmeticBinaryTestOps - && typ == DoubleBracket - || op `elem` [ "-nt", "-ot", "-ef"] - - checkUnmatchable id op lhs rhs = - when (op `elem` ["=", "==", "!="] && not (wordsCanBeEqual lhs rhs)) $ - warn id 2193 "The arguments to this comparison can never be equal. Make sure your syntax is correct." -checkConstantIfs _ _ = return () - -prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]" -prop_checkLiteralBreakingTest2 = verify checkLiteralBreakingTest "[ $foo=3 ]" -prop_checkLiteralBreakingTest3 = verify checkLiteralBreakingTest "[ $foo!=3 ]" -prop_checkLiteralBreakingTest4 = verify checkLiteralBreakingTest "[ \"$(ls) \" ]" -prop_checkLiteralBreakingTest5 = verify checkLiteralBreakingTest "[ -n \"$(true) \" ]" -prop_checkLiteralBreakingTest6 = verify checkLiteralBreakingTest "[ -z $(true)z ]" -prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]" -prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]" -prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]" -checkLiteralBreakingTest _ t = sequence_ $ - case t of - (TC_Nullary _ _ w@(T_NormalWord _ l)) -> do - guard . not $ isConstant w -- Covered by SC2078 - comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings." - (TC_Unary _ _ op w@(T_NormalWord _ l)) -> - case op of - "-n" -> tautologyWarning w "Argument to -n is always true due to literal strings." - "-z" -> tautologyWarning w "Argument to -z is always false due to literal strings." - _ -> fail "not relevant" - _ -> fail "not my problem" - where - hasEquals = matchToken ('=' `elem`) - isNonEmpty = matchToken (not . null) - matchToken m t = maybe False m (getLiteralString t) - - comparisonWarning list = do - token <- find hasEquals list - return $ err (getId token) 2077 "You need spaces around the comparison operator." - tautologyWarning t s = do - token <- find isNonEmpty $ getWordParts t - return $ err (getId token) 2157 s - -prop_checkConstantNullary = verify checkConstantNullary "[[ '$(foo)' ]]" -prop_checkConstantNullary2 = verify checkConstantNullary "[ \"-f lol\" ]" -prop_checkConstantNullary3 = verify checkConstantNullary "[[ cmd ]]" -prop_checkConstantNullary4 = verify checkConstantNullary "[[ ! cmd ]]" -prop_checkConstantNullary5 = verify checkConstantNullary "[[ true ]]" -prop_checkConstantNullary6 = verify checkConstantNullary "[ 1 ]" -prop_checkConstantNullary7 = verify checkConstantNullary "[ false ]" -checkConstantNullary _ (TC_Nullary _ _ t) | isConstant t = - case onlyLiteralString t of - "false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets." - "0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead." - "true" -> style (getId t) 2160 "Instead of '[ true ]', just use 'true'." - "1" -> style (getId t) 2161 "Instead of '[ 1 ]', use 'true'." - _ -> err (getId t) 2078 "This expression is constant. Did you forget a $ somewhere?" - where - string = onlyLiteralString t - -checkConstantNullary _ _ = return () - -prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" -prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" -prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" -checkForDecimals params t@(TA_Expansion id _) = sequence_ $ do - guard $ not (hasFloatingPoint params) - first:rest <- getLiteralString t - guard $ isDigit first && '.' `elem` rest - return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." -checkForDecimals _ _ = return () - -prop_checkDivBeforeMult = verify checkDivBeforeMult "echo $((c/n*100))" -prop_checkDivBeforeMult2 = verifyNot checkDivBeforeMult "echo $((c*100/n))" -prop_checkDivBeforeMult3 = verifyNot checkDivBeforeMult "echo $((c/10*10))" -checkDivBeforeMult params (TA_Binary _ "*" (TA_Binary id "/" _ x) y) - | not (hasFloatingPoint params) && x /= y = - info id 2017 "Increase precision by replacing a/b*c with a*c/b." -checkDivBeforeMult _ _ = return () - -prop_checkArithmeticDeref = verify checkArithmeticDeref "echo $((3+$foo))" -prop_checkArithmeticDeref2 = verify checkArithmeticDeref "cow=14; (( s+= $cow ))" -prop_checkArithmeticDeref3 = verifyNot checkArithmeticDeref "cow=1/40; (( s+= ${cow%%/*} ))" -prop_checkArithmeticDeref4 = verifyNot checkArithmeticDeref "(( ! $? ))" -prop_checkArithmeticDeref5 = verifyNot checkArithmeticDeref "(($1))" -prop_checkArithmeticDeref6 = verify checkArithmeticDeref "(( a[$i] ))" -prop_checkArithmeticDeref7 = verifyNot checkArithmeticDeref "(( 10#$n ))" -prop_checkArithmeticDeref8 = verifyNot checkArithmeticDeref "let i=$i+1" -prop_checkArithmeticDeref9 = verifyNot checkArithmeticDeref "(( a[foo] ))" -prop_checkArithmeticDeref10 = verifyNot checkArithmeticDeref "(( a[\\$foo] ))" -prop_checkArithmeticDeref11 = verify checkArithmeticDeref "a[$foo]=wee" -prop_checkArithmeticDeref11b = verifyNot checkArithmeticDeref "declare -A a; a[$foo]=wee" -prop_checkArithmeticDeref12 = verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done" -prop_checkArithmeticDeref13 = verifyNot checkArithmeticDeref "(( $$ ))" -prop_checkArithmeticDeref14 = verifyNot checkArithmeticDeref "(( $! ))" -prop_checkArithmeticDeref15 = verifyNot checkArithmeticDeref "(( ${!var} ))" -prop_checkArithmeticDeref16 = verifyNot checkArithmeticDeref "(( ${x+1} + ${x=42} ))" -checkArithmeticDeref params t@(TA_Expansion _ [T_DollarBraced id _ l]) = - unless (isException $ concat $ oversimplify l) getWarning - where - isException [] = True - isException s@(h:_) = any (`elem` "/.:#%?*@$-!+=^,") s || isDigit h - getWarning = fromMaybe noWarning . msum . NE.map warningFor $ parents params t - warningFor t = - case t of - T_Arithmetic {} -> return normalWarning - T_DollarArithmetic {} -> return normalWarning - T_ForArithmetic {} -> return normalWarning - T_Assignment {} -> return normalWarning - T_SimpleCommand {} -> return noWarning - _ -> Nothing - - normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables." - noWarning = return () -checkArithmeticDeref _ _ = return () - -prop_checkArithmeticBadOctal1 = verify checkArithmeticBadOctal "(( 0192 ))" -prop_checkArithmeticBadOctal2 = verifyNot checkArithmeticBadOctal "(( 0x192 ))" -prop_checkArithmeticBadOctal3 = verifyNot checkArithmeticBadOctal "(( 1 ^ 0777 ))" -checkArithmeticBadOctal _ t@(TA_Expansion id _) = sequence_ $ do - str <- getLiteralString t - guard $ str `matches` octalRE - return $ err id 2080 "Numbers with leading 0 are considered octal." - where - octalRE = mkRegex "^0[0-7]*[8-9]" -checkArithmeticBadOctal _ _ = return () - -prop_checkComparisonAgainstGlob = verify checkComparisonAgainstGlob "[[ $cow == $bar ]]" -prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow == \"$bar\" ]]" -prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]" -prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]" -prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]" -prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]" -prop_checkComparisonAgainstGlob7 = verify checkComparisonAgainstGlob "#!/bin/busybox sh\n[[ $f == *foo* ]]" -checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _ _])) - | op `elem` ["=", "==", "!="] = - warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching." -checkComparisonAgainstGlob params (TC_Binary _ SingleBracket op _ word) - | op `elem` ["=", "==", "!="] && isGlob word = - err (getId word) 2081 msg - where - msg = if (shellType params) `elem` [Bash, Ksh] -- Busybox does not support glob matching - then "[ .. ] can't match globs. Use [[ .. ]] or case statement." - else "[ .. ] can't match globs. Use a case statement." - -checkComparisonAgainstGlob params (TC_Binary _ DoubleBracket op _ word) - | shellType params == BusyboxSh && op `elem` ["=", "==", "!="] && isGlob word = - err (getId word) 2330 "BusyBox [[ .. ]] does not support glob matching. Use a case statement." - -checkComparisonAgainstGlob _ _ = return () - -prop_checkCaseAgainstGlob1 = verify checkCaseAgainstGlob "case foo in lol$n) foo;; esac" -prop_checkCaseAgainstGlob2 = verify checkCaseAgainstGlob "case foo in $(foo)) foo;; esac" -prop_checkCaseAgainstGlob3 = verifyNot checkCaseAgainstGlob "case foo in *$bar*) foo;; esac" -checkCaseAgainstGlob _ t = - case t of - (T_CaseExpression _ _ cases) -> mapM_ check cases - _ -> return () - where - check (_, list, _) = mapM_ check' list - check' expr@(T_NormalWord _ list) - -- If it's already a glob, assume that's what the user wanted - | not (isGlob expr) && any isQuoteableExpansion list = - warn (getId expr) 2254 "Quote expansions in case patterns to match literally rather than as a glob." - check' _ = return () - -prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)" -prop_checkCommarrays2 = verify checkCommarrays "a+=(1,2,3)" -prop_checkCommarrays3 = verifyNot checkCommarrays "cow=(1 \"foo,bar\" 3)" -prop_checkCommarrays4 = verifyNot checkCommarrays "cow=('one,' 'two')" -prop_checkCommarrays5 = verify checkCommarrays "a=([a]=b, [c]=d)" -prop_checkCommarrays6 = verify checkCommarrays "a=([a]=b,[c]=d,[e]=f)" -prop_checkCommarrays7 = verify checkCommarrays "a=(1,2)" -checkCommarrays _ (T_Array id l) = - when (any (isCommaSeparated . literal) l) $ - warn id 2054 "Use spaces, not commas, to separate array elements." - where - literal (T_IndexedElement _ _ l) = literal l - literal (T_NormalWord _ l) = concatMap literal l - literal (T_Literal _ str) = str - literal _ = "" - - isCommaSeparated = elem ',' -checkCommarrays _ _ = return () - -prop_checkOrNeq1 = verify checkOrNeq "if [[ $lol -ne cow || $lol -ne foo ]]; then echo foo; fi" -prop_checkOrNeq2 = verify checkOrNeq "(( a!=lol || a!=foo ))" -prop_checkOrNeq3 = verify checkOrNeq "[ \"$a\" != lol || \"$a\" != foo ]" -prop_checkOrNeq4 = verifyNot checkOrNeq "[ a != $cow || b != $foo ]" -prop_checkOrNeq5 = verifyNot checkOrNeq "[[ $a != /home || $a != */public_html/* ]]" -prop_checkOrNeq6 = verify checkOrNeq "[ $a != a ] || [ $a != b ]" -prop_checkOrNeq7 = verify checkOrNeq "[ $a != a ] || [ $a != b ] || true" -prop_checkOrNeq8 = verifyNot checkOrNeq "[[ $a != x || $a != x ]]" -prop_checkOrNeq9 = verifyNot checkOrNeq "[ 0 -ne $FOO ] || [ 0 -ne $BAR ]" --- This only catches the most idiomatic cases. Fixme? - --- For test-level "or": [ x != y -o x != z ] -checkOrNeq _ (TC_Or id typ op (TC_Binary _ _ op1 lhs1 rhs1 ) (TC_Binary _ _ op2 lhs2 rhs2)) - | (op1 == op2 && (op1 == "-ne" || op1 == "!=")) && lhs1 == lhs2 && rhs1 /= rhs2 && not (any isGlob [rhs1,rhs2]) = - warn id 2055 $ "You probably wanted " ++ (if typ == SingleBracket then "-a" else "&&") ++ " here, otherwise it's always true." - --- For arithmetic context "or" -checkOrNeq _ (TA_Binary id "||" (TA_Binary _ "!=" word1 _) (TA_Binary _ "!=" word2 _)) - | word1 == word2 = - warn id 2056 "You probably wanted && here, otherwise it's always true." - --- For command level "or": [ x != y ] || [ x != z ] -checkOrNeq _ (T_OrIf id lhs rhs) = sequence_ $ do - (lhs1, op1, rhs1) <- getExpr lhs - (lhs2, op2, rhs2) <- getExpr rhs - guard $ op1 == op2 && op1 `elem` ["-ne", "!="] - guard $ lhs1 == lhs2 && rhs1 /= rhs2 - guard . not $ any isGlob [rhs1, rhs2] - return $ warn id 2252 "You probably wanted && here, otherwise it's always true." - where - getExpr x = - case x of - T_OrIf _ lhs _ -> getExpr lhs -- Fetches x and y in `T_OrIf x (T_OrIf y z)` - T_Pipeline _ _ [x] -> getExpr x - T_Redirecting _ _ c -> getExpr c - T_Condition _ _ c -> getExpr c - TC_Binary _ _ op lhs rhs -> orient (lhs, op, rhs) - _ -> Nothing - - -- Swap items so that the constant side is rhs (or Nothing if both/neither is constant) - orient (lhs, op, rhs) = - case (isConstant lhs, isConstant rhs) of - (True, False) -> return (rhs, op, lhs) - (False, True) -> return (lhs, op, rhs) - _ -> Nothing - - -checkOrNeq _ _ = return () - - -prop_checkAndEq1 = verifyNot checkAndEq "cow=0; foo=0; if [[ $lol -eq cow && $lol -eq foo ]]; then echo foo; fi" -prop_checkAndEq2 = verifyNot checkAndEq "lol=0 foo=0; (( a==lol && a==foo ))" -prop_checkAndEq3 = verify checkAndEq "[ \"$a\" = lol && \"$a\" = foo ]" -prop_checkAndEq4 = verifyNot checkAndEq "[ a = $cow && b = $foo ]" -prop_checkAndEq5 = verifyNot checkAndEq "[[ $a = /home && $a = */public_html/* ]]" -prop_checkAndEq6 = verify checkAndEq "[ $a = a ] && [ $a = b ]" -prop_checkAndEq7 = verify checkAndEq "[ $a = a ] && [ $a = b ] || true" -prop_checkAndEq8 = verifyNot checkAndEq "[[ $a == x && $a == x ]]" -prop_checkAndEq9 = verifyNot checkAndEq "[ 0 -eq $FOO ] && [ 0 -eq $BAR ]" -prop_checkAndEq10 = verify checkAndEq "(( a == 1 && a == 2 ))" -prop_checkAndEq11 = verify checkAndEq "[ $x -eq 1 ] && [ $x -eq 2 ]" -prop_checkAndEq12 = verify checkAndEq "[ 1 -eq $x ] && [ $x -eq 2 ]" -prop_checkAndEq13 = verifyNot checkAndEq "[ 1 -eq $x ] && [ $x -eq 1 ]" -prop_checkAndEq14 = verifyNot checkAndEq "[ $a = $b ] && [ $a = $c ]" - -checkAndEqOperands "-eq" rhs1 rhs2 = isLiteralNumber rhs1 && isLiteralNumber rhs2 -checkAndEqOperands op rhs1 rhs2 | op == "=" || op == "==" = isLiteral rhs1 && isLiteral rhs2 -checkAndEqOperands _ _ _ = False - --- For test-level "and": [ x = y -a x = z ] -checkAndEq _ (TC_And id typ op (TC_Binary _ _ op1 lhs1 rhs1 ) (TC_Binary _ _ op2 lhs2 rhs2)) - | op1 == op2 && lhs1 == lhs2 && rhs1 /= rhs2 && checkAndEqOperands op1 rhs1 rhs2 = - warn id 2333 $ "You probably wanted " ++ (if typ == SingleBracket then "-o" else "||") ++ " here, otherwise it's always false." - --- For arithmetic context "and" -checkAndEq _ (TA_Binary id "&&" (TA_Binary _ "==" lhs1 rhs1) (TA_Binary _ "==" lhs2 rhs2)) - | lhs1 == lhs2 && isLiteralNumber rhs1 && isLiteralNumber rhs2 = - warn id 2334 "You probably wanted || here, otherwise it's always false." - --- For command level "and": [ x = y ] && [ x = z ] -checkAndEq _ (T_AndIf id lhs rhs) = sequence_ $ do - (lhs1, op1, rhs1) <- getExpr lhs - (lhs2, op2, rhs2) <- getExpr rhs - guard $ op1 == op2 - guard $ lhs1 == lhs2 && rhs1 /= rhs2 - guard $ checkAndEqOperands op1 rhs1 rhs2 - return $ warn id 2333 "You probably wanted || here, otherwise it's always false." - where - getExpr x = - case x of - T_AndIf _ lhs _ -> getExpr lhs -- Fetches x and y in `T_AndIf x (T_AndIf y z)` - T_Pipeline _ _ [x] -> getExpr x - T_Redirecting _ _ c -> getExpr c - T_Condition _ _ c -> getExpr c - TC_Binary _ _ op lhs rhs -> orient (lhs, op, rhs) - _ -> Nothing - - -- Swap items so that the constant side is rhs (or Nothing if both/neither is constant) - orient (lhs, op, rhs) = - case (isConstant lhs, isConstant rhs) of - (True, False) -> return (rhs, op, lhs) - (False, True) -> return (lhs, op, rhs) - _ -> Nothing - - -checkAndEq _ _ = return () - - -prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]" -prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]" -prop_checkValidCondOps2a = verifyNot checkValidCondOps "[ 3 \\> 2 ]" -prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]" -prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]" -checkValidCondOps _ (TC_Binary id _ s _ _) - | s `notElem` binaryTestOps = - warn id 2057 "Unknown binary operator." -checkValidCondOps _ (TC_Unary id _ s _) - | s `notElem` unaryTestOps = - warn id 2058 "Unknown unary operator." -checkValidCondOps _ _ = return () - -prop_checkUuoeVar1 = verify checkUuoeVar "for f in $(echo $tmp); do echo lol; done" -prop_checkUuoeVar2 = verify checkUuoeVar "date +`echo \"$format\"`" -prop_checkUuoeVar3 = verifyNot checkUuoeVar "foo \"$(echo -e '\r')\"" -prop_checkUuoeVar4 = verifyNot checkUuoeVar "echo $tmp" -prop_checkUuoeVar5 = verify checkUuoeVar "foo \"$(echo \"$(date) value:\" $value)\"" -prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\"" -prop_checkUuoeVar7 = verifyNot checkUuoeVar "foo $(echo $(bar))" -- covered by 2005 -prop_checkUuoeVar8 = verifyNot checkUuoeVar "#!/bin/sh\nz=$(echo)" -prop_checkUuoeVar9 = verify checkUuoeVar "foo $(echo $( check id cmd - T_DollarExpansion id [cmd] -> check id cmd - _ -> return () - where - couldBeOptimized f = case f of - T_Glob {} -> False - T_Extglob {} -> False - T_BraceExpansion {} -> False - T_NormalWord _ l -> all couldBeOptimized l - T_DoubleQuoted _ l -> all couldBeOptimized l - _ -> True - - check id (T_Pipeline _ _ [T_Redirecting _ _ c]) = warnForEcho id c - check _ _ = return () - isCovered first rest = null rest && tokenIsJustCommandOutput first - warnForEcho id = checkUnqualifiedCommand "echo" $ \_ vars -> - case vars of - (first:rest) -> - unless (isCovered first rest || "-" `isPrefixOf` onlyLiteralString first) $ - when (all couldBeOptimized vars) $ style id 2116 - "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'." - _ -> return () - - -prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1" -prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1" -prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo" -prop_checkTestRedirects4 = verifyNot checkTestRedirects "test 1 -eq 2 2> file" -checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" = - mapM_ check redirs - where - check t = - when (suspicious t) $ - warn (getId t) 2065 "This is interpreted as a shell file redirection, not a comparison." - suspicious t = -- Ignore redirections of stderr because these are valid for squashing e.g. int errors, - case t of -- and >> and similar redirections because these are probably not comparisons. - T_FdRedirect _ fd (T_IoFile _ op _) -> fd /= "2" && isComparison op - _ -> False - isComparison t = - case t of - T_Greater _ -> True - T_Less _ -> True - _ -> False -checkTestRedirects _ _ = return () - -prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '" -prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" -prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '" -prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '" -prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '" -prop_checkPS14a = verify checkPS1Assignments "export PS1=$'\\e[3m; '" -prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '" -prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '" -prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'" -prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'" -checkPS1Assignments _ (T_Assignment _ _ "PS1" _ word) = warnFor word - where - warnFor word = - let contents = concat $ oversimplify word in - when (containsUnescaped contents) $ - info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues" - containsUnescaped s = - let unenclosed = subRegex enclosedRegex s "" in - isJust $ matchRegex escapeRegex unenclosed - enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager - escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" -checkPS1Assignments _ _ = return () - -prop_checkBackticks1 = verify checkBackticks "echo `foo`" -prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)" -prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo" -checkBackticks params (T_Backticked id list) | not (null list) = - addComment $ - makeCommentWithFix StyleC id 2006 "Use $(...) notation instead of legacy backticks `...`." - (fixWith [replaceStart id params 1 "$(", replaceEnd id params 1 ")"]) -checkBackticks _ _ = return () - - -prop_checkBadParameterSubstitution1 = verify checkBadParameterSubstitution "${foo$n}" -prop_checkBadParameterSubstitution2 = verifyNot checkBadParameterSubstitution "${foo//$n/lol}" -prop_checkBadParameterSubstitution3 = verify checkBadParameterSubstitution "${$#}" -prop_checkBadParameterSubstitution4 = verify checkBadParameterSubstitution "${var${n}_$((i%2))}" -prop_checkBadParameterSubstitution5 = verifyNot checkBadParameterSubstitution "${bar}" -prop_checkBadParameterSubstitution6 = verify checkBadParameterSubstitution "${\"bar\"}" -prop_checkBadParameterSubstitution7 = verify checkBadParameterSubstitution "${{var}" -prop_checkBadParameterSubstitution8 = verify checkBadParameterSubstitution "${$(x)//x/y}" -prop_checkBadParameterSubstitution9 = verifyNot checkBadParameterSubstitution "$# ${#} $! ${!} ${!#} ${#!}" -prop_checkBadParameterSubstitution10 = verify checkBadParameterSubstitution "${'foo'}" -prop_checkBadParameterSubstitution11 = verify checkBadParameterSubstitution "${${x%.*}##*/}" - -checkBadParameterSubstitution _ t = - case t of - (T_DollarBraced i _ (T_NormalWord _ contents@(first:_))) -> - if isIndirection contents - then err i 2082 "To expand via indirection, use arrays, ${!name} or (for sh only) eval." - else checkFirst first - _ -> return () - - where - - isIndirection vars = - let list = mapMaybe isIndirectionPart vars in - not (null list) && and list - - isIndirectionPart t = - case t of T_DollarExpansion {} -> Just True - T_Backticked {} -> Just True - T_DollarBraced {} -> Just True - T_DollarArithmetic {} -> Just True - T_Literal _ s -> if all isVariableChar s - then Nothing - else Just False - _ -> Just False - - checkFirst t = - case t of - T_Literal id (c:_) -> - if isVariableChar c || isSpecialVariableChar c - then return () - else err id 2296 $ "Parameter expansions can't start with " ++ e4m [c] ++ ". Double check syntax." - - T_ParamSubSpecialChar {} -> return () - - T_DoubleQuoted id [T_Literal _ s] | isVariable s -> - err id 2297 "Double quotes must be outside ${}: ${\"invalid\"} vs \"${valid}\"." - - T_DollarBraced id braces _ | isUnmodifiedParameterExpansion t -> - err id 2298 $ - (if braces then "${${x}}" else "${$x}") - ++ " is invalid. For expansion, use ${x}. For indirection, use arrays, ${!x} or (for sh) eval." - - T_DollarBraced {} -> - err (getId t) 2299 "Parameter expansions can't be nested. Use temporary variables." - - _ | isCommandSubstitution t -> - err (getId t) 2300 "Parameter expansion can't be applied to command substitutions. Use temporary variables." - - _ -> err (getId t) 2301 $ "Parameter expansion starts with unexpected " ++ name t ++ ". Double check syntax." - - isVariable str = - case str of - [c] -> isVariableStartChar c || isSpecialVariableChar c || isDigit c - x -> isVariableName x - - name t = - case t of - T_SingleQuoted {} -> "quotes" - T_DoubleQuoted {} -> "quotes" - _ -> "syntax" - - -prop_checkInexplicablyUnquoted1 = verify checkInexplicablyUnquoted "echo 'var='value';'" -prop_checkInexplicablyUnquoted2 = verifyNot checkInexplicablyUnquoted "'foo'*" -prop_checkInexplicablyUnquoted3 = verifyNot checkInexplicablyUnquoted "wget --user-agent='something'" -prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUES (\"id\")\"" -prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\"" -prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\"" -prop_checkInexplicablyUnquoted7 = verifyNot checkInexplicablyUnquoted "${dir/\"foo\"/\"bar\"}" -prop_checkInexplicablyUnquoted8 = verifyNot checkInexplicablyUnquoted " 'foo'\\\n 'bar'" -prop_checkInexplicablyUnquoted9 = verifyNot checkInexplicablyUnquoted "[[ $x =~ \"foo\"(\"bar\"|\"baz\") ]]" -prop_checkInexplicablyUnquoted10 = verifyNot checkInexplicablyUnquoted "cmd ${x+--name=\"$x\" --output=\"$x.out\"}" -prop_checkInexplicablyUnquoted11 = verifyNot checkInexplicablyUnquoted "echo \"foo\"/\"bar\"" -prop_checkInexplicablyUnquoted12 = verifyNot checkInexplicablyUnquoted "declare \"foo\"=\"bar\"" -checkInexplicablyUnquoted params (T_NormalWord id tokens) = mapM_ check (tails tokens) - where - check (T_SingleQuoted _ _:T_Literal id str:_) - | not (null str) && all isAlphaNum str = - info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? " - - check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) = - case trapped of - T_DollarExpansion id _ -> warnAboutExpansion id - T_DollarBraced id _ _ -> warnAboutExpansion id - T_Literal id s - | not (quotesSingleThing a && quotesSingleThing b - || s `elem` ["=", ":", "/"] - || isSpecial (NE.toList $ getPath (parentMap params) trapped) - ) -> - warnAboutLiteral id - _ -> return () - - check _ = return () - - -- Regexes for [[ .. =~ re ]] are parsed with metacharacters like ()| as unquoted - -- literals. The same is true for ${x+"foo" "bar"}. Avoid overtriggering on these. - isSpecial t = - case t of - (T_Redirecting {} : _) -> False - T_DollarBraced {} : _ -> True - (a:(TC_Binary _ _ "=~" lhs rhs):rest) -> getId a == getId rhs - _:rest -> isSpecial rest - _ -> False - - -- If the surrounding quotes quote single things, like "$foo"_and_then_some_"$stuff", - -- the quotes were probably intentional and harmless. - quotesSingleThing x = case x of - [T_DollarExpansion _ _] -> True - [T_DollarBraced _ _ _] -> True - [T_Backticked _ _] -> True - _ -> False - - warnAboutExpansion id = - warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them." - warnAboutLiteral id = - warn id 2140 "Word is of the form \"A\"B\"C\" (B indicated). Did you mean \"ABC\" or \"A\\\"B\\\"C\"?" -checkInexplicablyUnquoted _ _ = return () - -prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\"" -prop_checkTildeInQuotes2 = verify checkTildeInQuotes "foo > '~/dir'" -prop_checkTildeInQuotes4 = verifyNot checkTildeInQuotes "~/file" -prop_checkTildeInQuotes5 = verifyNot checkTildeInQuotes "echo '/~foo/cow'" -prop_checkTildeInQuotes6 = verifyNot checkTildeInQuotes "awk '$0 ~ /foo/'" -checkTildeInQuotes _ = check - where - verify id ('~':'/':_) = warn id 2088 "Tilde does not expand in quotes. Use $HOME." - verify _ _ = return () - check (T_NormalWord _ (T_SingleQuoted id str:_)) = - verify id str - check (T_NormalWord _ (T_DoubleQuoted _ (T_Literal id str:_):_)) = - verify id str - check _ = return () - -prop_checkLonelyDotDash1 = verify checkLonelyDotDash "./ file" -prop_checkLonelyDotDash2 = verifyNot checkLonelyDotDash "./file" -checkLonelyDotDash _ t@(T_Redirecting id _ _) - | isUnqualifiedCommand t "./" = - err id 2083 "Don't add spaces after the slash in './file'." -checkLonelyDotDash _ _ = return () - - -prop_checkSpuriousExec1 = verify checkSpuriousExec "exec foo; true" -prop_checkSpuriousExec2 = verify checkSpuriousExec "if a; then exec b; exec c; fi" -prop_checkSpuriousExec3 = verifyNot checkSpuriousExec "echo cow; exec foo" -prop_checkSpuriousExec4 = verifyNot checkSpuriousExec "if a; then exec b; fi" -prop_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd" -prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd" -prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3" -prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.log 2>&1; bar" -prop_checkSpuriousExec9 = verify checkSpuriousExec "for file in rc.d/*; do exec \"$file\"; done" -prop_checkSpuriousExec10 = verifyNot checkSpuriousExec "exec file; r=$?; printf >&2 'failed\n'; return $r" -prop_checkSpuriousExec11 = verifyNot checkSpuriousExec "exec file; :" -prop_checkSpuriousExec12 = verifyNot checkSpuriousExec "#!/bin/bash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;" -prop_checkSpuriousExec13 = verify checkSpuriousExec "#!/bin/dash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;" -checkSpuriousExec params t = when (not $ hasExecfail params) $ doLists t - where - doLists (T_Script _ _ cmds) = doList cmds False - doLists (T_BraceGroup _ cmds) = doList cmds False - doLists (T_WhileExpression _ _ cmds) = doList cmds True - doLists (T_UntilExpression _ _ cmds) = doList cmds True - doLists (T_ForIn _ _ _ cmds) = doList cmds True - doLists (T_ForArithmetic _ _ _ _ cmds) = doList cmds True - doLists (T_IfExpression _ thens elses) = do - mapM_ (\(_, l) -> doList l False) thens - doList elses False - doLists _ = return () - - stripCleanup = reverse . dropWhile cleanup . reverse - cleanup (T_Pipeline _ _ [cmd]) = - isCommandMatch cmd (`elem` [":", "echo", "exit", "printf", "return"]) - || isAssignment cmd - cleanup _ = False - - doList = doList' . stripCleanup - -- The second parameter is True if we are in a loop - -- In that case we should emit the warning also if `exec' is the last statement - doList' (current:t@(following:_)) False = do - commentIfExec current - doList t False - doList' (current:tail) True = do - commentIfExec current - doList tail True - doList' _ _ = return () - - commentIfExec (T_Pipeline id _ [c]) = commentIfExec c - commentIfExec (T_Redirecting _ _ (T_SimpleCommand id _ (cmd:additionalArg:_))) | - getLiteralString cmd == Just "exec" = - warn id 2093 "Remove \"exec \" if script should continue after this command." - commentIfExec _ = return () - - -prop_checkSpuriousExpansion1 = verify checkSpuriousExpansion "if $(true); then true; fi" -prop_checkSpuriousExpansion3 = verifyNot checkSpuriousExpansion "$(cmd) --flag1 --flag2" -prop_checkSpuriousExpansion4 = verify checkSpuriousExpansion "$((i++))" -checkSpuriousExpansion _ (T_SimpleCommand _ _ [T_NormalWord _ [word]]) = check word - where - check word = case word of - T_DollarExpansion id _ -> - warn id 2091 "Remove surrounding $() to avoid executing output (or use eval if intentional)." - T_Backticked id _ -> - warn id 2092 "Remove backticks to avoid executing output (or use eval if intentional)." - T_DollarArithmetic id _ -> - err id 2084 "Remove '$' or use '_=$((expr))' to avoid executing output." - _ -> return () -checkSpuriousExpansion _ _ = return () - - -prop_checkDollarBrackets1 = verify checkDollarBrackets "echo $[1+2]" -prop_checkDollarBrackets2 = verifyNot checkDollarBrackets "echo $((1+2))" -checkDollarBrackets _ (T_DollarBracket id _) = - style id 2007 "Use $((..)) instead of deprecated $[..]" -checkDollarBrackets _ _ = return () - -prop_checkSshHereDoc1 = verify checkSshHereDoc "ssh host << foo\necho $PATH\nfoo" -prop_checkSshHereDoc2 = verifyNot checkSshHereDoc "ssh host << 'foo'\necho $PATH\nfoo" -checkSshHereDoc _ (T_Redirecting _ redirs cmd) - | cmd `isCommand` "ssh" = - mapM_ checkHereDoc redirs - where - hasVariables = mkRegex "[`$]" - checkHereDoc (T_FdRedirect _ _ (T_HereDoc id _ Unquoted token tokens)) - | not (all isConstant tokens) = - warn id 2087 $ "Quote '" ++ (e4m token) ++ "' to make here document expansions happen on the server side rather than on the client." - checkHereDoc _ = return () -checkSshHereDoc _ _ = return () - ---- Subshell detection -prop_subshellAssignmentCheck = verifyTree subshellAssignmentCheck "cat foo | while read bar; do a=$bar; done; echo \"$a\"" -prop_subshellAssignmentCheck2 = verifyNotTree subshellAssignmentCheck "while read bar; do a=$bar; done < file; echo \"$a\"" -prop_subshellAssignmentCheck3 = verifyTree subshellAssignmentCheck "( A=foo; ); rm $A" -prop_subshellAssignmentCheck4 = verifyNotTree subshellAssignmentCheck "( A=foo; rm $A; )" -prop_subshellAssignmentCheck5 = verifyTree subshellAssignmentCheck "cat foo | while read cow; do true; done; echo $cow;" -prop_subshellAssignmentCheck6 = verifyTree subshellAssignmentCheck "( export lol=$(ls); ); echo $lol;" -prop_subshellAssignmentCheck6a = verifyTree subshellAssignmentCheck "( typeset -a lol=a; ); echo $lol;" -prop_subshellAssignmentCheck7 = verifyTree subshellAssignmentCheck "cmd | while read foo; do (( n++ )); done; echo \"$n lines\"" -prop_subshellAssignmentCheck8 = verifyTree subshellAssignmentCheck "n=3 & echo $((n++))" -prop_subshellAssignmentCheck9 = verifyTree subshellAssignmentCheck "read n & n=foo$n" -prop_subshellAssignmentCheck10 = verifyTree subshellAssignmentCheck "(( n <<= 3 )) & (( n |= 4 )) &" -prop_subshellAssignmentCheck11 = verifyTree subshellAssignmentCheck "cat /etc/passwd | while read line; do let n=n+1; done\necho $n" -prop_subshellAssignmentCheck12 = verifyTree subshellAssignmentCheck "cat /etc/passwd | while read line; do let ++n; done\necho $n" -prop_subshellAssignmentCheck13 = verifyTree subshellAssignmentCheck "#!/bin/bash\necho foo | read bar; echo $bar" -prop_subshellAssignmentCheck14 = verifyNotTree subshellAssignmentCheck "#!/bin/ksh93\necho foo | read bar; echo $bar" -prop_subshellAssignmentCheck15 = verifyNotTree subshellAssignmentCheck "#!/bin/ksh\ncat foo | while read bar; do a=$bar; done\necho \"$a\"" -prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@" -prop_subshellAssignmentCheck17 = verifyNotTree subshellAssignmentCheck "foo=${ { bar=$(baz); } 2>&1; }; echo $foo $bar" -prop_subshellAssignmentCheck18 = verifyTree subshellAssignmentCheck "( exec {n}>&2; ); echo $n" -prop_subshellAssignmentCheck19 = verifyNotTree subshellAssignmentCheck "#!/bin/bash\nshopt -s lastpipe; echo a | read -r b; echo \"$b\"" -prop_subshellAssignmentCheck20 = verifyTree subshellAssignmentCheck "@test 'foo' { a=1; }\n@test 'bar' { echo $a; }\n" -prop_subshellAssignmentCheck21 = verifyNotTree subshellAssignmentCheck "test1() { echo foo | if [[ $var ]]; then echo $var; fi; }; test2() { echo $var; }" -prop_subshellAssignmentCheck22 = verifyNotTree subshellAssignmentCheck "( [[ -n $foo || -z $bar ]] ); echo $foo $bar" -prop_subshellAssignmentCheck23 = verifyNotTree subshellAssignmentCheck "( export foo ); echo $foo" -subshellAssignmentCheck params t = - let flow = variableFlow params - check = findSubshelled flow [("oops",[])] Map.empty - in execWriter check - - -findSubshelled [] _ _ = return () -findSubshelled (Assignment x@(_, _, str, data_):rest) scopes@((reason,scope):restscope) deadVars = - if isTrueAssignmentSource data_ - then findSubshelled rest ((reason, x:scope):restscope) $ Map.insert str Alive deadVars - else findSubshelled rest scopes deadVars - -findSubshelled (Reference (_, readToken, str):rest) scopes deadVars = do - unless (shouldIgnore str) $ case Map.findWithDefault Alive str deadVars of - Alive -> return () - Dead writeToken reason -> do - info (getId writeToken) 2030 $ "Modification of " ++ str ++ " is local (to subshell caused by "++ reason ++")." - info (getId readToken) 2031 $ str ++ " was modified in a subshell. That change might be lost." - findSubshelled rest scopes deadVars - where - shouldIgnore str = - str `elem` ["@", "*", "IFS"] - -findSubshelled (StackScope (SubshellScope reason):rest) scopes deadVars = - findSubshelled rest ((reason,[]):scopes) deadVars - -findSubshelled (StackScopeEnd:rest) ((reason, scope):oldScopes) deadVars = - findSubshelled rest oldScopes $ - foldl (\m (_, token, var, _) -> - Map.insert var (Dead token reason) m) deadVars scope - - --- FIXME: This is a very strange way of doing it. --- For each variable read/write, run a stateful function that emits --- comments. The comments are collected and returned. -doVariableFlowAnalysis :: - (Token -> Token -> String -> State t [v]) - -> (Token -> Token -> String -> DataType -> State t [v]) - -> t - -> [StackData] - -> [v] - -doVariableFlowAnalysis readFunc writeFunc empty flow = evalState ( - foldM (\list x -> do { l <- doFlow x; return $ l ++ list; }) [] flow - ) empty - where - doFlow (Reference (base, token, name)) = - readFunc base token name - doFlow (Assignment (base, token, name, values)) = - writeFunc base token name values - doFlow _ = return [] - --- Don't suggest quotes if this will instead be autocorrected --- from $foo=bar to foo=bar. This is not pretty but ok. -quotesMayConflictWithSC2281 params t = - case getPath (parentMap params) t of - _ NE.:| T_NormalWord parentId (me:T_Literal _ ('=':_):_) : T_SimpleCommand _ _ (cmd:_) : _ -> - (getId t) == (getId me) && (parentId == getId cmd) - _ -> False - -addDoubleQuotesAround params token = (surroundWith (getId token) params "\"") - -prop_checkSpacefulnessCfg1 = verify checkSpacefulnessCfg "a='cow moo'; echo $a" -prop_checkSpacefulnessCfg2 = verifyNot checkSpacefulnessCfg "a='cow moo'; [[ $a ]]" -prop_checkSpacefulnessCfg3 = verifyNot checkSpacefulnessCfg "a='cow*.mp3'; echo \"$a\"" -prop_checkSpacefulnessCfg4 = verify checkSpacefulnessCfg "for f in *.mp3; do echo $f; done" -prop_checkSpacefulnessCfg4a = verifyNot checkSpacefulnessCfg "foo=3; foo=$(echo $foo)" -prop_checkSpacefulnessCfg5 = verify checkSpacefulnessCfg "a='*'; b=$a; c=lol${b//foo/bar}; echo $c" -prop_checkSpacefulnessCfg6 = verify checkSpacefulnessCfg "a=foo$(lol); echo $a" -prop_checkSpacefulnessCfg7 = verify checkSpacefulnessCfg "a=foo\\ bar; rm $a" -prop_checkSpacefulnessCfg8 = verifyNot checkSpacefulnessCfg "a=foo\\ bar; a=foo; rm $a" -prop_checkSpacefulnessCfg10 = verify checkSpacefulnessCfg "rm $1" -prop_checkSpacefulnessCfg11 = verify checkSpacefulnessCfg "rm ${10//foo/bar}" -prop_checkSpacefulnessCfg12 = verifyNot checkSpacefulnessCfg "(( $1 + 3 ))" -prop_checkSpacefulnessCfg13 = verifyNot checkSpacefulnessCfg "if [[ $2 -gt 14 ]]; then true; fi" -prop_checkSpacefulnessCfg14 = verifyNot checkSpacefulnessCfg "foo=$3 env" -prop_checkSpacefulnessCfg15 = verifyNot checkSpacefulnessCfg "local foo=$1" -prop_checkSpacefulnessCfg16 = verifyNot checkSpacefulnessCfg "declare foo=$1" -prop_checkSpacefulnessCfg17 = verify checkSpacefulnessCfg "echo foo=$1" -prop_checkSpacefulnessCfg18 = verifyNot checkSpacefulnessCfg "$1 --flags" -prop_checkSpacefulnessCfg19 = verify checkSpacefulnessCfg "echo $PWD" -prop_checkSpacefulnessCfg20 = verifyNot checkSpacefulnessCfg "n+='foo bar'" -prop_checkSpacefulnessCfg21 = verifyNot checkSpacefulnessCfg "select foo in $bar; do true; done" -prop_checkSpacefulnessCfg22 = verifyNot checkSpacefulnessCfg "echo $\"$1\"" -prop_checkSpacefulnessCfg23 = verifyNot checkSpacefulnessCfg "a=(1); echo ${a[@]}" -prop_checkSpacefulnessCfg24 = verify checkSpacefulnessCfg "a='a b'; cat <<< $a" -prop_checkSpacefulnessCfg25 = verify checkSpacefulnessCfg "a='s/[0-9]//g'; sed $a" -prop_checkSpacefulnessCfg26 = verify checkSpacefulnessCfg "a='foo bar'; echo {1,2,$a}" -prop_checkSpacefulnessCfg27 = verifyNot checkSpacefulnessCfg "echo ${a:+'foo'}" -prop_checkSpacefulnessCfg28 = verifyNot checkSpacefulnessCfg "exec {n}>&1; echo $n" -prop_checkSpacefulnessCfg29 = verifyNot checkSpacefulnessCfg "n=$(stuff); exec {n}>&-;" -prop_checkSpacefulnessCfg30 = verify checkSpacefulnessCfg "file='foo bar'; echo foo > $file;" -prop_checkSpacefulnessCfg31 = verifyNot checkSpacefulnessCfg "echo \"`echo \\\"$1\\\"`\"" -prop_checkSpacefulnessCfg32 = verifyNot checkSpacefulnessCfg "var=$1; [ -v var ]" -prop_checkSpacefulnessCfg33 = verify checkSpacefulnessCfg "for file; do echo $file; done" -prop_checkSpacefulnessCfg34 = verify checkSpacefulnessCfg "declare foo$n=$1" -prop_checkSpacefulnessCfg35 = verifyNot checkSpacefulnessCfg "echo ${1+\"$1\"}" -prop_checkSpacefulnessCfg36 = verifyNot checkSpacefulnessCfg "arg=$#; echo $arg" -prop_checkSpacefulnessCfg37 = verifyNot checkSpacefulnessCfg "@test 'status' {\n [ $status -eq 0 ]\n}" -prop_checkSpacefulnessCfg37v = verify checkVerboseSpacefulnessCfg "@test 'status' {\n [ $status -eq 0 ]\n}" -prop_checkSpacefulnessCfg38 = verify checkSpacefulnessCfg "a=; echo $a" -prop_checkSpacefulnessCfg39 = verifyNot checkSpacefulnessCfg "a=''\"\"''; b=x$a; echo $b" -prop_checkSpacefulnessCfg40 = verifyNot checkSpacefulnessCfg "a=$((x+1)); echo $a" -prop_checkSpacefulnessCfg41 = verifyNot checkSpacefulnessCfg "exec $1 --flags" -prop_checkSpacefulnessCfg42 = verifyNot checkSpacefulnessCfg "run $1 --flags" -prop_checkSpacefulnessCfg43 = verifyNot checkSpacefulnessCfg "$foo=42" -prop_checkSpacefulnessCfg44 = verify checkSpacefulnessCfg "#!/bin/sh\nexport var=$value" -prop_checkSpacefulnessCfg45 = verifyNot checkSpacefulnessCfg "wait -zzx -p foo; echo $foo" -prop_checkSpacefulnessCfg46 = verifyNot checkSpacefulnessCfg "x=0; (( x += 1 )); echo $x" -prop_checkSpacefulnessCfg47 = verifyNot checkSpacefulnessCfg "x=0; (( x-- )); echo $x" -prop_checkSpacefulnessCfg48 = verifyNot checkSpacefulnessCfg "x=0; (( ++x )); echo $x" -prop_checkSpacefulnessCfg49 = verifyNot checkSpacefulnessCfg "for i in 1 2 3; do echo $i; done" -prop_checkSpacefulnessCfg50 = verify checkSpacefulnessCfg "for i in 1 2 *; do echo $i; done" -prop_checkSpacefulnessCfg51 = verify checkSpacefulnessCfg "x='foo bar'; x && x=1; echo $x" -prop_checkSpacefulnessCfg52 = verifyNot checkSpacefulnessCfg "x=1; if f; then x='foo bar'; exit; fi; echo $x" -prop_checkSpacefulnessCfg53 = verifyNot checkSpacefulnessCfg "s=1; f() { local s='a b'; }; f; echo $s" -prop_checkSpacefulnessCfg54 = verifyNot checkSpacefulnessCfg "s='a b'; f() { s=1; }; f; echo $s" -prop_checkSpacefulnessCfg55 = verify checkSpacefulnessCfg "s='a b'; x && f() { s=1; }; f; echo $s" -prop_checkSpacefulnessCfg56 = verifyNot checkSpacefulnessCfg "s=1; cat <(s='a b'); echo $s" -prop_checkSpacefulnessCfg57 = verifyNot checkSpacefulnessCfg "declare -i s=0; s=$(f); echo $s" -prop_checkSpacefulnessCfg58 = verify checkSpacefulnessCfg "f() { declare -i s; }; f; s=$(var); echo $s" -prop_checkSpacefulnessCfg59 = verifyNot checkSpacefulnessCfg "f() { declare -gi s; }; f; s=$(var); echo $s" -prop_checkSpacefulnessCfg60 = verify checkSpacefulnessCfg "declare -i s; declare +i s; s=$(foo); echo $s" -prop_checkSpacefulnessCfg61 = verify checkSpacefulnessCfg "declare -x X; y=foo$X; echo $y;" -prop_checkSpacefulnessCfg62 = verifyNot checkSpacefulnessCfg "f() { declare -x X; y=foo$X; echo $y; }" -prop_checkSpacefulnessCfg63 = verify checkSpacefulnessCfg "f && declare -i s; s='x + y'; echo $s" -prop_checkSpacefulnessCfg64 = verifyNot checkSpacefulnessCfg "declare -i s; s='x + y'; x=$s; echo $x" -prop_checkSpacefulnessCfg65 = verifyNot checkSpacefulnessCfg "f() { s=$?; echo $s; }; f" -prop_checkSpacefulnessCfg66 = verifyNot checkSpacefulnessCfg "f() { s=$?; echo $s; }" - -checkSpacefulnessCfg = checkSpacefulnessCfg' True -checkVerboseSpacefulnessCfg = checkSpacefulnessCfg' False - -checkSpacefulnessCfg' :: Bool -> (Parameters -> Token -> Writer [TokenComment] ()) -checkSpacefulnessCfg' dirtyPass params token@(T_DollarBraced id _ list) = - when (needsQuoting && (dirtyPass == not isClean)) $ - unless (name `elem` specialVariablesWithoutSpaces || quotesMayConflictWithSC2281 params token) $ - if dirtyPass - then - if isDefaultAssignment (parentMap params) token - then - info (getId token) 2223 - "This default assignment may cause DoS due to globbing. Quote it." - else - infoWithFix id 2086 "Double quote to prevent globbing and word splitting." $ - addDoubleQuotesAround params token - else - styleWithFix id 2248 "Prefer double quoting even when variables don't contain special characters." $ - addDoubleQuotesAround params token - - where - bracedString = concat $ oversimplify list - name = getBracedReference bracedString - parents = parentMap params - needsQuoting = - not (isArrayExpansion token) -- There's another warning for this - && not (isCountingReference token) - && not (isQuoteFree (shellType params) parents token) - && not (isQuotedAlternativeReference token) - && not (usedAsCommandName parents token) - - isClean = fromMaybe False $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id - value <- Map.lookup name $ CF.variablesInScope state - return $ isCleanState value - - isCleanState state = - (all (S.member CFVPInteger) $ CF.variableProperties state) - || CF.spaceStatus (CF.variableValue state) == CF.SpaceStatusClean - - isDefaultAssignment parents token = - let modifier = getBracedModifier bracedString in - any (`isPrefixOf` modifier) ["=", ":="] - && isParamTo parents ":" token - -checkSpacefulnessCfg' _ _ _ = return () - - -prop_CheckVariableBraces1 = verify checkVariableBraces "a='123'; echo $a" -prop_CheckVariableBraces2 = verifyNot checkVariableBraces "a='123'; echo ${a}" -prop_CheckVariableBraces3 = verifyNot checkVariableBraces "#shellcheck disable=SC2016\necho '$a'" -prop_CheckVariableBraces4 = verifyNot checkVariableBraces "echo $* $1" -prop_CheckVariableBraces5 = verifyNot checkVariableBraces "$foo=42" -checkVariableBraces params t@(T_DollarBraced id False l) - | name `notElem` unbracedVariables && not (quotesMayConflictWithSC2281 params t) = - styleWithFix id 2250 - "Prefer putting braces around variable references even when not strictly required." - (fixFor t) - where - name = getBracedReference $ concat $ oversimplify l - fixFor token = fixWith [replaceStart (getId token) params 1 "${" - ,replaceEnd (getId token) params 0 "}"] -checkVariableBraces _ _ = return () - -prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param" -prop_checkQuotesInLiterals1a = verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param" -prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\"" -prop_checkQuotesInLiterals3 =verifyNotTree checkQuotesInLiterals "param=('--foo='); app \"${param[@]}\"" -prop_checkQuotesInLiterals4 = verifyNotTree checkQuotesInLiterals "param=\"don't bother with this one\"; app $param" -prop_checkQuotesInLiterals5 = verifyNotTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; eval app $param" -prop_checkQuotesInLiterals6 = verifyTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm $param\"; $cmd" -prop_checkQuotesInLiterals6a = verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd" -prop_checkQuotesInLiterals7 = verifyTree checkQuotesInLiterals "param='my\\ file'; rm $param" -prop_checkQuotesInLiterals8 = verifyTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm $param" -prop_checkQuotesInLiterals9 = verifyNotTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm ${#param}" -checkQuotesInLiterals params t = - doVariableFlowAnalysis readF writeF Map.empty (variableFlow params) - where - getQuotes name = gets (Map.lookup name) - setQuotes name ref = modify $ Map.insert name ref - deleteQuotes = modify . Map.delete - parents = parentMap params - quoteRegex = mkRegex "\"|([/= ]|^)'|'( |$)|\\\\ " - containsQuotes s = s `matches` quoteRegex - - writeF _ _ name (DataString (SourceFrom values)) = do - quoteMap <- get - let quotedVars = msum $ map (forToken quoteMap) values - case quotedVars of - Nothing -> deleteQuotes name - Just x -> setQuotes name x - return [] - writeF _ _ _ _ = return [] - - forToken map (T_DollarBraced id _ t) = - -- skip getBracedReference here to avoid false positives on PE - Map.lookup (concat . oversimplify $ t) map - forToken quoteMap (T_DoubleQuoted id tokens) = - msum $ map (forToken quoteMap) tokens - forToken quoteMap (T_NormalWord id tokens) = - msum $ map (forToken quoteMap) tokens - forToken _ t = - if containsQuotes (concat $ oversimplify t) - then return $ getId t - else Nothing - - squashesQuotes t = - case t of - T_DollarBraced id _ l -> "#" `isPrefixOf` concat (oversimplify l) - _ -> False - - readF _ expr name = do - assignment <- getQuotes name - return $ case assignment of - Just j - | not (isParamTo parents "eval" expr) - && not (isQuoteFree (shellType params) parents expr) - && not (squashesQuotes expr) - -> [ - makeComment WarningC j 2089 $ - "Quotes/backslashes will be treated literally. " ++ suggestion, - makeComment WarningC (getId expr) 2090 - "Quotes/backslashes in this variable will not be respected." - ] - _ -> [] - suggestion = - if supportsArrays (shellType params) - then "Use an array." - else "Rewrite using set/\"$@\" or functions." - - -prop_checkFunctionsUsedExternally1 = - verifyTree checkFunctionsUsedExternally "foo() { :; }; sudo foo" -prop_checkFunctionsUsedExternally2 = - verifyTree checkFunctionsUsedExternally "alias f='a'; xargs -0 f" -prop_checkFunctionsUsedExternally2b = - verifyNotTree checkFunctionsUsedExternally "alias f='a'; find . -type f" -prop_checkFunctionsUsedExternally2c = - verifyTree checkFunctionsUsedExternally "alias f='a'; find . -type f -exec f +" -prop_checkFunctionsUsedExternally3 = - verifyNotTree checkFunctionsUsedExternally "f() { :; }; echo f" -prop_checkFunctionsUsedExternally4 = - verifyNotTree checkFunctionsUsedExternally "foo() { :; }; run0 \"foo\"" -prop_checkFunctionsUsedExternally5 = - verifyTree checkFunctionsUsedExternally "foo() { :; }; ssh host foo" -prop_checkFunctionsUsedExternally6 = - verifyNotTree checkFunctionsUsedExternally "foo() { :; }; ssh host echo foo" -prop_checkFunctionsUsedExternally7 = - verifyNotTree checkFunctionsUsedExternally "install() { :; }; sudo apt-get install foo" -prop_checkFunctionsUsedExternally8 = - verifyTree checkFunctionsUsedExternally "foo() { :; }; command sudo foo" -prop_checkFunctionsUsedExternally9 = - verifyTree checkFunctionsUsedExternally "foo() { :; }; exec -c doas foo" -checkFunctionsUsedExternally params t = - runNodeAnalysis checkCommand params t - where - checkCommand _ t@(T_SimpleCommand _ _ argv) = - case getCommandNameAndToken False t of - (Just str, t) -> do - let name = basename str - let args = skipOver t argv - let argStrings = map (\x -> (onlyLiteralString x, x)) args - let candidates = getPotentialCommands name argStrings - mapM_ (checkArg name (getId t)) candidates - _ -> return () - checkCommand _ _ = return () - - skipOver t list = drop 1 $ dropWhile (\c -> getId c /= id) $ list - where id = getId t - - -- Try to pick out the argument[s] that may be commands - getPotentialCommands name argAndString = - case name of - "chroot" -> firstNonFlag - "screen" -> firstNonFlag - "sudo" -> firstNonFlag - "doas" -> firstNonFlag - "run0" -> firstNonFlag - "xargs" -> firstNonFlag - "tmux" -> firstNonFlag - "ssh" -> take 1 $ drop 1 $ dropFlags argAndString - "find" -> take 1 $ drop 1 $ - dropWhile (\x -> fst x `notElem` findExecFlags) argAndString - _ -> [] - where - firstNonFlag = take 1 $ dropFlags argAndString - findExecFlags = ["-exec", "-execdir", "-ok"] - dropFlags = dropWhile (\x -> "-" `isPrefixOf` fst x) - - functionsAndAliases = Map.union (functions t) (aliases t) - - patternContext id = - case posLine . fst <$> Map.lookup id (tokenPositions params) of - Just l -> " on line " <> show l <> "." - _ -> "." - - checkArg cmd cmdId (_, arg) = sequence_ $ do - literalArg <- getUnquotedLiteral arg -- only consider unquoted literals - definitionId <- Map.lookup literalArg functionsAndAliases - return $ do - warn (getId arg) 2033 - "Shell functions can't be passed to external commands. Use separate script or sh -c." - info definitionId 2032 $ - "This function can't be invoked via " ++ cmd ++ patternContext cmdId - -prop_checkUnused0 = verifyNotTree checkUnusedAssignments "var=foo; echo $var" -prop_checkUnused1 = verifyTree checkUnusedAssignments "var=foo; echo $bar" -prop_checkUnused2 = verifyNotTree checkUnusedAssignments "var=foo; export var;" -prop_checkUnused3 = verifyTree checkUnusedAssignments "for f in *; do echo '$f'; done" -prop_checkUnused4 = verifyTree checkUnusedAssignments "local i=0" -prop_checkUnused5 = verifyNotTree checkUnusedAssignments "read lol; echo $lol" -prop_checkUnused6 = verifyNotTree checkUnusedAssignments "var=4; (( var++ ))" -prop_checkUnused7 = verifyNotTree checkUnusedAssignments "var=2; $((var))" -prop_checkUnused8 = verifyTree checkUnusedAssignments "var=2; var=3;" -prop_checkUnused9 = verifyNotTree checkUnusedAssignments "read ''" -prop_checkUnused10 = verifyNotTree checkUnusedAssignments "read -p 'test: '" -prop_checkUnused11 = verifyNotTree checkUnusedAssignments "bar=5; export foo[$bar]=3" -prop_checkUnused12 = verifyNotTree checkUnusedAssignments "read foo; echo ${!foo}" -prop_checkUnused13 = verifyNotTree checkUnusedAssignments "x=(1); (( x[0] ))" -prop_checkUnused14 = verifyNotTree checkUnusedAssignments "x=(1); n=0; echo ${x[n]}" -prop_checkUnused15 = verifyNotTree checkUnusedAssignments "x=(1); n=0; (( x[n] ))" -prop_checkUnused16 = verifyNotTree checkUnusedAssignments "foo=5; declare -x foo" -prop_checkUnused16b = verifyNotTree checkUnusedAssignments "f() { local -x foo; foo=42; bar; }; f" -prop_checkUnused17 = verifyNotTree checkUnusedAssignments "read -i 'foo' -e -p 'Input: ' bar; $bar;" -prop_checkUnused18 = verifyNotTree checkUnusedAssignments "a=1; arr=( [$a]=42 ); echo \"${arr[@]}\"" -prop_checkUnused19 = verifyNotTree checkUnusedAssignments "a=1; let b=a+1; echo $b" -prop_checkUnused20 = verifyNotTree checkUnusedAssignments "a=1; PS1='$a'" -prop_checkUnused21 = verifyNotTree checkUnusedAssignments "a=1; trap 'echo $a' INT" -prop_checkUnused22 = verifyNotTree checkUnusedAssignments "a=1; [ -v a ]" -prop_checkUnused23 = verifyNotTree checkUnusedAssignments "a=1; [ -R a ]" -prop_checkUnused24 = verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}" -prop_checkUnused25 = verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}" -prop_checkUnused26 = verifyNotTree checkUnusedAssignments "declare -F foo" -prop_checkUnused27 = verifyTree checkUnusedAssignments "var=3; [ var -eq 3 ]" -prop_checkUnused28 = verifyNotTree checkUnusedAssignments "var=3; [[ var -eq 3 ]]" -prop_checkUnused29 = verifyNotTree checkUnusedAssignments "var=(a b); declare -p var" -prop_checkUnused30 = verifyTree checkUnusedAssignments "let a=1" -prop_checkUnused31 = verifyTree checkUnusedAssignments "let 'a=1'" -prop_checkUnused32 = verifyTree checkUnusedAssignments "let a=b=c; echo $a" -prop_checkUnused33 = verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]" -prop_checkUnused34 = verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t" -prop_checkUnused35 = verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}" -prop_checkUnused36 = verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi" -prop_checkUnused37 = verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-" -prop_checkUnused38 = verifyTree checkUnusedAssignments "(( a=42 ))" -prop_checkUnused39 = verifyNotTree checkUnusedAssignments "declare -x -f foo" -prop_checkUnused40 = verifyNotTree checkUnusedAssignments "arr=(1 2); num=2; echo \"${arr[@]:num}\"" -prop_checkUnused41 = verifyNotTree checkUnusedAssignments "@test 'foo' {\ntrue\n}\n" -prop_checkUnused42 = verifyNotTree checkUnusedAssignments "DEFINE_string foo '' ''; echo \"${FLAGS_foo}\"" -prop_checkUnused43 = verifyTree checkUnusedAssignments "DEFINE_string foo '' ''" -prop_checkUnused44 = verifyNotTree checkUnusedAssignments "DEFINE_string \"foo$ibar\" x y" -prop_checkUnused45 = verifyTree checkUnusedAssignments "readonly foo=bar" -prop_checkUnused46 = verifyTree checkUnusedAssignments "readonly foo=(bar)" -prop_checkUnused47 = verifyNotTree checkUnusedAssignments "a=1; alias hello='echo $a'" -prop_checkUnused48 = verifyNotTree checkUnusedAssignments "_a=1" -prop_checkUnused49 = verifyNotTree checkUnusedAssignments "declare -A array; key=a; [[ -v array[$key] ]]" -prop_checkUnused50 = verifyNotTree checkUnusedAssignments "foofunc() { :; }; typeset -fx foofunc" -prop_checkUnused51 = verifyTree checkUnusedAssignments "x[y[z=1]]=1; echo ${x[@]}" - -checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) - where - flow = variableFlow params - references = Map.union (Map.fromList [(stripSuffix name, ()) | Reference (base, token, name) <- flow]) defaultMap - - assignments = Map.fromList [(name, token) | Assignment (_, token, name, _) <- flow, isVariableName name] - - unused = Map.assocs $ Map.difference assignments references - - warnFor (name, token) = - unless ("_" `isPrefixOf` name) $ - warn (getId token) 2034 $ - name ++ " appears unused. Verify use (or export if used externally)." - - stripSuffix = takeWhile isVariableChar - defaultMap = Map.fromList $ zip internalVariables $ repeat () - -prop_checkUnassignedReferences1 = verifyTree checkUnassignedReferences "echo $foo" -prop_checkUnassignedReferences2 = verifyNotTree checkUnassignedReferences "foo=hello; echo $foo" -prop_checkUnassignedReferences3 = verifyTree checkUnassignedReferences "MY_VALUE=3; echo $MYVALUE" -prop_checkUnassignedReferences4 = verifyNotTree checkUnassignedReferences "RANDOM2=foo; echo $RANDOM" -prop_checkUnassignedReferences5 = verifyNotTree checkUnassignedReferences "declare -A foo=([bar]=baz); echo ${foo[bar]}" -prop_checkUnassignedReferences6 = verifyNotTree checkUnassignedReferences "foo=..; echo ${foo-bar}" -prop_checkUnassignedReferences7 = verifyNotTree checkUnassignedReferences "getopts ':h' foo; echo $foo" -prop_checkUnassignedReferences8 = verifyNotTree checkUnassignedReferences "let 'foo = 1'; echo $foo" -prop_checkUnassignedReferences9 = verifyNotTree checkUnassignedReferences "echo ${foo-bar}" -prop_checkUnassignedReferences10 = verifyNotTree checkUnassignedReferences "echo ${foo:?}" -prop_checkUnassignedReferences11 = verifyNotTree checkUnassignedReferences "declare -A foo; echo \"${foo[@]}\"" -prop_checkUnassignedReferences12 = verifyNotTree checkUnassignedReferences "typeset -a foo; echo \"${foo[@]}\"" -prop_checkUnassignedReferences13 = verifyNotTree checkUnassignedReferences "f() { local foo; echo $foo; }" -prop_checkUnassignedReferences14 = verifyNotTree checkUnassignedReferences "foo=; echo $foo" -prop_checkUnassignedReferences15 = verifyNotTree checkUnassignedReferences "f() { true; }; export -f f" -prop_checkUnassignedReferences16 = verifyNotTree checkUnassignedReferences "declare -A foo=( [a b]=bar ); echo ${foo[a b]}" -prop_checkUnassignedReferences17 = verifyNotTree checkUnassignedReferences "USERS=foo; echo $USER" -prop_checkUnassignedReferences18 = verifyNotTree checkUnassignedReferences "FOOBAR=42; export FOOBAR=" -prop_checkUnassignedReferences19 = verifyNotTree checkUnassignedReferences "readonly foo=bar; echo $foo" -prop_checkUnassignedReferences20 = verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo" -prop_checkUnassignedReferences21 = verifyTree checkUnassignedReferences "echo ${#foo}" -prop_checkUnassignedReferences22 = verifyNotTree checkUnassignedReferences "echo ${!os*}" -prop_checkUnassignedReferences23 = verifyTree checkUnassignedReferences "declare -a foo; foo[bar]=42;" -prop_checkUnassignedReferences24 = verifyNotTree checkUnassignedReferences "declare -A foo; foo[bar]=42;" -prop_checkUnassignedReferences25 = verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;" -prop_checkUnassignedReferences26 = verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b" -prop_checkUnassignedReferences27 = verifyNotTree checkUnassignedReferences ": ${foo:=bar}" -prop_checkUnassignedReferences28 = verifyNotTree checkUnassignedReferences "#!/bin/ksh\necho \"${.sh.version}\"\n" -prop_checkUnassignedReferences29 = verifyNotTree checkUnassignedReferences "if [[ -v foo ]]; then echo $foo; fi" -prop_checkUnassignedReferences30 = verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi" -prop_checkUnassignedReferences31 = verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi" -prop_checkUnassignedReferences32 = verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi" -prop_checkUnassignedReferences33 = verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }" -prop_checkUnassignedReferences34 = verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))" -prop_checkUnassignedReferences35 = verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}" -prop_checkUnassignedReferences36 = verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\"" -prop_checkUnassignedReferences37 = verifyNotTree checkUnassignedReferences "var=howdy; printf -v 'array[0]' %s \"$var\"; printf %s \"${array[0]}\";" -prop_checkUnassignedReferences38 = verifyTree (checkUnassignedReferences' True) "echo $VAR" -prop_checkUnassignedReferences39 = verifyNotTree checkUnassignedReferences "builtin export var=4; echo $var" -prop_checkUnassignedReferences40 = verifyNotTree checkUnassignedReferences ": ${foo=bar}" -prop_checkUnassignedReferences41 = verifyNotTree checkUnassignedReferences "mapfile -t files 123; echo \"${files[@]}\"" -prop_checkUnassignedReferences42 = verifyNotTree checkUnassignedReferences "mapfile files -t; echo \"${files[@]}\"" -prop_checkUnassignedReferences43 = verifyNotTree checkUnassignedReferences "mapfile --future files; echo \"${files[@]}\"" -prop_checkUnassignedReferences_minusNPlain = verifyNotTree checkUnassignedReferences "if [ -n \"$x\" ]; then echo $x; fi" -prop_checkUnassignedReferences_minusZPlain = verifyNotTree checkUnassignedReferences "if [ -z \"$x\" ]; then echo \"\"; fi" -prop_checkUnassignedReferences_minusNBraced = verifyNotTree checkUnassignedReferences "if [ -n \"${x}\" ]; then echo $x; fi" -prop_checkUnassignedReferences_minusZBraced = verifyNotTree checkUnassignedReferences "if [ -z \"${x}\" ]; then echo \"\"; fi" -prop_checkUnassignedReferences_minusNDefault = verifyNotTree checkUnassignedReferences "if [ -n \"${x:-}\" ]; then echo $x; fi" -prop_checkUnassignedReferences_minusZDefault = verifyNotTree checkUnassignedReferences "if [ -z \"${x:-}\" ]; then echo \"\"; fi" -prop_checkUnassignedReferences50 = verifyNotTree checkUnassignedReferences "echo ${foo:+bar}" -prop_checkUnassignedReferences51 = verifyNotTree checkUnassignedReferences "echo ${foo:+$foo}" -prop_checkUnassignedReferences52 = verifyNotTree checkUnassignedReferences "wait -p pid; echo $pid" -prop_checkUnassignedReferences53 = verifyTree checkUnassignedReferences "x=($foo)" - -checkUnassignedReferences = checkUnassignedReferences' False -checkUnassignedReferences' includeGlobals params t = warnings - where - (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty) - defaultAssigned = Map.fromList $ map (\a -> (a, ())) $ filter (not . null) internalVariables - - tally (Assignment (_, _, name, _)) = - modify (\(read, written) -> (read, Map.insert name () written)) - tally (Reference (_, place, name)) = - modify (\(read, written) -> (Map.insertWith (const id) name place read, written)) - tally _ = return () - - unassigned = Map.toList $ Map.difference (Map.difference readMap writeMap) defaultAssigned - writtenVars = filter isVariableName $ Map.keys writeMap - - getBestMatch var = do - (match, score) <- listToMaybe best - guard $ goodMatch var match score - return match - where - matches = map (\x -> (x, match var x)) writtenVars - best = sortBy (comparing snd) matches - goodMatch var match score = - let l = length match in - l > 3 && score <= 1 - || l > 7 && score <= 2 - - isLocal = any isLower - - warningForGlobals var place = do - match <- getBestMatch var - return $ info (getId place) 2153 $ - "Possible misspelling: " ++ var ++ " may not be assigned. Did you mean " ++ match ++ "?" - - warningForLocals var place = - return $ warn (getId place) 2154 $ - var ++ " is referenced but not assigned" ++ optionalTip ++ "." - where - optionalTip = - if var `elem` commonCommands - then " (for output from commands, use \"$(" ++ var ++ " ..." ++ ")\" )" - else fromMaybe "" $ do - match <- getBestMatch var - return $ " (did you mean '" ++ match ++ "'?)" - - warningFor (var, place) = do - guard $ isVariableName var - guard . not $ isException var place || isGuarded place - (if includeGlobals || isLocal var - then warningForLocals - else warningForGlobals) var place - - warnings = execWriter . sequence $ mapMaybe warningFor unassigned - - -- ${foo[bar baz]} may not be referencing bar/baz. Just skip these. - -- We can also have ${foo:+$foo} should be treated like [[ -n $foo ]] && echo $foo - isException var t = any shouldExclude $ getPath (parentMap params) t - where - shouldExclude t = - case t of - (T_DollarBraced _ _ l) -> - let str = concat $ oversimplify l - ref = getBracedReference str - mod = getBracedModifier str - in - -- Either we're used as an array index like ${arr[here]} - ref /= var || - -- or the reference is guarded by a parent, ${here:+foo$here} - "+" `isPrefixOf` mod || ":+" `isPrefixOf` mod - _ -> False - - isGuarded (T_DollarBraced _ _ v) = - rest `matches` guardRegex - where - name = concat $ oversimplify v - rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name - isGuarded _ = False - -- :? or :- with optional array index and colon - guardRegex = mkRegex "^(\\[.*\\])?:?[-?]" - - match var candidate = - if var /= candidate && map toLower var == map toLower candidate - then 1 - else dist var candidate - - -prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt" -prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*" -prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt" -prop_checkGlobsAsOptions4 = verifyNot checkGlobsAsOptions "*.txt" -prop_checkGlobsAsOptions5 = verifyNot checkGlobsAsOptions "echo 'Files:' *.txt" -prop_checkGlobsAsOptions6 = verifyNot checkGlobsAsOptions "printf '%s\\n' *" -checkGlobsAsOptions _ cmd@(T_SimpleCommand _ _ args) = - unless ((fromMaybe "" $ getCommandBasename cmd) `elem` ["echo", "printf"]) $ - mapM_ check $ takeWhile (not . isEndOfArgs) (drop 1 args) - where - check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" = - info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options." - check _ = return () - - isEndOfArgs t = - case concat $ oversimplify t of - "--" -> True - ":::" -> True - "::::" -> True - _ -> False - -checkGlobsAsOptions _ _ = return () - - -prop_checkWhileReadPitfalls1 = verify checkWhileReadPitfalls "while read foo; do ssh $foo uptime; done < file" -prop_checkWhileReadPitfalls2 = verifyNot checkWhileReadPitfalls "while read -u 3 foo; do ssh $foo uptime; done 3< file" -prop_checkWhileReadPitfalls3 = verifyNot checkWhileReadPitfalls "while true; do ssh host uptime; done" -prop_checkWhileReadPitfalls4 = verifyNot checkWhileReadPitfalls "while read foo; do ssh $foo hostname < /dev/null; done" -prop_checkWhileReadPitfalls5 = verifyNot checkWhileReadPitfalls "while read foo; do echo ls | ssh $foo; done" -prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo" -prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file" -prop_checkWhileReadPitfalls8 = verifyNot checkWhileReadPitfalls "while read foo; do ssh -n $foo uptime; done < file" -prop_checkWhileReadPitfalls9 = verify checkWhileReadPitfalls "while read foo; do ffmpeg -i foo.mkv bar.mkv -an; done" -prop_checkWhileReadPitfalls10 = verify checkWhileReadPitfalls "while read foo; do mplayer foo.ogv > file; done" -prop_checkWhileReadPitfalls11 = verifyNot checkWhileReadPitfalls "while read foo; do mplayer foo.ogv <<< q; done" -prop_checkWhileReadPitfalls12 = verifyNot checkWhileReadPitfalls "while read foo\ndo\nmplayer foo.ogv << EOF\nq\nEOF\ndone" -prop_checkWhileReadPitfalls13 = verify checkWhileReadPitfalls "while read foo; do x=$(ssh host cmd); done" -prop_checkWhileReadPitfalls14 = verify checkWhileReadPitfalls "while read foo; do echo $(ssh host cmd) < /dev/null; done" -prop_checkWhileReadPitfalls15 = verify checkWhileReadPitfalls "while read foo; do ssh $foo cmd & done" - -checkWhileReadPitfalls params (T_WhileExpression id [command] contents) - | isStdinReadCommand command = - mapM_ checkMuncher contents - where - -- Map of munching commands to a function that checks if the flags should exclude it - munchers = Map.fromList [ - ("ssh", (hasFlag, addFlag, "-n")), - ("ffmpeg", (hasArgument, addFlag, "-nostdin")), - ("mplayer", (hasArgument, addFlag, "-noconsolecontrols")), - ("HandBrakeCLI", (\_ _ -> False, addRedirect, "< /dev/null")) - ] - -- Use flag parsing, e.g. "-an" -> "a", "n" - hasFlag ('-':flag) = elem flag . map snd . getAllFlags - -- Simple string match, e.g. "-an" -> "-an" - hasArgument arg = elem arg . mapMaybe getLiteralString . fromJust . getCommandArgv - addFlag string cmd = fixWith [replaceEnd (getId $ getCommandTokenOrThis cmd) params 0 (' ':string)] - addRedirect string cmd = fixWith [replaceEnd (getId cmd) params 0 (' ':string)] - - isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) = - let plaintext = oversimplify cmd - in headOrDefault "" plaintext == "read" - && ("-u" `notElem` plaintext) - && not (any stdinRedirect redirs) - isStdinReadCommand _ = False - - checkMuncher :: Token -> Writer [TokenComment] () - checkMuncher (T_Pipeline _ _ (T_Redirecting _ redirs cmd:_)) = do - -- Check command substitutions regardless of the command - case cmd of - T_SimpleCommand _ vars args -> - mapM_ checkMuncher $ concat $ concatMap getCommandSequences $ concatMap getWords $ vars ++ args - _ -> return () - - unless (any stdinRedirect redirs) $ do - -- Recurse into ifs/loops/groups/etc if this doesn't redirect - mapM_ checkMuncher $ concat $ getCommandSequences cmd - - -- Check the actual command - sequence_ $ do - name <- getCommandBasename cmd - (check, fix, flag) <- Map.lookup name munchers - guard $ not (check flag cmd) - - return $ do - info id 2095 $ - name ++ " may swallow stdin, preventing this loop from working properly." - warnWithFix (getId cmd) 2095 - ("Use " ++ name ++ " " ++ flag ++ " to prevent " ++ name ++ " from swallowing stdin.") - (fix flag cmd) - checkMuncher (T_Backgrounded _ t) = checkMuncher t - checkMuncher _ = return () - - stdinRedirect (T_FdRedirect _ fd op) - | fd == "0" = True - | fd == "" = - case op of - T_IoFile _ (T_Less _) _ -> True - T_IoDuplicate _ (T_LESSAND _) _ -> True - T_HereString _ _ -> True - T_HereDoc {} -> True - _ -> False - stdinRedirect _ = False - - getWords t = - case t of - T_Assignment _ _ _ _ x -> getWordParts x - _ -> getWordParts t -checkWhileReadPitfalls _ _ = return () - - -prop_checkPrefixAssign1 = verify checkPrefixAssignmentReference "var=foo echo $var" -prop_checkPrefixAssign2 = verifyNot checkPrefixAssignmentReference "var=$(echo $var) cmd" -checkPrefixAssignmentReference params t@(T_DollarBraced id _ value) = - check path - where - name = getBracedReference $ concat $ oversimplify value - path = NE.toList $ getPath (parentMap params) t - idPath = map getId path - - check [] = return () - check (t:rest) = - case t of - T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars - _ -> check rest - checkVar (T_Assignment aId mode aName [] value) | - aName == name && (aId `notElem` idPath) = do - warn aId 2097 "This assignment is only seen by the forked process." - warn id 2098 "This expansion will not see the mentioned assignment." - checkVar _ = return () - -checkPrefixAssignmentReference _ _ = return () - -prop_checkCharRangeGlob1 = verify checkCharRangeGlob "ls *[:digit:].jpg" -prop_checkCharRangeGlob2 = verifyNot checkCharRangeGlob "ls *[[:digit:]].jpg" -prop_checkCharRangeGlob3 = verify checkCharRangeGlob "ls [10-15]" -prop_checkCharRangeGlob4 = verifyNot checkCharRangeGlob "ls [a-zA-Z]" -prop_checkCharRangeGlob5 = verifyNot checkCharRangeGlob "tr -d [aa]" -- tr has 2060 -prop_checkCharRangeGlob6 = verifyNot checkCharRangeGlob "[[ $x == [!!]* ]]" -prop_checkCharRangeGlob7 = verifyNot checkCharRangeGlob "[[ -v arr[keykey] ]]" -prop_checkCharRangeGlob8 = verifyNot checkCharRangeGlob "[[ arr[keykey] -gt 1 ]]" -prop_checkCharRangeGlob9 = verifyNot checkCharRangeGlob "read arr[keykey]" -- tr has 2313 -checkCharRangeGlob p t@(T_Glob id str) | - isCharClass str && not isIgnoredCommand && not (isDereferenced t) = - if ":" `isPrefixOf` contents - && ":" `isSuffixOf` contents - && contents /= ":" - then warn id 2101 "Named class needs outer [], e.g. [[:digit:]]." - else - when ('[' `notElem` contents && hasDupes) $ - info id 2102 "Ranges can only match single chars (mentioned due to duplicates)." - where - isCharClass str = "[" `isPrefixOf` str && "]" `isSuffixOf` str - contents = dropNegation . drop 1 . take (length str - 1) $ str - hasDupes = any ((>1) . length) . group . sort . filter (/= '-') $ contents - dropNegation s = - case s of - '!':rest -> rest - '^':rest -> rest - x -> x - - isIgnoredCommand = fromMaybe False $ do - cmd <- getClosestCommand (parentMap p) t - return $ isCommandMatch cmd (`elem` ["tr", "read"]) - - -- Check if this is a dereferencing context like [[ -v array[operandhere] ]] - isDereferenced = fromMaybe False . msum . NE.map isDereferencingOp . getPath (parentMap p) - isDereferencingOp t = - case t of - TC_Binary _ DoubleBracket str _ _ -> return $ isDereferencingBinaryOp str - TC_Unary _ _ str _ -> return $ str == "-v" - T_SimpleCommand {} -> return False - _ -> Nothing -checkCharRangeGlob _ _ = return () - - - -prop_checkCdAndBack1 = verify checkCdAndBack "for f in *; do cd $f; git pull; cd ..; done" -prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done" -prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done" -prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -" -prop_checkCdAndBack5 = verifyNot checkCdAndBack "cd ..; foo; cd .." -prop_checkCdAndBack6 = verify checkCdAndBack "for dir in */; do cd \"$dir\"; some_cmd; cd ..; done" -prop_checkCdAndBack7 = verifyNot checkCdAndBack "set -e; for dir in */; do cd \"$dir\"; some_cmd; cd ..; done" -prop_checkCdAndBack8 = verifyNot checkCdAndBack "cd tmp\nfoo\n# shellcheck disable=SC2103\ncd ..\n" -checkCdAndBack params t = - unless (hasSetE params) $ mapM_ doList $ getCommandSequences t - where - isCdRevert t = - case oversimplify t of - [_, p] -> p `elem` ["..", "-"] - _ -> False - - getCandidate (T_Annotation _ _ x) = getCandidate x - getCandidate (T_Pipeline id _ [x]) | x `isCommand` "cd" = return x - getCandidate _ = Nothing - - findCdPair list = - case list of - (a:b:rest) -> - if isCdRevert b && not (isCdRevert a) - then return $ getId b - else findCdPair (b:rest) - _ -> Nothing - - doList list = sequence_ $ do - cd <- findCdPair $ mapMaybe getCandidate list - return $ info cd 2103 "Use a ( subshell ) to avoid having to cd back." - -prop_checkLoopKeywordScope1 = verify checkLoopKeywordScope "continue 2" -prop_checkLoopKeywordScope2 = verify checkLoopKeywordScope "for f; do ( break; ); done" -prop_checkLoopKeywordScope3 = verify checkLoopKeywordScope "if true; then continue; fi" -prop_checkLoopKeywordScope4 = verifyNot checkLoopKeywordScope "while true; do break; done" -prop_checkLoopKeywordScope5 = verify checkLoopKeywordScope "if true; then break; fi" -prop_checkLoopKeywordScope6 = verify checkLoopKeywordScope "while true; do true | { break; }; done" -prop_checkLoopKeywordScope7 = verifyNot checkLoopKeywordScope "#!/bin/ksh\nwhile true; do true | { break; }; done" -checkLoopKeywordScope params t | - Just name <- getCommandName t, name `elem` ["continue", "break"] = - if any isLoop path - then case map subshellType $ filter (not . isFunction) path of - Just str:_ -> warn (getId t) 2106 $ - "This only exits the subshell caused by the " ++ str ++ "." - _ -> return () - else case path of - -- breaking at a source/function invocation is an abomination. Let's ignore it. - h:_ | isFunction h -> err (getId t) 2104 $ "In functions, use return instead of " ++ name ++ "." - _ -> err (getId t) 2105 $ name ++ " is only valid in loops." - where - path = let p = getPath (parentMap params) t in NE.filter relevant p - subshellType t = case leadType params t of - NoneScope -> Nothing - SubshellScope str -> return str - relevant t = isLoop t || isFunction t || isJust (subshellType t) -checkLoopKeywordScope _ _ = return () - - -prop_checkFunctionDeclarations1 = verify checkFunctionDeclarations "#!/bin/ksh\nfunction foo() { command foo --lol \"$@\"; }" -prop_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }" -prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }" -checkFunctionDeclarations params - (T_Function id (FunctionKeyword hasKeyword) (FunctionParentheses hasParens) _ _) = - case shellType params of - Bash -> return () - Ksh -> - when (hasKeyword && hasParens) $ - err id 2111 "ksh does not allow 'function' keyword and '()' at the same time." - Dash -> forSh - BusyboxSh -> forSh - Sh -> forSh - - where - forSh = do - when (hasKeyword && hasParens) $ - warn id 2112 "'function' keyword is non-standard. Delete it." - when (hasKeyword && not hasParens) $ - warn id 2113 "'function' keyword is non-standard. Use 'foo()' instead of 'function foo'." -checkFunctionDeclarations _ _ = return () - - - -prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar" -prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar" -checkStderrPipe params = - case shellType params of - Ksh -> match - _ -> const $ return () - where - match (T_Pipe id "|&") = - err id 2118 "Ksh does not support |&. Use 2>&1 |." - match _ = return () - -prop_checkUnpassedInFunctions1 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo" -prop_checkUnpassedInFunctions2 = verifyNotTree checkUnpassedInFunctions "foo() { echo $1; };" -prop_checkUnpassedInFunctions3 = verifyNotTree checkUnpassedInFunctions "foo() { echo $lol; }; foo" -prop_checkUnpassedInFunctions4 = verifyNotTree checkUnpassedInFunctions "foo() { echo $0; }; foo" -prop_checkUnpassedInFunctions5 = verifyNotTree checkUnpassedInFunctions "foo() { echo $1; }; foo 'lol'; foo" -prop_checkUnpassedInFunctions6 = verifyNotTree checkUnpassedInFunctions "foo() { set -- *; echo $1; }; foo" -prop_checkUnpassedInFunctions7 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo; foo;" -prop_checkUnpassedInFunctions8 = verifyNotTree checkUnpassedInFunctions "foo() { echo $((1)); }; foo;" -prop_checkUnpassedInFunctions9 = verifyNotTree checkUnpassedInFunctions "foo() { echo $(($b)); }; foo;" -prop_checkUnpassedInFunctions10 = verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;" -prop_checkUnpassedInFunctions11 = verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;" -prop_checkUnpassedInFunctions12 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;" -prop_checkUnpassedInFunctions13 = verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n" -prop_checkUnpassedInFunctions14 = verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo" -prop_checkUnpassedInFunctions15 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1-x}; }; foo" -prop_checkUnpassedInFunctions16 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1:-x}; }; foo" -prop_checkUnpassedInFunctions17 = verifyNotTree checkUnpassedInFunctions "foo() { mycommand ${1+--verbose}; }; foo" -prop_checkUnpassedInFunctions18 = verifyNotTree checkUnpassedInFunctions "foo() { if mycheck; then foo ${1?Missing}; fi; }; foo" -checkUnpassedInFunctions params root = - execWriter $ mapM_ warnForGroup referenceGroups - where - functionMap :: Map.Map String Token - functionMap = Map.fromList $ execWriter $ doAnalysis (tell . maybeToList . findFunction) root - - findFunction t@(T_Function id _ _ name body) - | any (isPositionalReference t) flow && not (any isPositionalAssignment flow) - = return (name,t) - where flow = getVariableFlow params body - findFunction _ = Nothing - - isPositionalAssignment x = - case x of - Assignment (_, _, str, _) -> isPositional str - _ -> False - - isPositionalReference function x = - case x of - Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function && not (hasDefaultValue t) - _ -> False - - isDirectChildOf child parent = fromMaybe False $ do - function <- find (\x -> case x of - T_Function {} -> True - T_Script {} -> True -- for sourced files - _ -> False) $ - getPath (parentMap params) child - return $ getId parent == getId function - - referenceList :: [(String, Bool, Token)] - referenceList = execWriter $ - doAnalysis (sequence_ . checkCommand) root - - checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ()) - checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do - str <- getLiteralString cmd - guard $ Map.member str functionMap - return $ tell [(str, null args, t)] - checkCommand _ = Nothing - - isPositional str = str == "*" || str == "@" || str == "#" - || (all isDigit str && str /= "0" && str /= "") - - -- True if t is a variable that specifies a default value, - -- such as ${1-x} or ${1:-x}. - hasDefaultValue t = - case t of - T_DollarBraced _ True l -> - let str = concat $ oversimplify l - in isDefaultValueModifier $ getBracedModifier str - _ -> False - - isDefaultValueModifier str = - case str of - ':':c:_ -> c `elem` handlesDefault - c:_ -> c `elem` handlesDefault - _ -> False - where handlesDefault = "-+?" - - isArgumentless (_, b, _) = b - referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList - updateWith x@(name, _, _) = Map.insertWith (++) name [x] - - warnForGroup group = - -- Allow ignoring SC2120 on the function to ignore all calls - when (all isArgumentless group && not ignoring) $ do - mapM_ suggestParams group - warnForDeclaration func name - where (name, func) = getFunction group - ignoring = shouldIgnoreCode params 2120 func - - suggestParams (name, _, thing) = - info (getId thing) 2119 $ - "Use " ++ (e4m name) ++ " \"$@\" if function's $1 should mean script's $1." - warnForDeclaration func name = - warn (getId func) 2120 $ - name ++ " references arguments, but none are ever passed." - - getFunction ((name, _, _):_) = - (name, functionMap Map.! name) - - -prop_checkOverridingPath1 = verify checkOverridingPath "PATH=\"$var/$foo\"" -prop_checkOverridingPath2 = verify checkOverridingPath "PATH=\"mydir\"" -prop_checkOverridingPath3 = verify checkOverridingPath "PATH=/cow/foo" -prop_checkOverridingPath4 = verifyNot checkOverridingPath "PATH=/cow/foo/bin" -prop_checkOverridingPath5 = verifyNot checkOverridingPath "PATH='/bin:/sbin'" -prop_checkOverridingPath6 = verifyNot checkOverridingPath "PATH=\"$var/$foo\" cmd" -prop_checkOverridingPath7 = verifyNot checkOverridingPath "PATH=$OLDPATH" -prop_checkOverridingPath8 = verifyNot checkOverridingPath "PATH=$PATH:/stuff" -checkOverridingPath _ (T_SimpleCommand _ vars []) = - mapM_ checkVar vars - where - checkVar (T_Assignment id Assign "PATH" [] word) - | not $ any (`isInfixOf` string) ["/bin", "/sbin" ] = do - when ('/' `elem` string && ':' `notElem` string) $ notify id - when (isLiteral word && ':' `notElem` string && '/' `notElem` string) $ notify id - where string = concat $ oversimplify word - checkVar _ = return () - notify id = warn id 2123 "PATH is the shell search path. Use another name." -checkOverridingPath _ _ = return () - -prop_checkTildeInPath1 = verify checkTildeInPath "PATH=\"$PATH:~/bin\"" -prop_checkTildeInPath2 = verify checkTildeInPath "PATH='~foo/bin'" -prop_checkTildeInPath3 = verifyNot checkTildeInPath "PATH=~/bin" -checkTildeInPath _ (T_SimpleCommand _ vars _) = - mapM_ checkVar vars - where - checkVar (T_Assignment id Assign "PATH" [] (T_NormalWord _ parts)) - | any (\x -> isQuoted x && hasTilde x) parts = - warn id 2147 "Literal tilde in PATH works poorly across programs." - checkVar _ = return () - - hasTilde t = '~' `elem` onlyLiteralString t - isQuoted T_DoubleQuoted {} = True - isQuoted T_SingleQuoted {} = True - isQuoted _ = False -checkTildeInPath _ _ = return () - -prop_checkUnsupported3 = verify checkUnsupported "#!/bin/sh\ncase foo in bar) baz ;& esac" -prop_checkUnsupported4 = verify checkUnsupported "#!/bin/ksh\ncase foo in bar) baz ;;& esac" -prop_checkUnsupported5 = verifyNot checkUnsupported "#!/bin/bash\necho \"${ ls; }\"" -prop_checkUnsupported6 = verify checkUnsupported "#!/bin/ash\necho \"${ ls; }\"" -checkUnsupported params t = - unless (null support || (shellType params `elem` support)) $ - report name - where - (name, support) = shellSupport t - report s = err (getId t) 2127 $ - "To use " ++ s ++ ", specify #!/usr/bin/env " ++ - (intercalate " or " . map (map toLower . show) $ support) - --- TODO: Move more of these checks here -shellSupport t = - case t of - T_CaseExpression _ _ list -> forCase (map (\(a,_,_) -> a) list) - T_DollarBraceCommandExpansion {} -> ("${ ..; } command expansion", [Bash, Ksh]) - _ -> ("", []) - where - forCase seps | CaseContinue `elem` seps = ("cases with ;;&", [Bash]) - forCase seps | CaseFallThrough `elem` seps = ("cases with ;&", [Bash, Ksh]) - forCase _ = ("", []) - - -groupWith f = groupBy ((==) `on` f) - -prop_checkMultipleAppends1 = verify checkMultipleAppends "foo >> file; bar >> file; baz >> file;" -prop_checkMultipleAppends2 = verify checkMultipleAppends "foo >> file; bar | grep f >> file; baz >> file;" -prop_checkMultipleAppends3 = verifyNot checkMultipleAppends "foo < file; bar < file; baz < file;" -checkMultipleAppends params t = - mapM_ checkList $ getCommandSequences t - where - checkList list = - mapM_ checkGroup (groupWith (fmap fst) $ map getTarget list) - checkGroup (Just (_,id):_:_:_) = - style id 2129 - "Consider using { cmd1; cmd2; } >> file instead of individual redirects." - checkGroup _ = return () - getTarget (T_Annotation _ _ t) = getTarget t - getTarget (T_Pipeline _ _ args@(_:_)) = getTarget (last args) - getTarget (T_Redirecting id list _) = do - file <- mapMaybe getAppend list !!! 0 - return (file, id) - getTarget _ = Nothing - getAppend (T_FdRedirect _ _ (T_IoFile _ T_DGREAT {} f)) = return f - getAppend _ = Nothing - - -prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\"" -prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'" -prop_checkSuspiciousIFS3 = verify checkSuspiciousIFS "IFS=' \\t\\n'" -checkSuspiciousIFS params (T_Assignment _ _ "IFS" [] value) = - mapM_ check $ getLiteralString value - where - hasDollarSingle = shellType params == Bash || shellType params == Ksh - n = if hasDollarSingle then "$'\\n'" else "''" - t = if hasDollarSingle then "$'\\t'" else "\"$(printf '\\t')\"" - check value = - case value of - "\\n" -> suggest n - "\\t" -> suggest t - x | '\\' `elem` x -> suggest2 "a literal backslash" - x | 'n' `elem` x -> suggest2 "the literal letter 'n'" - x | 't' `elem` x -> suggest2 "the literal letter 't'" - _ -> return () - suggest r = warn (getId value) 2141 $ "This backslash is literal. Did you mean IFS=" ++ r ++ " ?" - suggest2 desc = warn (getId value) 2141 $ "This IFS value contains " ++ desc ++ ". For tabs/linefeeds/escapes, use $'..', literal, or printf." -checkSuspiciousIFS _ _ = return () - - -prop_checkGrepQ1 = verify checkShouldUseGrepQ "[[ $(foo | grep bar) ]]" -prop_checkGrepQ2 = verify checkShouldUseGrepQ "[ -z $(fgrep lol) ]" -prop_checkGrepQ3 = verify checkShouldUseGrepQ "[ -n \"$(foo | zgrep lol)\" ]" -prop_checkGrepQ4 = verifyNot checkShouldUseGrepQ "[ -z $(grep bar | cmd) ]" -prop_checkGrepQ5 = verifyNot checkShouldUseGrepQ "rm $(ls | grep file)" -prop_checkGrepQ6 = verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]" -checkShouldUseGrepQ params t = - sequence_ $ case t of - TC_Nullary id _ token -> check id True token - TC_Unary id _ "-n" token -> check id True token - TC_Unary id _ "-z" token -> check id False token - _ -> fail "not check" - where - check id bool token = do - name <- getFinalGrep token - let op = if bool then "-n" else "-z" - let flip = if bool then "" else "! " - return . style id 2143 $ - "Use " ++ flip ++ name ++ " -q instead of " ++ - "comparing output with [ " ++ op ++ " .. ]." - - getFinalGrep t = do - cmds <- getPipeline t - guard . not . null $ cmds - name <- getCommandBasename $ last cmds - guard . isGrep $ name - return name - getPipeline t = - case t of - T_NormalWord _ [x] -> getPipeline x - T_DoubleQuoted _ [x] -> getPipeline x - T_DollarExpansion _ [x] -> getPipeline x - T_Pipeline _ _ cmds -> return cmds - _ -> fail "unknown" - isGrep = (`elem` ["grep", "egrep", "fgrep", "bz3grep", "bzgrep", "xzgrep", "zgrep", "zipgrep", "zstdgrep"]) - -prop_checkTestArgumentSplitting1 = verify checkTestArgumentSplitting "[ -e *.mp3 ]" -prop_checkTestArgumentSplitting2 = verifyNot checkTestArgumentSplitting "[[ $a == *b* ]]" -prop_checkTestArgumentSplitting3 = verify checkTestArgumentSplitting "[[ *.png == '' ]]" -prop_checkTestArgumentSplitting4 = verify checkTestArgumentSplitting "[[ foo == f{o,oo,ooo} ]]" -prop_checkTestArgumentSplitting5 = verify checkTestArgumentSplitting "[[ $@ ]]" -prop_checkTestArgumentSplitting6 = verify checkTestArgumentSplitting "[ -e $@ ]" -prop_checkTestArgumentSplitting7 = verify checkTestArgumentSplitting "[ $@ == $@ ]" -prop_checkTestArgumentSplitting8 = verify checkTestArgumentSplitting "[[ $@ = $@ ]]" -prop_checkTestArgumentSplitting9 = verifyNot checkTestArgumentSplitting "[[ foo =~ bar{1,2} ]]" -prop_checkTestArgumentSplitting10 = verifyNot checkTestArgumentSplitting "[ \"$@\" ]" -prop_checkTestArgumentSplitting11 = verify checkTestArgumentSplitting "[[ \"$@\" ]]" -prop_checkTestArgumentSplitting12 = verify checkTestArgumentSplitting "[ *.png ]" -prop_checkTestArgumentSplitting13 = verify checkTestArgumentSplitting "[ \"$@\" == \"\" ]" -prop_checkTestArgumentSplitting14 = verify checkTestArgumentSplitting "[[ \"$@\" == \"\" ]]" -prop_checkTestArgumentSplitting15 = verifyNot checkTestArgumentSplitting "[[ \"$*\" == \"\" ]]" -prop_checkTestArgumentSplitting16 = verifyNot checkTestArgumentSplitting "[[ -v foo[123] ]]" -prop_checkTestArgumentSplitting17 = verifyNot checkTestArgumentSplitting "#!/bin/ksh\n[ -e foo* ]" -prop_checkTestArgumentSplitting18 = verify checkTestArgumentSplitting "#!/bin/ksh\n[ -d foo* ]" -prop_checkTestArgumentSplitting19 = verifyNot checkTestArgumentSplitting "[[ var[x] -eq 2*3 ]]" -prop_checkTestArgumentSplitting20 = verify checkTestArgumentSplitting "[ var[x] -eq 2 ]" -prop_checkTestArgumentSplitting21 = verify checkTestArgumentSplitting "[ 6 -eq 2*3 ]" -checkTestArgumentSplitting :: Parameters -> Token -> Writer [TokenComment] () -checkTestArgumentSplitting params t = - case t of - (TC_Unary _ typ op token) | isGlob token -> - if op == "-v" - then - when (typ == SingleBracket) $ - err (getId token) 2208 $ - "Use [[ ]] or quote arguments to -v to avoid glob expansion." - else - if (typ == SingleBracket && shellType params == Ksh) - then - -- Ksh appears to stop processing after unrecognized tokens, so operators - -- will effectively work with globs, but only the first match. - when (op `elem` [['-', c] | c <- "bcdfgkprsuwxLhNOGRS" ]) $ - warn (getId token) 2245 $ - op ++ " only applies to the first expansion of this glob. Use a loop to check any/all." - else - err (getId token) 2144 $ - op ++ " doesn't work with globs. Use a for loop." - - (TC_Nullary _ typ token) -> do - checkBraces typ token - checkGlobs typ token - when (typ == DoubleBracket) $ - checkArrays typ token - - (TC_Unary _ typ op token) -> checkAll typ token - - (TC_Binary _ typ op lhs rhs) | op `elem` arithmeticBinaryTestOps -> - if typ == DoubleBracket - then - mapM_ (\c -> do - checkArrays typ c - checkBraces typ c) [lhs, rhs] - else - mapM_ (\c -> do - checkNumericalGlob typ c - checkArrays typ c - checkBraces typ c) [lhs, rhs] - - (TC_Binary _ typ op lhs rhs) -> - if op `elem` ["=", "==", "!=", "=~"] - then do - checkAll typ lhs - checkArrays typ rhs - checkBraces typ rhs - else mapM_ (checkAll typ) [lhs, rhs] - _ -> return () - where - checkAll typ token = do - checkArrays typ token - checkBraces typ token - checkGlobs typ token - - checkArrays typ token = - when (any isArrayExpansion $ getWordParts token) $ - if typ == SingleBracket - then warn (getId token) 2198 "Arrays don't work as operands in [ ]. Use a loop (or concatenate with * instead of @)." - else err (getId token) 2199 "Arrays implicitly concatenate in [[ ]]. Use a loop (or explicit * instead of @)." - - checkBraces typ token = - when (any isBraceExpansion $ getWordParts token) $ - if typ == SingleBracket - then warn (getId token) 2200 "Brace expansions don't work as operands in [ ]. Use a loop." - else err (getId token) 2201 "Brace expansion doesn't happen in [[ ]]. Use a loop." - - checkGlobs typ token = - when (isGlob token) $ - if typ == SingleBracket - then warn (getId token) 2202 "Globs don't work as operands in [ ]. Use a loop." - else err (getId token) 2203 "Globs are ignored in [[ ]] except right of =/!=. Use a loop." - - checkNumericalGlob SingleBracket token = - -- var[x] and x*2 look like globs - when (shellType params /= Ksh && isGlob token) $ - err (getId token) 2255 "[ ] does not apply arithmetic evaluation. Evaluate with $((..)) for numbers, or use string comparator for strings." - - -prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo" -prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo" -prop_checkReadWithoutR3 = verifyNot checkReadWithoutR "read -t 0" -prop_checkReadWithoutR4 = verifyNot checkReadWithoutR "read -t 0 && read --d '' -r bar" -prop_checkReadWithoutR5 = verifyNot checkReadWithoutR "read -t 0 foo < file.txt" -prop_checkReadWithoutR6 = verifyNot checkReadWithoutR "read -u 3 -t 0" -checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" - && "r" `notElem` map snd flags && not has_t0 = - info (getId $ getCommandTokenOrThis t) 2162 "read without -r will mangle backslashes." - where - flags = getAllFlags t - has_t0 = Just "0" == do - parsed <- getGnuOpts flagsForRead $ arguments t - (_, t) <- lookup "t" parsed - getLiteralString t - -checkReadWithoutR _ _ = return () - -prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo" -prop_checkUncheckedCd2 = verifyNotTree checkUncheckedCdPushdPopd "cd ~/src || exit; rm -r foo" -prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; cd ~/src; rm -r foo" -prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCdPushdPopd "if cd foo; then rm foo; fi" -prop_checkUncheckedCd5 = verifyTree checkUncheckedCdPushdPopd "if true; then cd foo; fi" -prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .." -prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar" -prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm bar" -prop_checkUncheckedCd9 = verifyTree checkUncheckedCdPushdPopd "builtin cd ~/src; rm -r foo" -prop_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo" -prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo" -prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; rm -r foo" -prop_checkUncheckedPushd4 = verifyNotTree checkUncheckedCdPushdPopd "if pushd foo; then rm foo; fi" -prop_checkUncheckedPushd5 = verifyTree checkUncheckedCdPushdPopd "if true; then pushd foo; fi" -prop_checkUncheckedPushd6 = verifyNotTree checkUncheckedCdPushdPopd "pushd .." -prop_checkUncheckedPushd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npushd foo\nrm bar" -prop_checkUncheckedPushd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; pushd foo; rm bar" -prop_checkUncheckedPushd9 = verifyNotTree checkUncheckedCdPushdPopd "pushd -n foo" -prop_checkUncheckedPopd1 = verifyTree checkUncheckedCdPushdPopd "popd; rm -r foo" -prop_checkUncheckedPopd2 = verifyNotTree checkUncheckedCdPushdPopd "popd || exit; rm -r foo" -prop_checkUncheckedPopd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; popd; rm -r foo" -prop_checkUncheckedPopd4 = verifyNotTree checkUncheckedCdPushdPopd "if popd; then rm foo; fi" -prop_checkUncheckedPopd5 = verifyTree checkUncheckedCdPushdPopd "if true; then popd; fi" -prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd" -prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar" -prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar" -prop_checkUncheckedPopd9 = verifyNotTree checkUncheckedCdPushdPopd "popd -n foo" -prop_checkUncheckedPopd10 = verifyNotTree checkUncheckedCdPushdPopd "cd ../.." -prop_checkUncheckedPopd11 = verifyNotTree checkUncheckedCdPushdPopd "cd ../.././.." -prop_checkUncheckedPopd12 = verifyNotTree checkUncheckedCdPushdPopd "cd /" -prop_checkUncheckedPopd13 = verifyTree checkUncheckedCdPushdPopd "cd ../../.../.." - -checkUncheckedCdPushdPopd params root = - if hasSetE params then - [] - else execWriter $ doAnalysis checkElement root - where - checkElement t@T_SimpleCommand {} - | name `elem` ["cd", "pushd", "popd"] - && not (isSafeDir t) - && not (name `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t))) - && not (isCondition $ getPath (parentMap params) t) = - warnWithFix (getId t) 2164 - ("Use '" ++ name ++ " ... || exit' or '" ++ name ++ " ... || return' in case " ++ name ++ " fails.") - (fixWith [replaceEnd (getId t) params 0 " || exit"]) - where name = getName t - checkElement _ = return () - getName t = fromMaybe "" $ getCommandName t - isSafeDir t = case oversimplify t of - [_, str] -> str `matches` regex - _ -> False - regex = mkRegex "^/*((\\.|\\.\\.)/+)*(\\.|\\.\\.)?$" - -prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done" -prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done" -prop_checkLoopVariableReassignment3 = verifyNot checkLoopVariableReassignment "for i in *; do for j in *.bar; do true; done; done" -prop_checkLoopVariableReassignment4 = verifyNot checkLoopVariableReassignment "for _ in *; do for _ in *.bar; do true; done; done" -checkLoopVariableReassignment params token = - sequence_ $ case token of - T_ForIn {} -> check - T_ForArithmetic {} -> check - _ -> Nothing - where - check = do - str <- loopVariable token - guard $ str /= "_" - next <- find (\x -> loopVariable x == Just str) path - return $ do - warn (getId token) 2165 "This nested loop overrides the index variable of its parent." - warn (getId next) 2167 "This parent loop has its index variable overridden." - path = NE.tail $ getPath (parentMap params) token - loopVariable :: Token -> Maybe String - loopVariable t = - case t of - T_ForIn _ s _ _ -> return s - T_ForArithmetic _ - (TA_Sequence _ - [TA_Assignment _ "=" - (TA_Variable _ var _ ) _]) - _ _ _ -> return var - _ -> fail "not loop" - -prop_checkTrailingBracket1 = verify checkTrailingBracket "if -z n ]]; then true; fi " -prop_checkTrailingBracket2 = verifyNot checkTrailingBracket "if [[ -z n ]]; then true; fi " -prop_checkTrailingBracket3 = verify checkTrailingBracket "a || b ] && thing" -prop_checkTrailingBracket4 = verifyNot checkTrailingBracket "run [ foo ]" -prop_checkTrailingBracket5 = verifyNot checkTrailingBracket "run bar ']'" -checkTrailingBracket _ token = - case token of - T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token - _ -> return () - where - check (T_NormalWord id [T_Literal _ str]) command - | str `elem` [ "]]", "]" ] - && opposite `notElem` parameters - = warn id 2171 $ - "Found trailing " ++ str ++ " outside test. Add missing " ++ opposite ++ " or quote if intentional." - where - opposite = invert str - parameters = oversimplify command - check _ _ = return () - invert s = - case s of - "]]" -> "[[" - "]" -> "[" - x -> x - -prop_checkReturnAgainstZero1 = verify checkReturnAgainstZero "[ $? -eq 0 ]" -prop_checkReturnAgainstZero2 = verify checkReturnAgainstZero "[[ \"$?\" -gt 0 ]]" -prop_checkReturnAgainstZero3 = verify checkReturnAgainstZero "[[ 0 -ne $? ]]" -prop_checkReturnAgainstZero4 = verifyNot checkReturnAgainstZero "[[ $? -eq 4 ]]" -prop_checkReturnAgainstZero5 = verify checkReturnAgainstZero "[[ 0 -eq $? ]]" -prop_checkReturnAgainstZero6 = verifyNot checkReturnAgainstZero "[[ $R -eq 0 ]]" -prop_checkReturnAgainstZero7 = verify checkReturnAgainstZero "(( $? == 0 ))" -prop_checkReturnAgainstZero8 = verify checkReturnAgainstZero "(( $? ))" -prop_checkReturnAgainstZero9 = verify checkReturnAgainstZero "(( ! $? ))" -prop_checkReturnAgainstZero10 = verifyNot checkReturnAgainstZero "x=$(( $? > 0 ))" -prop_checkReturnAgainstZero11 = verify checkReturnAgainstZero "(( ! ! ! $? ))" -prop_checkReturnAgainstZero12 = verify checkReturnAgainstZero "[ ! $? -eq 0 ]" -prop_checkReturnAgainstZero13 = verifyNot checkReturnAgainstZero "(( ! $? && $? > 42))" -prop_checkReturnAgainstZero14 = verifyNot checkReturnAgainstZero "[[ -e foo || $? -eq 0 ]]" -prop_checkReturnAgainstZero15 = verifyNot checkReturnAgainstZero "(( $?, n=1 ))" -prop_checkReturnAgainstZero16 = verifyNot checkReturnAgainstZero "(( $? || $? == 4 ))" -prop_checkReturnAgainstZero17 = verifyNot checkReturnAgainstZero "(( $? + 0 ))" -prop_checkReturnAgainstZero18 = verifyNot checkReturnAgainstZero "f() { if [ $? -eq 0 ]; then :; fi; }" -prop_checkReturnAgainstZero19 = verifyNot checkReturnAgainstZero "f() ( [ $? -eq 0 ] || exit 42; )" -prop_checkReturnAgainstZero20 = verify checkReturnAgainstZero "f() { if :; then x; [ $? -eq 0 ] && exit; fi; }" -prop_checkReturnAgainstZero21 = verify checkReturnAgainstZero "(( ( $? ) ))" -prop_checkReturnAgainstZero22 = verify checkReturnAgainstZero "[[ ( $? -eq 0 ) ]]" -checkReturnAgainstZero params token = - case token of - TC_Binary id _ op lhs rhs -> check op lhs rhs - TA_Binary id op lhs rhs - | op `elem` [">", "<", ">=", "<=", "==", "!="] -> check op lhs rhs - TA_Unary id op@"!" exp - | isExitCode exp -> message (checksSuccessLhs op) (getId exp) - TA_Sequence _ [exp] - | isExitCode exp -> message False (getId exp) - _ -> return () - where - -- We don't want to warn about composite expressions like - -- [[ $? -eq 0 || $? -eq 4 ]] since these can be annoying to rewrite. - isOnlyTestInCommand t = - case NE.tail $ getPath (parentMap params) t of - (T_Condition {}):_ -> True - (T_Arithmetic {}):_ -> True - (TA_Sequence _ [_]):(T_Arithmetic {}):_ -> True - - -- Some negations and groupings are also fine - next@(TC_Unary _ _ "!" _):_ -> isOnlyTestInCommand next - next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next - next@(TC_Group {}):_ -> isOnlyTestInCommand next - next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next - next@(TA_Parenthesis _ _):_ -> isOnlyTestInCommand next - _ -> False - - -- TODO: Do better $? tracking and filter on whether - -- the target command is in the same function - getFirstCommandInFunction = f - where - f t = case t of - T_Function _ _ _ _ x -> f x - T_BraceGroup _ (x:_) -> f x - T_Subshell _ (x:_) -> f x - T_Annotation _ _ x -> f x - T_AndIf _ x _ -> f x - T_OrIf _ x _ -> f x - T_Pipeline _ _ (x:_) -> f x - T_Redirecting _ _ (T_IfExpression _ (((x:_),_):_) _) -> f x - x -> x - - isFirstCommandInFunction = fromMaybe False $ do - let path = getPath (parentMap params) token - func <- find isFunction path - cmd <- getClosestCommand (parentMap params) token - return $ getId cmd == getId (getFirstCommandInFunction func) - - -- Is "$? op 0" trying to check if the command succeeded? - checksSuccessLhs op = not $ op `elem` ["-gt", "-ne", "!=", "!"] - -- Is "0 op $?" trying to check if the command succeeded? - checksSuccessRhs op = op `notElem` ["-ne", "!="] - - check op lhs rhs = - if isZero rhs && isExitCode lhs - then message (checksSuccessLhs op) (getId lhs) - else when (isZero lhs && isExitCode rhs) $ message (checksSuccessRhs op) (getId rhs) - isZero t = getLiteralString t == Just "0" - isExitCode t = - case getWordParts t of - [T_DollarBraced _ _ l] -> concat (oversimplify l) == "?" - _ -> False - - message forSuccess id = when (isOnlyTestInCommand token && not isFirstCommandInFunction) $ style id 2181 $ - "Check exit code directly with e.g. 'if " ++ (if forSuccess then "" else "! ") ++ "mycmd;', not indirectly with $?." - - -prop_checkRedirectedNowhere1 = verify checkRedirectedNowhere "> file" -prop_checkRedirectedNowhere2 = verify checkRedirectedNowhere "> file | grep foo" -prop_checkRedirectedNowhere3 = verify checkRedirectedNowhere "grep foo | > bar" -prop_checkRedirectedNowhere4 = verifyNot checkRedirectedNowhere "grep foo > bar" -prop_checkRedirectedNowhere5 = verifyNot checkRedirectedNowhere "foo | grep bar > baz" -prop_checkRedirectedNowhere6 = verifyNot checkRedirectedNowhere "var=$(value) 2> /dev/null" -prop_checkRedirectedNowhere7 = verifyNot checkRedirectedNowhere "var=$(< file)" -prop_checkRedirectedNowhere8 = verifyNot checkRedirectedNowhere "var=`< file`" -checkRedirectedNowhere params token = - case token of - T_Pipeline _ _ [single] -> sequence_ $ do - redir <- getDanglingRedirect single - guard . not $ isInExpansion token - return $ warn (getId redir) 2188 "This redirection doesn't have a command. Move to its command (or use 'true' as no-op)." - - T_Pipeline _ _ list -> forM_ list $ \x -> sequence_ $ do - redir <- getDanglingRedirect x - return $ err (getId redir) 2189 "You can't have | between this redirection and the command it should apply to." - - _ -> return () - where - isInExpansion t = - case NE.tail $ getPath (parentMap params) t of - T_DollarExpansion _ [_] : _ -> True - T_Backticked _ [_] : _ -> True - t@T_Annotation {} : _ -> isInExpansion t - _ -> False - getDanglingRedirect token = - case token of - T_Redirecting _ (first:_) (T_SimpleCommand _ [] []) -> return first - _ -> Nothing - - -prop_checkArrayAssignmentIndices1 = verifyTree checkArrayAssignmentIndices "declare -A foo; foo=(bar)" -prop_checkArrayAssignmentIndices2 = verifyNotTree checkArrayAssignmentIndices "declare -a foo; foo=(bar)" -prop_checkArrayAssignmentIndices3 = verifyNotTree checkArrayAssignmentIndices "declare -A foo; foo=([i]=bar)" -prop_checkArrayAssignmentIndices4 = verifyTree checkArrayAssignmentIndices "typeset -A foo; foo+=(bar)" -prop_checkArrayAssignmentIndices5 = verifyTree checkArrayAssignmentIndices "arr=( [foo]= bar )" -prop_checkArrayAssignmentIndices6 = verifyTree checkArrayAssignmentIndices "arr=( [foo] = bar )" -prop_checkArrayAssignmentIndices7 = verifyNotTree checkArrayAssignmentIndices "arr=( var=value )" -prop_checkArrayAssignmentIndices8 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=bar )" -prop_checkArrayAssignmentIndices9 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=\"\" )" -prop_checkArrayAssignmentIndices10 = verifyTree checkArrayAssignmentIndices "declare -A arr; arr=( var=value )" -prop_checkArrayAssignmentIndices11 = verifyTree checkArrayAssignmentIndices "arr=( 1=value )" -prop_checkArrayAssignmentIndices12 = verifyTree checkArrayAssignmentIndices "arr=( $a=value )" -prop_checkArrayAssignmentIndices13 = verifyTree checkArrayAssignmentIndices "arr=( $((1+1))=value )" -checkArrayAssignmentIndices params root = - runNodeAnalysis check params root - where - assocs = getAssociativeArrays root - check _ t = - case t of - T_Assignment _ _ name [] (T_Array _ list) -> - let isAssoc = name `elem` assocs in - mapM_ (checkElement isAssoc) list - _ -> return () - - checkElement isAssociative t = - case t of - T_IndexedElement _ _ (T_Literal id "") -> - warn id 2192 "This array element has no value. Remove spaces after = or use \"\" for empty string." - T_IndexedElement {} -> - return () - - T_NormalWord _ parts -> - let literalEquals = do - T_Literal id str <- parts - let (before, after) = break ('=' ==) str - guard $ all isDigit before && not (null after) - return $ warnWithFix id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it." (surroundWith id params "\"") - in - if null literalEquals && isAssociative - then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ." - else sequence_ literalEquals - - _ -> return () - - -prop_checkUnmatchableCases1 = verify checkUnmatchableCases "case foo in bar) true; esac" -prop_checkUnmatchableCases2 = verify checkUnmatchableCases "case foo-$bar in ??|*) true; esac" -prop_checkUnmatchableCases3 = verify checkUnmatchableCases "case foo in foo) true; esac" -prop_checkUnmatchableCases4 = verifyNot checkUnmatchableCases "case foo-$bar in foo*|*bar|*baz*) true; esac" -prop_checkUnmatchableCases5 = verify checkUnmatchableCases "case $f in *.txt) true;; f??.txt) false;; esac" -prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac" -prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac" -prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac" -prop_checkUnmatchableCases9 = verifyNot checkUnmatchableCases "case $f in x) true;;& x) false;; esac" -checkUnmatchableCases params t = - case t of - T_CaseExpression _ word list -> do - -- Check all patterns for whether they can ever match - let allpatterns = concatMap snd3 list - -- Check only the non-fallthrough branches for shadowing - let breakpatterns = concatMap snd3 $ filter (\x -> fst3 x == CaseBreak) list - - if isConstant word - then warn (getId word) 2194 - "This word is constant. Did you forget the $ on a variable?" - else mapM_ (check $ wordToPseudoGlob word) allpatterns - - let exactGlobs = tupMap wordToExactPseudoGlob breakpatterns - let fuzzyGlobs = tupMap wordToPseudoGlob breakpatterns - let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs) - - mapM_ checkDoms dominators - - _ -> return () - where - fst3 (x,_,_) = x - snd3 (_,x,_) = x - tp = tokenPositions params - check target candidate = unless (pseudoGlobsCanOverlap target $ wordToPseudoGlob candidate) $ - warn (getId candidate) 2195 - "This pattern will never match the case statement's word. Double check them." - - tupMap f l = map (\x -> (x, f x)) l - checkDoms ((glob, Just x), rest) = - forM_ (find (\(_, p) -> x `pseudoGlobIsSuperSetof` p) rest) $ - \(first,_) -> do - warn (getId glob) 2221 $ "This pattern always overrides a later one" <> patternContext (getId first) - warn (getId first) 2222 $ "This pattern never matches because of a previous pattern" <> patternContext (getId glob) - where - patternContext :: Id -> String - patternContext id = - case posLine . fst <$> Map.lookup id tp of - Just l -> " on line " <> show l <> "." - _ -> "." - checkDoms _ = return () - - -prop_checkSubshellAsTest1 = verify checkSubshellAsTest "( -e file )" -prop_checkSubshellAsTest2 = verify checkSubshellAsTest "( 1 -gt 2 )" -prop_checkSubshellAsTest3 = verifyNot checkSubshellAsTest "( grep -c foo bar )" -prop_checkSubshellAsTest4 = verifyNot checkSubshellAsTest "[ 1 -gt 2 ]" -prop_checkSubshellAsTest5 = verify checkSubshellAsTest "( -e file && -x file )" -prop_checkSubshellAsTest6 = verify checkSubshellAsTest "( -e file || -x file && -t 1 )" -prop_checkSubshellAsTest7 = verify checkSubshellAsTest "( ! -d file )" -checkSubshellAsTest _ t = - case t of - T_Subshell id [w] -> check id w - _ -> return () - where - check id t = case t of - (T_Banged _ w) -> check id w - (T_AndIf _ w _) -> check id w - (T_OrIf _ w _) -> check id w - (T_Pipeline _ _ [T_Redirecting _ _ (T_SimpleCommand _ [] (first:second:_))]) -> - checkParams id first second - _ -> return () - - - checkParams id first second = do - when (maybe False (`elem` unaryTestOps) $ getLiteralString first) $ - err id 2204 "(..) is a subshell. Did you mean [ .. ], a test expression?" - when (maybe False (`elem` binaryTestOps) $ getLiteralString second) $ - warn id 2205 "(..) is a subshell. Did you mean [ .. ], a test expression?" - - -prop_checkSplittingInArrays1 = verify checkSplittingInArrays "a=( $var )" -prop_checkSplittingInArrays2 = verify checkSplittingInArrays "a=( $(cmd) )" -prop_checkSplittingInArrays3 = verifyNot checkSplittingInArrays "a=( \"$var\" )" -prop_checkSplittingInArrays4 = verifyNot checkSplittingInArrays "a=( \"$(cmd)\" )" -prop_checkSplittingInArrays5 = verifyNot checkSplittingInArrays "a=( $! $$ $# )" -prop_checkSplittingInArrays6 = verifyNot checkSplittingInArrays "a=( ${#arr[@]} )" -prop_checkSplittingInArrays7 = verifyNot checkSplittingInArrays "a=( foo{1,2} )" -prop_checkSplittingInArrays8 = verifyNot checkSplittingInArrays "a=( * )" -checkSplittingInArrays params t = - case t of - T_Array _ elements -> mapM_ check elements - _ -> return () - where - check word = case word of - T_NormalWord _ parts -> mapM_ checkPart parts - _ -> return () - checkPart part = case part of - T_DollarExpansion id _ -> forCommand id - T_DollarBraceCommandExpansion id _ _ -> forCommand id - T_Backticked id _ -> forCommand id - T_DollarBraced id _ str | - not (isCountingReference part) - && not (isQuotedAlternativeReference part) - && getBracedReference (concat $ oversimplify str) `notElem` variablesWithoutSpaces - -> warn id 2206 $ - if shellType params == Ksh - then "Quote to prevent word splitting/globbing, or split robustly with read -A or while read." - else "Quote to prevent word splitting/globbing, or split robustly with mapfile or read -a." - _ -> return () - - forCommand id = - warn id 2207 $ - if shellType params == Ksh - then "Prefer read -A or while read to split command output (or quote to avoid splitting)." - else "Prefer mapfile or read -a to split command output (or quote to avoid splitting)." - - -prop_checkRedirectionToNumber1 = verify checkRedirectionToNumber "( 1 > 2 )" -prop_checkRedirectionToNumber2 = verify checkRedirectionToNumber "foo 1>2" -prop_checkRedirectionToNumber3 = verifyNot checkRedirectionToNumber "echo foo > '2'" -prop_checkRedirectionToNumber4 = verifyNot checkRedirectionToNumber "foo 1>&2" -checkRedirectionToNumber _ t = case t of - T_IoFile id _ word -> sequence_ $ do - file <- getUnquotedLiteral word - guard $ all isDigit file - return $ warn id 2210 "This is a file redirection. Was it supposed to be a comparison or fd operation?" - _ -> return () - -prop_checkGlobAsCommand1 = verify checkGlobAsCommand "foo*" -prop_checkGlobAsCommand2 = verify checkGlobAsCommand "$(var[i])" -prop_checkGlobAsCommand3 = verifyNot checkGlobAsCommand "echo foo*" -checkGlobAsCommand _ t = case t of - T_SimpleCommand _ _ (first:_) - | isGlob first -> - warn (getId first) 2211 "This is a glob used as a command name. Was it supposed to be in ${..}, array, or is it missing quoting?" - _ -> return () - - -prop_checkFlagAsCommand1 = verify checkFlagAsCommand "-e file" -prop_checkFlagAsCommand2 = verify checkFlagAsCommand "foo\n --bar=baz" -prop_checkFlagAsCommand3 = verifyNot checkFlagAsCommand "'--myexec--' args" -prop_checkFlagAsCommand4 = verifyNot checkFlagAsCommand "var=cmd --arg" -- Handled by SC2037 -checkFlagAsCommand _ t = case t of - T_SimpleCommand _ [] (first:_) - | isUnquotedFlag first -> - warn (getId first) 2215 "This flag is used as a command name. Bad line break or missing [ .. ]?" - _ -> return () - - -prop_checkEmptyCondition1 = verify checkEmptyCondition "if [ ]; then ..; fi" -prop_checkEmptyCondition2 = verifyNot checkEmptyCondition "[ foo -o bar ]" -checkEmptyCondition _ t = case t of - TC_Empty id _ -> style id 2212 "Use 'false' instead of empty [/[[ conditionals." - _ -> return () - -prop_checkPipeToNowhere1 = verify checkPipeToNowhere "foo | echo bar" -prop_checkPipeToNowhere2 = verify checkPipeToNowhere "basename < file.txt" -prop_checkPipeToNowhere3 = verify checkPipeToNowhere "printf 'Lol' <<< str" -prop_checkPipeToNowhere4 = verify checkPipeToNowhere "printf 'Lol' << eof\nlol\neof\n" -prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du" -prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)" -prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls" -prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin" -prop_checkPipeToNowhere10 = verify checkPipeToNowhere "ls > file | grep foo" -prop_checkPipeToNowhere11 = verify checkPipeToNowhere "ls | grep foo < file" -prop_checkPipeToNowhere12 = verify checkPipeToNowhere "ls > foo > bar" -prop_checkPipeToNowhere13 = verify checkPipeToNowhere "ls > foo 2> bar > baz" -prop_checkPipeToNowhere14 = verify checkPipeToNowhere "ls > foo &> bar" -prop_checkPipeToNowhere15 = verifyNot checkPipeToNowhere "ls > foo 2> bar |& grep 'No space left'" -prop_checkPipeToNowhere16 = verifyNot checkPipeToNowhere "echo World | cat << EOF\nhello $(cat)\nEOF\n" -prop_checkPipeToNowhere17 = verify checkPipeToNowhere "echo World | cat << 'EOF'\nhello $(cat)\nEOF\n" -prop_checkPipeToNowhere18 = verifyNot checkPipeToNowhere "ls 1>&3 3>&1 3>&- | wc -l" -prop_checkPipeToNowhere19 = verifyNot checkPipeToNowhere "find . -print0 | du --files0-from=/dev/stdin" -prop_checkPipeToNowhere20 = verifyNot checkPipeToNowhere "find . | du --exclude-from=/dev/fd/0" -prop_checkPipeToNowhere21 = verifyNot checkPipeToNowhere "yes | cp -ri foo/* bar" -prop_checkPipeToNowhere22 = verifyNot checkPipeToNowhere "yes | rm --interactive *" - -data PipeType = StdoutPipe | StdoutStderrPipe | NoPipe deriving (Eq) -checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity () -checkPipeToNowhere params t = - case t of - T_Pipeline _ pipes cmds -> - mapM_ checkPipe $ commandsWithContext pipes cmds - T_Redirecting _ redirects cmd | any redirectsStdin redirects -> checkRedir cmd - _ -> return () - where - checkPipe (input, stage, output) = do - let hasConsumers = hasAdditionalConsumers stage - let hasProducers = hasAdditionalProducers stage - - sequence_ $ do - cmd <- getCommand stage - name <- getCommandBasename cmd - guard $ name `elem` nonReadingCommands - guard $ not hasConsumers && input /= NoPipe - guard . not $ commandSpecificException name cmd - - -- Confusing echo for cat is so common that it's worth a special case - let suggestion = - if name == "echo" - then "Did you want 'cat' instead?" - else "Wrong command or missing xargs?" - return $ warn (getId cmd) 2216 $ - "Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion - - sequence_ $ do - T_Redirecting _ redirs cmd <- return stage - fds <- mapM getRedirectionFds redirs - - let fdAndToken :: [(Integer, Token)] - fdAndToken = - concatMap (\(list, redir) -> map (\n -> (n, redir)) list) $ - zip fds redirs - - let fdMap = - Map.fromListWith (++) $ - map (\(a,b) -> (a,[b])) fdAndToken - - let inputWarning = sequence_ $ do - guard $ input /= NoPipe && not hasConsumers - (override:_) <- Map.lookup 0 fdMap - return $ err (getOpId override) 2259 $ - "This redirection overrides piped input. To use both, merge or pass filenames." - - -- Only produce output warnings for regular pipes, since these are - -- way more common, and `foo > out 2> err |& foo` can still write - -- to stderr if the files fail to open - let outputWarning = sequence_ $ do - guard $ output == StdoutPipe && not hasProducers - (override:_) <- Map.lookup 1 fdMap - return $ err (getOpId override) 2260 $ - "This redirection overrides the output pipe. Use 'tee' to output to both." - - return $ do - inputWarning - outputWarning - mapM_ warnAboutDupes $ Map.assocs fdMap - - commandSpecificException name cmd = - case name of - "du" -> any ((`elem` ["exclude-from", "files0-from"]) . snd) $ getAllFlags cmd - _ | name `elem` interactiveFlagCmds -> hasInteractiveFlag cmd - _ -> False - - warnAboutDupes (n, list@(_:_:_)) = - forM_ list $ \c -> err (getOpId c) 2261 $ - "Multiple redirections compete for " ++ str n ++ ". Use cat, tee, or pass filenames instead." - warnAboutDupes _ = return () - - alternative = - if shellType params `elem` [Bash, Ksh] - then "process substitutions or temp files" - else "temporary files" - - str n = - case n of - 0 -> "stdin" - 1 -> "stdout" - 2 -> "stderr" - _ -> "FD " ++ show n - - checkRedir cmd = sequence_ $ do - name <- getCommandBasename cmd - guard $ name `elem` nonReadingCommands - guard . not $ hasAdditionalConsumers cmd - guard . not $ name `elem` interactiveFlagCmds && hasInteractiveFlag cmd - let suggestion = - if name == "echo" - then "Did you want 'cat' instead?" - else "Bad quoting, wrong command or missing xargs?" - return $ warn (getId cmd) 2217 $ - "Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion - - -- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")? - hasAdditionalConsumers = treeContains mayConsume - -- Could any words in a SimpleCommand produce stdout? E.g. >(tee foo) - hasAdditionalProducers = treeContains mayProduce - treeContains pred t = isNothing $ - doAnalysis (guard . not . pred) t - - interactiveFlagCmds = [ "cp", "mv", "rm" ] - hasInteractiveFlag cmd = cmd `hasFlag` "i" || cmd `hasFlag` "interactive" - - mayConsume t = - case t of - T_ProcSub _ "<" _ -> True - T_Backticked {} -> True - T_DollarExpansion {} -> True - _ -> False - - mayProduce t = - case t of - T_ProcSub _ ">" _ -> True - _ -> False - - getOpId t = - case t of - T_FdRedirect _ _ x -> getOpId x - T_IoFile _ op _ -> getId op - _ -> getId t - - getRedirectionFds t = - case t of - T_FdRedirect _ "" x -> getDefaultFds x - T_FdRedirect _ "&" _ -> return [1, 2] - T_FdRedirect _ num x | all isDigit num -> - -- Don't report the number unless we know what it is. - -- This avoids triggering on 3>&1 1>&3 - getDefaultFds x *> return [read num] - -- Don't bother with {fd}>42 and such - _ -> Nothing - - getDefaultFds redir = - case redir of - T_HereDoc {} -> return [0] - T_HereString {} -> return [0] - T_IoFile _ op _ -> - case op of - T_Less {} -> return [0] - T_Greater {} -> return [1] - T_DGREAT {} -> return [1] - T_GREATAND {} -> return [1, 2] - T_CLOBBER {} -> return [1] - T_IoDuplicate _ op "-" -> getDefaultFds op - _ -> Nothing - _ -> Nothing - - redirectsStdin t = - fromMaybe False $ do - fds <- getRedirectionFds t - return $ 0 `elem` fds - - pipeType t = - case t of - T_Pipe _ "|" -> StdoutPipe - T_Pipe _ "|&" -> StdoutStderrPipe - _ -> NoPipe - - commandsWithContext pipes cmds = - let pipeTypes = map pipeType pipes - inputs = NoPipe : pipeTypes - outputs = pipeTypes ++ [NoPipe] - in - zip3 inputs cmds outputs - -prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { true; }" -prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f" -prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi" -prop_checkUseBeforeDefinition4 = verifyNotTree checkUseBeforeDefinition "mycmd || mycmd() { f; }" -prop_checkUseBeforeDefinition5 = verifyTree checkUseBeforeDefinition "false || mycmd; mycmd() { f; }" -prop_checkUseBeforeDefinition6 = verifyNotTree checkUseBeforeDefinition "f() { one; }; f; f() { two; }; f" -checkUseBeforeDefinition :: Parameters -> Token -> [TokenComment] -checkUseBeforeDefinition params t = fromMaybe [] $ do - cfga <- cfgAnalysis params - let funcs = execState (doAnalysis findFunction t) Map.empty - -- Green cut: no point enumerating commands if there are no functions. - guard . not $ Map.null funcs - return $ execWriter $ doAnalysis (findInvocation cfga funcs) t - where - findFunction t = - case t of - T_Function id _ _ name _ -> modify (Map.insertWith (++) name [id]) - _ -> return () - - findInvocation cfga funcs t = - case t of - T_SimpleCommand id _ (cmd:_) -> sequence_ $ do - name <- getLiteralString cmd - invocations <- Map.lookup name funcs - -- Is the function definitely being defined later? - guard $ any (\c -> CF.doesPostDominate cfga c id) invocations - -- Was one already defined, so it's actually a re-definition? - guard . not $ any (\c -> CF.doesPostDominate cfga id c) invocations - return $ err id 2218 "This function is only defined later. Move the definition up." - _ -> return () - -prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done" -prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done" -prop_checkForLoopGlobVariables3 = verifyNot checkForLoopGlobVariables "for i in $var; do true; done" -checkForLoopGlobVariables _ t = - case t of - T_ForIn _ _ words _ -> mapM_ check words - _ -> return () - where - check (T_NormalWord _ parts) = - when (any isGlob parts) $ - mapM_ suggest $ filter isQuoteableExpansion parts - suggest t = info (getId t) 2231 - "Quote expansions in this for loop glob to prevent wordsplitting, e.g. \"$dir\"/*.txt ." - - -prop_checkSubshelledTests1 = verify checkSubshelledTests "a && ( [ b ] || ! [ c ] )" -prop_checkSubshelledTests2 = verify checkSubshelledTests "( [ a ] )" -prop_checkSubshelledTests3 = verify checkSubshelledTests "( [ a ] && [ b ] || test c )" -prop_checkSubshelledTests4 = verify checkSubshelledTests "( [ a ] && { [ b ] && [ c ]; } )" -prop_checkSubshelledTests5 = verifyNot checkSubshelledTests "( [[ ${var:=x} = y ]] )" -prop_checkSubshelledTests6 = verifyNot checkSubshelledTests "( [[ $((i++)) = 10 ]] )" -prop_checkSubshelledTests7 = verifyNot checkSubshelledTests "( [[ $((i+=1)) = 10 ]] )" -prop_checkSubshelledTests8 = verify checkSubshelledTests "# shellcheck disable=SC2234\nf() ( [[ x ]] )" - -checkSubshelledTests params t = - case t of - T_Subshell id list | all isTestStructure list && (not (hasAssignment t)) -> - case () of - -- Special case for if (test) and while (test) - _ | isCompoundCondition (getPath (parentMap params) t) -> - style id 2233 "Remove superfluous (..) around condition to avoid subshell overhead." - - -- Special case for ([ x ]), except for func() ( [ x ] ) - _ | isSingleTest list && (not $ isFunctionBody (getPath (parentMap params) t)) -> - style id 2234 "Remove superfluous (..) around test command to avoid subshell overhead." - - -- General case for ([ x ] || [ y ] && etc) - _ -> style id 2235 "Use { ..; } instead of (..) to avoid subshell overhead." - _ -> return () - where - - isSingleTest cmds = - case cmds of - [c] | isTestCommand c -> True - _ -> False - - isFunctionBody path = - case path of - (_ NE.:| f:_) -> isFunction f - _ -> False - - isTestStructure t = - case t of - T_Banged _ t -> isTestStructure t - T_AndIf _ a b -> isTestStructure a && isTestStructure b - T_OrIf _ a b -> isTestStructure a && isTestStructure b - T_Pipeline _ [] [T_Redirecting _ _ cmd] -> - case cmd of - T_BraceGroup _ ts -> all isTestStructure ts - T_Subshell _ ts -> all isTestStructure ts - _ -> isTestCommand t - _ -> isTestCommand t - - isTestCommand t = - case t of - T_Pipeline _ [] [T_Redirecting _ _ cmd] -> - case cmd of - T_Condition {} -> True - _ -> cmd `isCommand` "test" - _ -> False - - -- Check if a T_Subshell is used as a condition, e.g. if ( test ) - -- This technically also triggers for `if true; then ( test ); fi` - -- but it's still a valid suggestion. - isCompoundCondition chain = - case dropWhile skippable (NE.tail chain) of - T_IfExpression {} : _ -> True - T_WhileExpression {} : _ -> True - T_UntilExpression {} : _ -> True - _ -> False - - hasAssignment t = isNothing $ doAnalysis guardNotAssignment t - guardNotAssignment t = - case t of - TA_Assignment {} -> Nothing - TA_Unary _ s _ -> guard . not $ "++" `isInfixOf` s || "--" `isInfixOf` s - T_DollarBraced _ _ l -> - let str = concat $ oversimplify l - modifier = getBracedModifier str - in - guard . not $ "=" `isPrefixOf` modifier || ":=" `isPrefixOf` modifier - T_DollarBraceCommandExpansion {} -> Nothing - _ -> Just () - - -- Skip any parent of a T_Subshell until we reach something interesting - skippable t = - case t of - T_Redirecting _ [] _ -> True - T_Pipeline _ [] _ -> True - T_Annotation {} -> True - _ -> False - -prop_checkUnnecessarilyInvertedTest1 = verify checkUnnecessarilyInvertedTest "[ ! -z $var ]" -prop_checkUnnecessarilyInvertedTest2 = verify checkUnnecessarilyInvertedTest "! [[ -n $var ]]" -prop_checkUnnecessarilyInvertedTest3 = verifyNot checkUnnecessarilyInvertedTest "! [ -x $var ]" -prop_checkUnnecessarilyInvertedTest4 = verifyNot checkUnnecessarilyInvertedTest "[[ ! -w $var ]]" -prop_checkUnnecessarilyInvertedTest5 = verifyNot checkUnnecessarilyInvertedTest "[ -z $var ]" -prop_checkUnnecessarilyInvertedTest6 = verify checkUnnecessarilyInvertedTest "! [ $var != foo ]" -prop_checkUnnecessarilyInvertedTest7 = verify checkUnnecessarilyInvertedTest "[[ ! $var == foo ]]" -prop_checkUnnecessarilyInvertedTest8 = verifyNot checkUnnecessarilyInvertedTest "! [[ $var =~ .* ]]" -prop_checkUnnecessarilyInvertedTest9 = verify checkUnnecessarilyInvertedTest "[ ! $var -eq 0 ]" -prop_checkUnnecessarilyInvertedTest10 = verify checkUnnecessarilyInvertedTest "! [[ $var -gt 3 ]]" -checkUnnecessarilyInvertedTest _ t = - case t of - TC_Unary _ _ "!" (TC_Unary _ _ op _) -> - case op of - "-n" -> style (getId t) 2236 "Use -z instead of ! -n." - "-z" -> style (getId t) 2236 "Use -n instead of ! -z." - _ -> return () - T_Banged _ (T_Pipeline _ _ - [T_Redirecting _ _ (T_Condition _ _ (TC_Unary _ _ op _))]) -> - case op of - "-n" -> style (getId t) 2237 "Use [ -z .. ] instead of ! [ -n .. ]." - "-z" -> style (getId t) 2237 "Use [ -n .. ] instead of ! [ -z .. ]." - _ -> return () - TC_Unary _ _ "!" (TC_Binary _ bracketStyle op _ _) -> - maybeSuggestRewrite True bracketStyle (getId t) op - T_Banged _ (T_Pipeline _ _ - [T_Redirecting _ _ (T_Condition _ _ (TC_Binary _ bracketStyle op _ _))]) -> - maybeSuggestRewrite False bracketStyle (getId t) op - _ -> return () - where - inversionMap = Map.fromList [ - ("=", "!="), - ("==", "!="), - ("!=", "="), - ("-eq", "-ne"), - ("-ne", "-eq"), - ("-le", "-gt"), - ("-gt", "-le"), - ("-ge", "-lt"), - ("-lt", "-ge") - ] - maybeSuggestRewrite bangInside bracketStyle id op = sequence_ $ do - newOp <- Map.lookup op inversionMap - let oldExpr = "a " ++ op ++ " b" - let newExpr = "a " ++ newOp ++ " b" - let bracket s = if bracketStyle == SingleBracket then "[ " ++ s ++ " ]" else "[[ " ++ s ++ " ]]" - return $ - if bangInside - then style id 2335 $ "Use " ++ newExpr ++ " instead of ! " ++ oldExpr ++ "." - else style id 2335 $ "Use " ++ (bracket newExpr) ++ " instead of ! " ++ (bracket oldExpr) ++ "." - - -prop_checkRedirectionToCommand1 = verify checkRedirectionToCommand "ls > rm" -prop_checkRedirectionToCommand2 = verifyNot checkRedirectionToCommand "ls > 'rm'" -prop_checkRedirectionToCommand3 = verifyNot checkRedirectionToCommand "ls > myfile" -checkRedirectionToCommand _ t = - case t of - T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands - && str /= "file" -> -- This would be confusing - warn id 2238 "Redirecting to/from command name instead of file. Did you want pipes/xargs (or quote to ignore)?" - _ -> return () - -prop_checkNullaryExpansionTest1 = verify checkNullaryExpansionTest "[[ $(a) ]]" -prop_checkNullaryExpansionTest2 = verify checkNullaryExpansionTest "[[ $a ]]" -prop_checkNullaryExpansionTest3 = verifyNot checkNullaryExpansionTest "[[ $a=1 ]]" -prop_checkNullaryExpansionTest4 = verifyNot checkNullaryExpansionTest "[[ -n $(a) ]]" -prop_checkNullaryExpansionTest5 = verify checkNullaryExpansionTest "[[ \"$a$b\" ]]" -prop_checkNullaryExpansionTest6 = verify checkNullaryExpansionTest "[[ `x` ]]" -checkNullaryExpansionTest params t = - case t of - TC_Nullary _ _ word -> - case getWordParts word of - [t] | isCommandSubstitution t -> - styleWithFix id 2243 "Prefer explicit -n to check for output (or run command without [/[[ to check for success)." fix - - -- If they're constant, you get SC2157 &co - x | all (not . isConstant) x -> - styleWithFix id 2244 "Prefer explicit -n to check non-empty string (or use =/-ne to check boolean/integer)." fix - _ -> return () - where - id = getId word - fix = fixWith [replaceStart id params 0 "-n "] - _ -> return () - - -prop_checkDollarQuoteParen1 = verify checkDollarQuoteParen "$\"(foo)\"" -prop_checkDollarQuoteParen2 = verify checkDollarQuoteParen "$\"{foo}\"" -prop_checkDollarQuoteParen3 = verifyNot checkDollarQuoteParen "\"$(foo)\"" -prop_checkDollarQuoteParen4 = verifyNot checkDollarQuoteParen "$\"..\"" -checkDollarQuoteParen params t = - case t of - T_DollarDoubleQuoted id ((T_Literal _ (c:_)):_) | c `elem` "({" -> - warnWithFix id 2247 "Flip leading $ and \" if this should be a quoted substitution." (fix id) - _ -> return () - where - fix id = fixWith [replaceStart id params 2 "\"$"] - -prop_checkTranslatedStringVariable1 = verify checkTranslatedStringVariable "foo_bar2=val; $\"foo_bar2\"" -prop_checkTranslatedStringVariable2 = verifyNot checkTranslatedStringVariable "$\"foo_bar2\"" -prop_checkTranslatedStringVariable3 = verifyNot checkTranslatedStringVariable "$\"..\"" -prop_checkTranslatedStringVariable4 = verifyNot checkTranslatedStringVariable "var=val; $\"$var\"" -prop_checkTranslatedStringVariable5 = verifyNot checkTranslatedStringVariable "foo=var; bar=val2; $\"foo bar\"" -checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s]) - | all isVariableChar s - && S.member s assignments - = warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id) - where - assignments = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params, isVariableName name] - fix id = fixWith [replaceStart id params 2 "\"$"] -checkTranslatedStringVariable _ _ = return () - -prop_checkDefaultCase1 = verify checkDefaultCase "case $1 in a) true ;; esac" -prop_checkDefaultCase2 = verify checkDefaultCase "case $1 in ?*?) true ;; *? ) true ;; esac" -prop_checkDefaultCase3 = verifyNot checkDefaultCase "case $1 in x|*) true ;; esac" -prop_checkDefaultCase4 = verifyNot checkDefaultCase "case $1 in **) true ;; esac" -checkDefaultCase _ t = - case t of - T_CaseExpression id _ list -> - unless (any canMatchAny list) $ - info id 2249 "Consider adding a default *) case, even if it just exits with error." - _ -> return () - where - canMatchAny (_, list, _) = any canMatchAny' list - -- hlint objects to 'pattern' as a variable name - canMatchAny' pat = fromMaybe False $ do - pg <- wordToExactPseudoGlob pat - return $ pseudoGlobIsSuperSetof pg [PGMany] - -prop_checkUselessBang1 = verify checkUselessBang "set -e; ! true; rest" -prop_checkUselessBang2 = verifyNot checkUselessBang "! true; rest" -prop_checkUselessBang3 = verify checkUselessBang "set -e; while true; do ! true; done" -prop_checkUselessBang4 = verifyNot checkUselessBang "set -e; if ! true; then true; fi" -prop_checkUselessBang5 = verifyNot checkUselessBang "set -e; ( ! true )" -prop_checkUselessBang6 = verify checkUselessBang "set -e; { ! true; }" -prop_checkUselessBang7 = verifyNot checkUselessBang "set -e; x() { ! [ x ]; }" -prop_checkUselessBang8 = verifyNot checkUselessBang "set -e; if { ! true; }; then true; fi" -prop_checkUselessBang9 = verifyNot checkUselessBang "set -e; while ! true; do true; done" -prop_checkUselessBang10 = verify checkUselessBang "set -e\nshellcheck disable=SC0000\n! true\nrest" -checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturningCommands t) - where - check t = - case t of - T_Banged id cmd | not $ isCondition (getPath (parentMap params) t) -> - addComment $ makeCommentWithFix InfoC id 2251 - "This ! is not on a condition and skips errexit. Use `&& exit 1` instead, or make sure $? is checked." - (fixWith [replaceStart id params 1 "", replaceEnd (getId cmd) params 0 " && exit 1"]) - T_Annotation _ _ t -> check t - _ -> return () - - -- Get all the subcommands that aren't likely to be the return value - getNonReturningCommands :: Token -> [Token] - getNonReturningCommands t = - case t of - T_Script _ _ list -> dropLast list - T_BraceGroup _ list -> if isFunctionBody t then dropLast list else list - T_Subshell _ list -> dropLast list - T_WhileExpression _ conds cmds -> dropLast conds ++ cmds - T_UntilExpression _ conds cmds -> dropLast conds ++ cmds - T_ForIn _ _ _ list -> list - T_ForArithmetic _ _ _ _ list -> list - T_Annotation _ _ t -> getNonReturningCommands t - T_IfExpression _ conds elses -> - concatMap (dropLast . fst) conds ++ concatMap snd conds ++ elses - _ -> [] - - isFunctionBody t = - case getPath (parentMap params) t of - _ NE.:| T_Function {}:_-> True - _ -> False - - dropLast t = - case t of - [_] -> [] - x:rest -> x : dropLast rest - _ -> [] - -prop_checkModifiedArithmeticInRedirection1 = verify checkModifiedArithmeticInRedirection "ls > $((i++))" -prop_checkModifiedArithmeticInRedirection2 = verify checkModifiedArithmeticInRedirection "cat < \"foo$((i++)).txt\"" -prop_checkModifiedArithmeticInRedirection3 = verifyNot checkModifiedArithmeticInRedirection "while true; do true; done > $((i++))" -prop_checkModifiedArithmeticInRedirection4 = verify checkModifiedArithmeticInRedirection "cat <<< $((i++))" -prop_checkModifiedArithmeticInRedirection5 = verify checkModifiedArithmeticInRedirection "cat << foo\n$((i++))\nfoo\n" -prop_checkModifiedArithmeticInRedirection6 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/dash\nls > $((i=i+1))" -prop_checkModifiedArithmeticInRedirection7 = verifyNot checkModifiedArithmeticInRedirection "#!/bin/busybox sh\ncat << foo\n$((i++))\nfoo\n" -checkModifiedArithmeticInRedirection params t = unless (shellType params == Dash || shellType params == BusyboxSh) $ - case t of - T_Redirecting _ redirs (T_SimpleCommand _ _ (_:_)) -> mapM_ checkRedirs redirs - _ -> return () - where - checkRedirs t = - case t of - T_FdRedirect _ _ (T_IoFile _ _ word) -> - mapM_ checkArithmetic $ getWordParts word - T_FdRedirect _ _ (T_HereString _ word) -> - mapM_ checkArithmetic $ getWordParts word - T_FdRedirect _ _ (T_HereDoc _ _ _ _ list) -> - mapM_ checkArithmetic list - _ -> return () - checkArithmetic t = - case t of - T_DollarArithmetic _ x -> checkModifying x - _ -> return () - checkModifying t = - case t of - TA_Sequence _ list -> mapM_ checkModifying list - TA_Unary id s _ | s `elem` ["|++", "++|", "|--", "--|"] -> warnFor id - TA_Assignment id _ _ _ -> warnFor id - TA_Binary _ _ x y -> mapM_ checkModifying [x ,y] - TA_Trinary _ x y z -> mapM_ checkModifying [x, y, z] - _ -> return () - warnFor id = - warn id 2257 "Arithmetic modifications in command redirections may be discarded. Do them separately." - -prop_checkAliasUsedInSameParsingUnit1 = verifyTree checkAliasUsedInSameParsingUnit "alias x=y; x" -prop_checkAliasUsedInSameParsingUnit2 = verifyNotTree checkAliasUsedInSameParsingUnit "alias x=y\nx" -prop_checkAliasUsedInSameParsingUnit3 = verifyTree checkAliasUsedInSameParsingUnit "{ alias x=y\nx\n}" -prop_checkAliasUsedInSameParsingUnit4 = verifyNotTree checkAliasUsedInSameParsingUnit "alias x=y; 'x';" -prop_checkAliasUsedInSameParsingUnit5 = verifyNotTree checkAliasUsedInSameParsingUnit ":\n{\n#shellcheck disable=SC2262\nalias x=y\nx\n}" -prop_checkAliasUsedInSameParsingUnit6 = verifyNotTree checkAliasUsedInSameParsingUnit ":\n{\n#shellcheck disable=SC2262\nalias x=y\nalias x=z\nx\n}" -- Only consider the first instance -checkAliasUsedInSameParsingUnit :: Parameters -> Token -> [TokenComment] -checkAliasUsedInSameParsingUnit params root = - let - -- Get all root commands - commands = concat $ getCommandSequences root - -- Group them by whether they start on the same line where the previous one ended - units = groupByLink followsOnLine commands - in - execWriter $ mapM_ checkUnit units - where - lineSpan t = - let m = tokenPositions params in do - (start, end) <- Map.lookup t m - return $ (posLine start, posLine end) - - followsOnLine a b = fromMaybe False $ do - (_, end) <- lineSpan (getId a) - (start, _) <- lineSpan (getId b) - return $ end == start - - checkUnit :: [Token] -> Writer [TokenComment] () - checkUnit unit = evalStateT (mapM_ (doAnalysis findCommands) unit) (Map.empty) - - findCommands :: Token -> StateT (Map.Map String Token) (Writer [TokenComment]) () - findCommands t = case t of - T_SimpleCommand _ _ (cmd:args) -> - case getUnquotedLiteral cmd of - Just "alias" -> - mapM_ addAlias args - Just name | '/' `notElem` name -> do - cmd <- gets (Map.lookup name) - case cmd of - Just alias -> - unless (isSourced params t || shouldIgnoreCode params 2262 alias) $ do - warn (getId alias) 2262 "This alias can't be defined and used in the same parsing unit. Use a function instead." - info (getId t) 2263 "Since they're in the same parsing unit, this command will not refer to the previously mentioned alias." - _ -> return () - _ -> return () - _ -> return () - addAlias arg = do - let (name, value) = break (== '=') $ getLiteralStringDef "-" arg - when (isVariableName name && not (null value)) $ - modify (Map.insertWith (\new old -> old) name arg) - -isSourced params t = - let - f (T_SourceCommand {}) = True - f _ = False - in - any f $ getPath (parentMap params) t - - --- Like groupBy, but compares pairs of adjacent elements, rather than against the first of the span -prop_groupByLink1 = groupByLink (\a b -> a+1 == b) [1,2,3,2,3,7,8,9] == [[1,2,3], [2,3], [7,8,9]] -prop_groupByLink2 = groupByLink (==) ([] :: [()]) == [] -groupByLink :: (a -> a -> Bool) -> [a] -> [[a]] -groupByLink f list = - case list of - [] -> [] - (x:xs) -> foldr c n xs x [] - where - c next rest current span = - if f current next - then rest next (current:span) - else (reverse $ current:span) : rest next [] - n current span = [reverse (current:span)] - - -prop_checkBlatantRecursion1 = verify checkBlatantRecursion ":(){ :|:& };:" -prop_checkBlatantRecursion2 = verify checkBlatantRecursion "f() { f; }" -prop_checkBlatantRecursion3 = verifyNot checkBlatantRecursion "f() { command f; }" -prop_checkBlatantRecursion4 = verify checkBlatantRecursion "cd() { cd \"$lol/$1\" || exit; }" -prop_checkBlatantRecursion5 = verifyNot checkBlatantRecursion "cd() { [ -z \"$1\" ] || cd \"$1\"; }" -prop_checkBlatantRecursion6 = verifyNot checkBlatantRecursion "cd() { something; cd $1; }" -prop_checkBlatantRecursion7 = verifyNot checkBlatantRecursion "cd() { builtin cd $1; }" -checkBlatantRecursion :: Parameters -> Token -> Writer [TokenComment] () -checkBlatantRecursion params t = - case t of - T_Function _ _ _ name body -> - case getCommandSequences body of - [first : _] -> checkList name first - _ -> return () - _ -> return () - where - checkList :: String -> Token -> Writer [TokenComment] () - checkList name t = - case t of - T_Backgrounded _ t -> checkList name t - T_AndIf _ lhs _ -> checkList name lhs - T_OrIf _ lhs _ -> checkList name lhs - T_Pipeline _ _ cmds -> mapM_ (checkCommand name) cmds - _ -> return () - - checkCommand :: String -> Token -> Writer [TokenComment] () - checkCommand name cmd = sequence_ $ do - let (invokedM, t) = getCommandNameAndToken True cmd - invoked <- invokedM - guard $ name == invoked - return $ - errWithFix (getId t) 2264 - ("This function unconditionally re-invokes itself. Missing 'command'?") - (fixWith [replaceStart (getId t) params 0 $ "command "]) - - -prop_checkBadTestAndOr1 = verify checkBadTestAndOr "[ x ] & [ y ]" -prop_checkBadTestAndOr2 = verify checkBadTestAndOr "test -e foo & [ y ]" -prop_checkBadTestAndOr3 = verify checkBadTestAndOr "[ x ] | [ y ]" -checkBadTestAndOr params t = - case t of - T_Pipeline _ seps cmds@(_:_:_) -> checkOrs seps cmds - T_Backgrounded id cmd -> checkAnds id cmd - _ -> return () - where - checkOrs seps cmds = - let maybeSeps = map Just seps - commandWithSeps = zip3 (Nothing:maybeSeps) cmds (maybeSeps ++ [Nothing]) - in - mapM_ checkTest commandWithSeps - checkTest (before, cmd, after) = - when (isTestCommand cmd) $ do - checkPipe before - checkPipe after - - checkPipe t = - case t of - Just (T_Pipe id "|") -> - warnWithFix id 2266 "Use || for logical OR. Single | will pipe." $ - fixWith [replaceEnd id params 0 "|"] - _ -> return () - - checkAnds id t = - case t of - T_AndIf _ _ rhs -> checkAnds id rhs - T_OrIf _ _ rhs -> checkAnds id rhs - T_Pipeline _ _ list | not (null list) -> checkAnds id (last list) - cmd -> when (isTestCommand cmd) $ - errWithFix id 2265 "Use && for logical AND. Single & will background and return true." $ - (fixWith [replaceEnd id params 0 "&"]) - - -prop_checkComparisonWithLeadingX1 = verify checkComparisonWithLeadingX "[ x$foo = xlol ]" -prop_checkComparisonWithLeadingX2 = verify checkComparisonWithLeadingX "test x$foo = xlol" -prop_checkComparisonWithLeadingX3 = verifyNot checkComparisonWithLeadingX "[ $foo = xbar ]" -prop_checkComparisonWithLeadingX4 = verifyNot checkComparisonWithLeadingX "test $foo = xbar" -prop_checkComparisonWithLeadingX5 = verify checkComparisonWithLeadingX "[ \"x$foo\" = 'xlol' ]" -prop_checkComparisonWithLeadingX6 = verify checkComparisonWithLeadingX "[ x\"$foo\" = x'lol' ]" -prop_checkComparisonWithLeadingX7 = verify checkComparisonWithLeadingX "[ X$foo != Xbar ]" -checkComparisonWithLeadingX params t = - case t of - TC_Binary id typ op lhs rhs - | op `elem` ["=", "==", "!="] -> - check lhs rhs - T_SimpleCommand _ _ [cmd, lhs, op, rhs] - | getLiteralString cmd == Just "test" && - getLiteralString op `elem` [Just "=", Just "==", Just "!="] -> - check lhs rhs - _ -> return () - where - msg = "Avoid x-prefix in comparisons as it no longer serves a purpose." - check lhs rhs = sequence_ $ do - l <- fixLeadingX lhs - r <- fixLeadingX rhs - return $ styleWithFix (getId lhs) 2268 msg $ fixWith [l, r] - - fixLeadingX token = - case getWordParts token of - T_Literal id (c:_):_ | toLower c == 'x' -> - case token of - -- The side is a single, unquoted x or X, so we have to quote - T_NormalWord _ [T_Literal id [c]] -> - return $ replaceStart id params 1 "\"\"" - -- Otherwise we can just delete it - _ -> return $ replaceStart id params 1 "" - T_SingleQuoted id (c:rest):_ | toLower c == 'x' -> - -- Replace the single quote and the character x or X - return $ replaceStart id params 2 "'" - _ -> Nothing - - -prop_checkAssignToSelf1 = verify checkAssignToSelf "x=$x" -prop_checkAssignToSelf2 = verify checkAssignToSelf "x=${x}" -prop_checkAssignToSelf3 = verify checkAssignToSelf "x=\"$x\"" -prop_checkAssignToSelf4 = verifyNot checkAssignToSelf "x=$x mycmd" -checkAssignToSelf _ t = - case t of - T_SimpleCommand _ vars [] -> mapM_ check vars - _ -> return () - where - check t = - case t of - T_Assignment id Assign name [] t -> - case getWordParts t of - [T_DollarBraced _ _ b] -> do - when (Just name == getLiteralString b) $ - msg id - _ -> return () - _ -> return () - msg id = info id 2269 "This variable is assigned to itself, so the assignment does nothing." - - -prop_checkEqualsInCommand1a = verifyCodes checkEqualsInCommand [2277] "#!/bin/bash\n0='foo'" -prop_checkEqualsInCommand2a = verifyCodes checkEqualsInCommand [2278] "#!/bin/ksh \n$0='foo'" -prop_checkEqualsInCommand3a = verifyCodes checkEqualsInCommand [2279] "#!/bin/dash\n${0}='foo'" -prop_checkEqualsInCommand4a = verifyCodes checkEqualsInCommand [2280] "#!/bin/sh \n0='foo'" - -prop_checkEqualsInCommand1b = verifyCodes checkEqualsInCommand [2270] "1='foo'" -prop_checkEqualsInCommand2b = verifyCodes checkEqualsInCommand [2270] "${2}='foo'" - -prop_checkEqualsInCommand1c = verifyCodes checkEqualsInCommand [2271] "var$((n+1))=value" -prop_checkEqualsInCommand2c = verifyCodes checkEqualsInCommand [2271] "var${x}=value" -prop_checkEqualsInCommand3c = verifyCodes checkEqualsInCommand [2271] "var$((cmd))x='foo'" -prop_checkEqualsInCommand4c = verifyCodes checkEqualsInCommand [2271] "$(cmd)='foo'" - -prop_checkEqualsInCommand1d = verifyCodes checkEqualsInCommand [2273] "=======" -prop_checkEqualsInCommand2d = verifyCodes checkEqualsInCommand [2274] "======= Here =======" -prop_checkEqualsInCommand3d = verifyCodes checkEqualsInCommand [2275] "foo\n=42" - -prop_checkEqualsInCommand1e = verifyCodes checkEqualsInCommand [] "--foo=bar" -prop_checkEqualsInCommand2e = verifyCodes checkEqualsInCommand [] "$(cmd)'=foo'" -prop_checkEqualsInCommand3e = verifyCodes checkEqualsInCommand [2276] "var${x}/=value" -prop_checkEqualsInCommand4e = verifyCodes checkEqualsInCommand [2276] "${}=value" -prop_checkEqualsInCommand5e = verifyCodes checkEqualsInCommand [2276] "${#x}=value" - -prop_checkEqualsInCommand1f = verifyCodes checkEqualsInCommand [2281] "$var=foo" -prop_checkEqualsInCommand2f = verifyCodes checkEqualsInCommand [2281] "$a=$b" -prop_checkEqualsInCommand3f = verifyCodes checkEqualsInCommand [2281] "${var}=foo" -prop_checkEqualsInCommand4f = verifyCodes checkEqualsInCommand [2281] "${var[42]}=foo" -prop_checkEqualsInCommand5f = verifyCodes checkEqualsInCommand [2281] "$var+=foo" - -prop_checkEqualsInCommand1g = verifyCodes checkEqualsInCommand [2282] "411toppm=true" - -checkEqualsInCommand params originalToken = - case originalToken of - T_SimpleCommand _ _ (word:_) -> check word - _ -> return () - where - hasEquals t = - case t of - T_Literal _ s -> '=' `elem` s - _ -> False - - check t@(T_NormalWord _ list) | any hasEquals list = - case break hasEquals list of - (leading, (eq:_)) -> msg t (stripSinglePlus leading) eq - _ -> return () - check _ = return () - - -- This is a workaround for the parser adding + and = as separate literals - stripSinglePlus l = - case reverse l of - (T_Literal _ "+"):rest -> reverse rest - _ -> l - - positionalAssignmentRe = mkRegex "^[0-9][0-9]?=" - positionalMsg id = - err id 2270 "To assign positional parameters, use 'set -- first second ..' (or use [ ] to compare)." - indirectionMsg id = - err id 2271 "For indirection, use arrays, declare \"var$n=value\", or (for sh) read/eval." - badComparisonMsg id = - err id 2272 "Command name contains ==. For comparison, use [ \"$var\" = value ]." - conflictMarkerMsg id = - err id 2273 "Sequence of ===s found. Merge conflict or intended as a commented border?" - borderMsg id = - err id 2274 "Command name starts with ===. Intended as a commented border?" - prefixMsg id = - err id 2275 "Command name starts with =. Bad line break?" - genericMsg id = - err id 2276 "This is interpreted as a command name containing '='. Bad assignment or comparison?" - assign0Msg id bashfix = - case shellType params of - Bash -> errWithFix id 2277 "Use BASH_ARGV0 to assign to $0 in bash (or use [ ] to compare)." bashfix - Ksh -> err id 2278 "$0 can't be assigned in Ksh (but it does reflect the current function)." - Dash -> err id 2279 "$0 can't be assigned in Dash. This becomes a command name." - BusyboxSh -> err id 2279 "$0 can't be assigned in Busybox Ash. This becomes a command name." - _ -> err id 2280 "$0 can't be assigned this way, and there is no portable alternative." - leadingNumberMsg id = - err id 2282 "Variable names can't start with numbers, so this is interpreted as a command." - - isExpansion t = - case t of - T_Arithmetic {} -> True - _ -> isQuoteableExpansion t - - isConflictMarker cmd = fromMaybe False $ do - str <- getUnquotedLiteral cmd - guard $ all (== '=') str - guard $ length str >= 4 && length str <= 12 -- Git uses 7 but who knows - return True - - mayBeVariableName l = fromMaybe False $ do - guard . not $ any isQuotes l - guard . not $ any willBecomeMultipleArgs l - str <- getLiteralStringExt (\_ -> Just "x") (T_NormalWord (Id 0) l) - return $ isVariableName str - - isLeadingNumberVar s = - case takeWhile (/= '=') s of - lead@(x:_) -> isDigit x && all isVariableChar lead && not (all isDigit lead) - [] -> False - - msg cmd leading (T_Literal litId s) = do - -- There are many different cases, and the order of the branches matter. - case leading of - -- --foo=42 - [] | "-" `isPrefixOf` s -> -- There's SC2215 for these - return () - - -- ======Hello====== - [] | "=" `isPrefixOf` s -> - case originalToken of - T_SimpleCommand _ [] [word] | isConflictMarker word -> - conflictMarkerMsg (getId originalToken) - _ | "===" `isPrefixOf` s -> borderMsg (getId originalToken) - _ -> prefixMsg (getId cmd) - - -- '$var==42' - _ | "==" `isInfixOf` s -> - badComparisonMsg (getId cmd) - - -- '${foo[x]}=42' and '$foo=42' - [T_DollarBraced id braced l] | "=" `isPrefixOf` s -> do - let variableStr = concat $ oversimplify l - let variableReference = getBracedReference variableStr - let variableModifier = getBracedModifier variableStr - let isPlain = isVariableName variableStr - let isPositional = all isDigit variableStr - - let isArray = variableReference /= "" - && "[" `isPrefixOf` variableModifier - && "]" `isSuffixOf` variableModifier - - case () of - -- '$foo=bar' should already have caused a parse-time SC1066 - -- _ | not braced && isPlain -> - -- return () - - _ | variableStr == "" -> -- Don't try to fix ${}=foo - genericMsg (getId cmd) - - -- '$#=42' or '${#var}=42' - _ | "#" `isPrefixOf` variableStr -> - genericMsg (getId cmd) - - -- '${0}=42' - _ | variableStr == "0" -> - assign0Msg id $ fixWith [replaceToken id params "BASH_ARGV0"] - - -- '$2=2' - _ | isPositional -> - positionalMsg id - - _ | isArray || isPlain -> - errWithFix id 2281 - ("Don't use " ++ (if braced then "${}" else "$") ++ " on the left side of assignments.") $ - fixWith $ - if braced - then [ replaceStart id params 2 "", replaceEnd id params 1 "" ] - else [ replaceStart id params 1 "" ] - - _ -> indirectionMsg id - - -- 2=42 - [] | s `matches` positionalAssignmentRe -> - if "0=" `isPrefixOf` s - then - assign0Msg litId $ fixWith [replaceStart litId params 1 "BASH_ARGV0"] - else - positionalMsg litId - - -- 9foo=42 - [] | isLeadingNumberVar s -> - leadingNumberMsg (getId cmd) - - -- var${foo}x=42 - (_:_) | mayBeVariableName leading && (all isVariableChar $ takeWhile (/= '=') s) -> - indirectionMsg (getId cmd) - - _ -> genericMsg (getId cmd) - - -prop_checkSecondArgIsComparison1 = verify checkSecondArgIsComparison "foo = $bar" -prop_checkSecondArgIsComparison2 = verify checkSecondArgIsComparison "$foo = $bar" -prop_checkSecondArgIsComparison3 = verify checkSecondArgIsComparison "2f == $bar" -prop_checkSecondArgIsComparison4 = verify checkSecondArgIsComparison "'var' =$bar" -prop_checkSecondArgIsComparison5 = verify checkSecondArgIsComparison "foo ='$bar'" -prop_checkSecondArgIsComparison6 = verify checkSecondArgIsComparison "$foo =$bar" -prop_checkSecondArgIsComparison7 = verify checkSecondArgIsComparison "2f ==$bar" -prop_checkSecondArgIsComparison8 = verify checkSecondArgIsComparison "'var' =$bar" -prop_checkSecondArgIsComparison9 = verify checkSecondArgIsComparison "var += $(foo)" -prop_checkSecondArgIsComparison10 = verify checkSecondArgIsComparison "var +=$(foo)" -checkSecondArgIsComparison _ t = - case t of - T_SimpleCommand _ _ (lhs:arg:_) -> sequence_ $ do - argString <- getLeadingUnquotedString arg - case argString of - '=':'=':'=':'=':_ -> Nothing -- Don't warn about `echo ======` and such - '+':'=':_ -> - return $ err (headId t) 2285 $ - "Remove spaces around += to assign (or quote '+=' if literal)." - '=':'=':_ -> - return $ err (getId t) 2284 $ - "Use [ x = y ] to compare values (or quote '==' if literal)." - '=':_ -> - return $ err (headId arg) 2283 $ - "Remove spaces around = to assign (or use [ ] to compare, or quote '=' if literal)." - _ -> Nothing - _ -> return () - where - -- We don't pinpoint exactly, but this helps cases like foo =$bar - headId t = - case t of - T_NormalWord _ (x:_) -> getId x - _ -> getId t - - -prop_checkCommandWithTrailingSymbol1 = verify checkCommandWithTrailingSymbol "/" -prop_checkCommandWithTrailingSymbol2 = verify checkCommandWithTrailingSymbol "/foo/ bar/baz" -prop_checkCommandWithTrailingSymbol3 = verify checkCommandWithTrailingSymbol "/" -prop_checkCommandWithTrailingSymbol4 = verifyNot checkCommandWithTrailingSymbol "/*" -prop_checkCommandWithTrailingSymbol5 = verifyNot checkCommandWithTrailingSymbol "$foo/$bar" -prop_checkCommandWithTrailingSymbol6 = verify checkCommandWithTrailingSymbol "foo, bar" -prop_checkCommandWithTrailingSymbol7 = verifyNot checkCommandWithTrailingSymbol ". foo.sh" -prop_checkCommandWithTrailingSymbol8 = verifyNot checkCommandWithTrailingSymbol ": foo" -prop_checkCommandWithTrailingSymbol9 = verifyNot checkCommandWithTrailingSymbol "/usr/bin/python[23] file.py" - -checkCommandWithTrailingSymbol _ t = - case t of - T_SimpleCommand _ _ (cmd:_) -> - let str = getLiteralStringDef "x" cmd - last = lastOrDefault 'x' str - in - case str of - "." -> return () -- The . command - ":" -> return () -- The : command - " " -> return () -- Probably caught by SC1101 - "//" -> return () -- Probably caught by SC1127 - "" -> err (getId cmd) 2286 "This empty string is interpreted as a command name. Double check syntax (or use 'true' as a no-op)." - _ | last == '/' -> err (getId cmd) 2287 "This is interpreted as a command name ending with '/'. Double check syntax." - _ | last `elem` "\\.,([{<>}])#\"\'% " -> warn (getId cmd) 2288 ("This is interpreted as a command name ending with " ++ (format last) ++ ". Double check syntax.") - _ | '\t' `elem` str -> err (getId cmd) 2289 "This is interpreted as a command name containing a tab. Double check syntax." - _ | '\n' `elem` str -> err (getId cmd) 2289 "This is interpreted as a command name containing a linefeed. Double check syntax." - _ -> return () - _ -> return () - where - format x = - case x of - ' ' -> "space" - '\'' -> "apostrophe" - '\"' -> "doublequote" - x -> '\'' : x : "\'" - - -prop_checkRequireDoubleBracket1 = verifyTree checkRequireDoubleBracket "[ -x foo ]" -prop_checkRequireDoubleBracket2 = verifyTree checkRequireDoubleBracket "[ foo -o bar ]" -prop_checkRequireDoubleBracket3 = verifyNotTree checkRequireDoubleBracket "#!/bin/sh\n[ -x foo ]" -prop_checkRequireDoubleBracket4 = verifyNotTree checkRequireDoubleBracket "[[ -x foo ]]" -checkRequireDoubleBracket params = - if (shellType params) `elem` [Bash, Ksh, BusyboxSh] - then nodeChecksToTreeCheck [check] params - else const [] - where - check _ t = case t of - T_Condition id SingleBracket _ -> - styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh/Busybox." (fixFor t) - _ -> return () - - fixFor t = fixWith $ - if isSimple t - then - [ - replaceStart (getId t) params 0 "[", - replaceEnd (getId t) params 0 "]" - ] - else [] - - -- We don't tag operators like < and -o well enough to replace them, - -- so just handle the simple cases. - isSimple t = case t of - T_Condition _ _ s -> isSimple s - TC_Binary _ _ op _ _ -> not $ any (\x -> x `elem` op) "<>" - TC_Unary {} -> True - TC_Nullary {} -> True - _ -> False - - -prop_checkUnquotedParameterExpansionPattern1 = verify checkUnquotedParameterExpansionPattern "echo \"${var#$x}\"" -prop_checkUnquotedParameterExpansionPattern2 = verify checkUnquotedParameterExpansionPattern "echo \"${var%%$(x)}\"" -prop_checkUnquotedParameterExpansionPattern3 = verifyNot checkUnquotedParameterExpansionPattern "echo \"${var[#$x]}\"" -prop_checkUnquotedParameterExpansionPattern4 = verifyNot checkUnquotedParameterExpansionPattern "echo \"${var%\"$x\"}\"" - -checkUnquotedParameterExpansionPattern params x = - case x of - T_DollarBraced _ True word@(T_NormalWord _ (T_Literal _ s : rest@(_:_))) -> do - let modifier = getBracedModifier $ concat $ oversimplify word - when ("%" `isPrefixOf` modifier || "#" `isPrefixOf` modifier) $ - mapM_ check rest - _ -> return () - where - check t = - case t of - T_DollarBraced {} -> inform t - T_DollarExpansion {} -> inform t - T_Backticked {} -> inform t - _ -> return () - - inform t = - infoWithFix (getId t) 2295 - "Expansions inside ${..} need to be quoted separately, otherwise they match as patterns." $ - surroundWith (getId t) params "\"" - - -prop_checkArrayValueUsedAsIndex1 = verifyTree checkArrayValueUsedAsIndex "for i in ${arr[@]}; do echo ${arr[i]}; done" -prop_checkArrayValueUsedAsIndex2 = verifyTree checkArrayValueUsedAsIndex "for i in ${arr[@]}; do echo ${arr[$i]}; done" -prop_checkArrayValueUsedAsIndex3 = verifyTree checkArrayValueUsedAsIndex "for i in ${arr[@]}; do echo $((arr[i])); done" -prop_checkArrayValueUsedAsIndex4 = verifyTree checkArrayValueUsedAsIndex "for i in ${arr1[@]} ${arr2[@]}; do echo ${arr1[$i]}; done" -prop_checkArrayValueUsedAsIndex5 = verifyTree checkArrayValueUsedAsIndex "for i in ${arr1[@]} ${arr2[@]}; do echo ${arr2[$i]}; done" -prop_checkArrayValueUsedAsIndex7 = verifyNotTree checkArrayValueUsedAsIndex "for i in ${arr[@]}; do echo ${arr[K]}; done" -prop_checkArrayValueUsedAsIndex8 = verifyNotTree checkArrayValueUsedAsIndex "for i in ${arr[@]}; do i=42; echo ${arr[i]}; done" -prop_checkArrayValueUsedAsIndex9 = verifyNotTree checkArrayValueUsedAsIndex "for i in ${arr[@]}; do echo ${arr2[i]}; done" - -checkArrayValueUsedAsIndex params _ = - doVariableFlowAnalysis read write Map.empty (variableFlow params) - where - write loop@T_ForIn {} _ name (DataString (SourceFrom words)) = do - modify $ Map.insert name (loop, mapMaybe f words) - return [] - where - f x = do - name <- getArrayName x - return (x, name) - - write _ _ name _ = do - modify $ Map.delete name - return [] - - read _ t name = do - varMap <- get - return $ fromMaybe [] $ do - (loop, arrays) <- Map.lookup name varMap - (arrayRef, arrayName) <- getArrayIfUsedAsIndex name t - -- Is this one of the 'for' arrays? - (loopWord, _) <- find ((==arrayName) . snd) arrays - -- Are we still in this loop? - let loopId = getId loop - guard $ any (\t -> loopId == getId t) (getPath parents t) - return [ - makeComment WarningC (getId loopWord) 2302 "This loops over values. To loop over keys, use \"${!array[@]}\".", - makeComment WarningC (getId arrayRef) 2303 $ (e4m name) ++ " is an array value, not a key. Use directly or loop over keys instead." - ] - - parents = parentMap params - - getArrayName :: Token -> Maybe String - getArrayName t = do - [T_DollarBraced _ _ l] <- return $ getWordParts t - let str = concat $ oversimplify l - guard $ getBracedModifier str == "[@]" && not ("!" `isPrefixOf` str) - return $ getBracedReference str - - -- This is much uglier than it should be - getArrayIfUsedAsIndex :: String -> Token -> Maybe (Token, String) - getArrayIfUsedAsIndex name t = - case t of - T_DollarBraced _ _ list -> do - let ref = getBracedReference $ concat $ oversimplify list - guard $ ref == name - -- We found a $name. Look up the chain to see if it's ${arr[$name]} - list@T_NormalWord {} <- Map.lookup (getId t) parents - (T_DollarBraced _ _ parentList) <- Map.lookup (getId list) parents - (T_Literal _ head : index : T_Literal _ tail : _) <- return $ getWordParts parentList - let str = concat $ oversimplify list - let modifier = getBracedModifier str - guard $ getId index == getId t - guard $ "[${VAR}]" `isPrefixOf` modifier - return (t, getBracedReference str) - - T_NormalWord wordId list -> do - -- We found just name. Check if it's part of ${something[name]} - parent@(T_DollarBraced _ _ parentList) <- Map.lookup wordId parents - let str = concat $ oversimplify t - let modifier = getBracedModifier str - guard $ ("[" ++ name ++ "]") `isPrefixOf` modifier - return (parent, getBracedReference str) - - TA_Variable indexId ref [] -> do - -- We found arithmetic name. See if it's part of arithmetic arr[name] - guard $ ref == name - (TA_Sequence seqId [element]) <- Map.lookup indexId parents - guard $ getId element == indexId - parent@(TA_Variable arrayId arrayName [element]) <- Map.lookup seqId parents - guard $ getId element == seqId - return (parent, arrayName) - - _ -> Nothing - -prop_checkSetESuppressed1 = verifyTree checkSetESuppressed "set -e; f(){ :; }; x=$(f)" -prop_checkSetESuppressed2 = verifyNotTree checkSetESuppressed "f(){ :; }; x=$(f)" -prop_checkSetESuppressed3 = verifyNotTree checkSetESuppressed "set -e; f(){ :; }; x=$(set -e; f)" -prop_checkSetESuppressed4 = verifyTree checkSetESuppressed "set -e; f(){ :; }; baz=$(set -e; f) || :" -prop_checkSetESuppressed5 = verifyNotTree checkSetESuppressed "set -e; f(){ :; }; baz=$(echo \"\") || :" -prop_checkSetESuppressed6 = verifyTree checkSetESuppressed "set -e; f(){ :; }; f && echo" -prop_checkSetESuppressed7 = verifyTree checkSetESuppressed "set -e; f(){ :; }; f || echo" -prop_checkSetESuppressed8 = verifyNotTree checkSetESuppressed "set -e; f(){ :; }; echo && f" -prop_checkSetESuppressed9 = verifyNotTree checkSetESuppressed "set -e; f(){ :; }; echo || f" -prop_checkSetESuppressed10 = verifyTree checkSetESuppressed "set -e; f(){ :; }; ! f" -prop_checkSetESuppressed11 = verifyTree checkSetESuppressed "set -e; f(){ :; }; if f; then :; fi" -prop_checkSetESuppressed12 = verifyTree checkSetESuppressed "set -e; f(){ :; }; if set -e; f; then :; fi" -prop_checkSetESuppressed13 = verifyTree checkSetESuppressed "set -e; f(){ :; }; while f; do :; done" -prop_checkSetESuppressed14 = verifyTree checkSetESuppressed "set -e; f(){ :; }; while set -e; f; do :; done" -prop_checkSetESuppressed15 = verifyTree checkSetESuppressed "set -e; f(){ :; }; until f; do :; done" -prop_checkSetESuppressed16 = verifyTree checkSetESuppressed "set -e; f(){ :; }; until set -e; f; do :; done" -prop_checkSetESuppressed17 = verifyNotTree checkSetESuppressed "set -e; f(){ :; }; g(){ :; }; g f" -prop_checkSetESuppressed18 = verifyNotTree checkSetESuppressed "set -e; shopt -s inherit_errexit; f(){ :; }; x=$(f)" -prop_checkSetESuppressed19 = verifyNotTree checkSetESuppressed "set -e; set -o posix; f(){ :; }; x=$(f)" -checkSetESuppressed params t = - if hasSetE params then runNodeAnalysis checkNode params t else [] - where - checkNode _ (T_SimpleCommand _ _ (cmd:_)) = when (isFunction cmd) (checkCmd cmd) - checkNode _ _ = return () - - functions_ = functions t - - isFunction cmd = isJust $ do - literalArg <- getUnquotedLiteral cmd - Map.lookup literalArg functions_ - - checkCmd cmd = go $ NE.toList $ getPath (parentMap params) cmd - where - go (child:parent:rest) = do - case parent of - T_Banged _ condition | child `isIn` [condition] -> informConditional "a ! condition" cmd - T_AndIf _ condition _ | child `isIn` [condition] -> informConditional "an && condition" cmd - T_OrIf _ condition _ | child `isIn` [condition] -> informConditional "an || condition" cmd - T_IfExpression _ condition _ | child `isIn` concatMap fst condition -> informConditional "an 'if' condition" cmd - T_UntilExpression _ condition _ | child `isIn` condition -> informConditional "an 'until' condition" cmd - T_WhileExpression _ condition _ | child `isIn` condition -> informConditional "a 'while' condition" cmd - T_DollarExpansion {} | not $ errExitEnabled parent -> informUninherited cmd - T_Backticked {} | not $ errExitEnabled parent -> informUninherited cmd - _ -> return () - go (parent:rest) - go _ = return () - - informConditional condType t = - info (getId t) 2310 ( - "This function is invoked in " ++ condType ++ " so set -e " ++ - "will be disabled. Invoke separately if failures should " ++ - "cause the script to exit.") - informUninherited t = - info (getId t) 2311 ( - "Bash implicitly disabled set -e for this function " ++ - "invocation because it's inside a command substitution. " ++ - "Add set -e; before it or enable inherit_errexit.") - errExitEnabled t = hasInheritErrexit params || containsSetE t - isIn t cmds = getId t `elem` map getId cmds - - -prop_checkExtraMaskedReturns1 = verifyTree checkExtraMaskedReturns "cat < <(ls)" -prop_checkExtraMaskedReturns2 = verifyTree checkExtraMaskedReturns "read -ra arr <(ls)" -prop_checkExtraMaskedReturns3 = verifyTree checkExtraMaskedReturns "ls >(cat)" -prop_checkExtraMaskedReturns4 = verifyTree checkExtraMaskedReturns "false | true" -prop_checkExtraMaskedReturns5 = verifyNotTree checkExtraMaskedReturns "set -o pipefail; false | true" -prop_checkExtraMaskedReturns6 = verifyNotTree checkExtraMaskedReturns "false | true || true" -prop_checkExtraMaskedReturns7 = verifyTree checkExtraMaskedReturns "true $(false)" -prop_checkExtraMaskedReturns8 = verifyTree checkExtraMaskedReturns "x=$(false)$(true)" -prop_checkExtraMaskedReturns9 = verifyNotTree checkExtraMaskedReturns "x=$(false)true" -prop_checkExtraMaskedReturns10 = verifyTree checkExtraMaskedReturns "x=`false``false`" -prop_checkExtraMaskedReturns11 = verifyTree checkExtraMaskedReturns "x=\"$(false)$(true)\"" -prop_checkExtraMaskedReturns12 = verifyTree checkExtraMaskedReturns "x=\"$(false)\"\"$(true)\"" -prop_checkExtraMaskedReturns13 = verifyTree checkExtraMaskedReturns "true <<<$(false)" -prop_checkExtraMaskedReturns14 = verifyNotTree checkExtraMaskedReturns "echo asdf | false" -prop_checkExtraMaskedReturns15 = verifyNotTree checkExtraMaskedReturns "readonly x=$(false)" -prop_checkExtraMaskedReturns16 = verifyTree checkExtraMaskedReturns "readarray -t files < <(ls)" -prop_checkExtraMaskedReturns17 = verifyNotTree checkExtraMaskedReturns "x=( $(false) false )" -prop_checkExtraMaskedReturns18 = verifyTree checkExtraMaskedReturns "x=( $(false) $(false) )" -prop_checkExtraMaskedReturns19 = verifyNotTree checkExtraMaskedReturns "x=( $(false) [4]=false )" -prop_checkExtraMaskedReturns20 = verifyTree checkExtraMaskedReturns "x=( $(false) [4]=$(false) )" -prop_checkExtraMaskedReturns21 = verifyTree checkExtraMaskedReturns "cat << foo\n $(false)\nfoo" -prop_checkExtraMaskedReturns22 = verifyTree checkExtraMaskedReturns "[[ $(false) ]]" -prop_checkExtraMaskedReturns23 = verifyNotTree checkExtraMaskedReturns "x=$(false) y=z" -prop_checkExtraMaskedReturns24 = verifyNotTree checkExtraMaskedReturns "x=$(( $(date +%s) ))" -prop_checkExtraMaskedReturns25 = verifyTree checkExtraMaskedReturns "echo $(( $(date +%s) ))" -prop_checkExtraMaskedReturns26 = verifyNotTree checkExtraMaskedReturns "x=( $(false) )" -prop_checkExtraMaskedReturns27 = verifyTree checkExtraMaskedReturns "x=$(false) false" -prop_checkExtraMaskedReturns28 = verifyTree checkExtraMaskedReturns "x=$(false) y=$(false)" -prop_checkExtraMaskedReturns29 = verifyNotTree checkExtraMaskedReturns "false < <(set -e)" -prop_checkExtraMaskedReturns30 = verifyNotTree checkExtraMaskedReturns "false < <(shopt -s cdspell)" -prop_checkExtraMaskedReturns31 = verifyNotTree checkExtraMaskedReturns "false < <(dirname \"${BASH_SOURCE[0]}\")" -prop_checkExtraMaskedReturns32 = verifyNotTree checkExtraMaskedReturns "false < <(basename \"${BASH_SOURCE[0]}\")" -prop_checkExtraMaskedReturns33 = verifyNotTree checkExtraMaskedReturns "{ false || true; } | true" -prop_checkExtraMaskedReturns34 = verifyNotTree checkExtraMaskedReturns "{ false || :; } | true" -prop_checkExtraMaskedReturns35 = verifyTree checkExtraMaskedReturns "f() { local -r x=$(false); }" -prop_checkExtraMaskedReturns36 = verifyNotTree checkExtraMaskedReturns "time false" -prop_checkExtraMaskedReturns37 = verifyNotTree checkExtraMaskedReturns "time $(time false)" -prop_checkExtraMaskedReturns38 = verifyTree checkExtraMaskedReturns "x=$(time time time false) time $(time false)" - -checkExtraMaskedReturns params t = - runNodeAnalysis findMaskingNodes params (removeTransparentCommands t) - where - findMaskingNodes _ (T_Arithmetic _ list) = findMaskedNodesInList [list] - findMaskingNodes _ (T_Array _ list) = findMaskedNodesInList $ allButLastSimpleCommands list - findMaskingNodes _ (T_Condition _ _ condition) = findMaskedNodesInList [condition] - findMaskingNodes _ (T_DoubleQuoted _ list) = findMaskedNodesInList $ allButLastSimpleCommands list - findMaskingNodes _ (T_HereDoc _ _ _ _ list) = findMaskedNodesInList list - findMaskingNodes _ (T_HereString _ word) = findMaskedNodesInList [word] - findMaskingNodes _ (T_NormalWord _ parts) = findMaskedNodesInList $ allButLastSimpleCommands parts - findMaskingNodes _ (T_Pipeline _ _ cmds) | not (hasPipefail params) = findMaskedNodesInList $ allButLastSimpleCommands cmds - findMaskingNodes _ (T_ProcSub _ _ list) = findMaskedNodesInList list - findMaskingNodes _ (T_SimpleCommand _ assigns (_:args)) = findMaskedNodesInList $ assigns ++ args - findMaskingNodes _ (T_SimpleCommand _ assigns []) = findMaskedNodesInList $ allButLastSimpleCommands assigns - findMaskingNodes _ _ = return () - - findMaskedNodesInList = mapM_ (doAnalysis findMaskedNodes) - - isMaskedNode t = not (isHarmlessCommand t || isCheckedElsewhere t || isMaskDeliberate t) - findMaskedNodes t@(T_SimpleCommand _ _ (_:_)) = when (isMaskedNode t) $ inform t - findMaskedNodes t@T_Condition {} = when (isMaskedNode t) $ inform t - findMaskedNodes _ = return () - - containsSimpleCommand t = isNothing $ doAnalysis go t - where - go t = case t of - T_SimpleCommand {} -> fail "" - _ -> return () - - allButLastSimpleCommands cmds = - if null simpleCommands then [] else init simpleCommands - where - simpleCommands = filter containsSimpleCommand cmds - - removeTransparentCommands t = - doTransform go t - where - go cmd@(T_SimpleCommand id assigns (_:args)) | isTransparentCommand cmd - = T_SimpleCommand id assigns args - go t = t - - inform t = info (getId t) 2312 ("Consider invoking this command " - ++ "separately to avoid masking its return value (or use '|| true' " - ++ "to ignore).") - - isMaskDeliberate t = any isOrIf $ NE.init $ parents params t - where - isOrIf (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd])) - = getCommandBasename cmd `elem` [Just "true", Just ":"] - isOrIf _ = False - - isCheckedElsewhere t = any isDeclaringCommand $ NE.tail $ parents params t - where - isDeclaringCommand t = fromMaybe False $ do - cmd <- getCommand t - basename <- getCommandBasename cmd - return $ - case basename of - -- local -r x=$(false) is intentionally ignored for SC2155 - "local" | "r" `elem` (map snd $ getAllFlags cmd) -> False - _ -> basename `elem` declaringCommands - - isHarmlessCommand t = fromMaybe False $ do - basename <- getCommandBasename t - return $ basename `elem` [ - "echo" - ,"basename" - ,"dirname" - ,"printf" - ,"set" - ,"shopt" - ] - - isTransparentCommand t = getCommandBasename t == Just "time" - - --- hard error on negated command that is not last -prop_checkBatsTestDoesNotUseNegation1 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! true; false; }" -prop_checkBatsTestDoesNotUseNegation2 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]]; false; }" -prop_checkBatsTestDoesNotUseNegation3 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ]; false; }" --- acceptable formats: --- using run -prop_checkBatsTestDoesNotUseNegation4 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { run ! true; }" --- using || false -prop_checkBatsTestDoesNotUseNegation5 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]] || false; }" -prop_checkBatsTestDoesNotUseNegation6 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ] || false; }" --- only style warning when last command -prop_checkBatsTestDoesNotUseNegation7 = verifyCodes checkBatsTestDoesNotUseNegation [2314] "#!/usr/bin/env/bats\n@test \"name\" { ! true; }" -prop_checkBatsTestDoesNotUseNegation8 = verifyCodes checkBatsTestDoesNotUseNegation [2315] "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]]; }" -prop_checkBatsTestDoesNotUseNegation9 = verifyCodes checkBatsTestDoesNotUseNegation [2315] "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ]; }" - -checkBatsTestDoesNotUseNegation params t = - case t of - T_BatsTest _ _ (T_BraceGroup _ commands) -> mapM_ (check commands) commands - _ -> return () - where - check commands t = - case t of - T_Banged id (T_Pipeline _ _ [T_Redirecting _ _ (T_Condition idCondition _ _)]) -> - if t `isLastOf` commands - then style id 2315 "In Bats, ! will not fail the test if it is not the last command anymore. Fold the `!` into the conditional!" - else err id 2315 "In Bats, ! does not cause a test failure. Fold the `!` into the conditional!" - - T_Banged id cmd -> if t `isLastOf` commands - then styleWithFix id 2314 "In Bats, ! will not fail the test if it is not the last command anymore. Use `run ! ` (on Bats >= 1.5.0) instead." - (fixWith [replaceStart id params 0 "run "]) - else errWithFix id 2314 "In Bats, ! does not cause a test failure. Use 'run ! ' (on Bats >= 1.5.0) instead." - (fixWith [replaceStart id params 0 "run "]) - _ -> return () - isLastOf t commands = - case commands of - [x] -> x == t - x:rest -> isLastOf t rest - [] -> False - - -prop_checkCommandIsUnreachable1 = verify checkCommandIsUnreachable "foo; bar; exit; baz" -prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit; }; foo; bar; die; baz" -prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz" -prop_checkCommandIsUnreachable4 = verifyNot checkCommandIsUnreachable "f() { foo; }; # Maybe sourced" -prop_checkCommandIsUnreachable5 = verify checkCommandIsUnreachable "f() { foo; }; exit # Not sourced" -checkCommandIsUnreachable params t = - case t of - T_Pipeline {} -> sequence_ $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga (getId t) - guard . not $ CF.stateIsReachable state - guard . not $ isSourced params t - guard . not $ any (\t -> isUnreachable t || isUnreachableFunction t) $ NE.drop 1 $ getPath (parentMap params) t - return $ info (getId t) 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." - T_Function id _ _ _ _ -> - when (isUnreachableFunction t - && (not . any isUnreachableFunction . NE.drop 1 $ getPath (parentMap params) t) - && (not $ isSourced params t)) $ - info id 2329 "This function is never invoked. Check usage (or ignored if invoked indirectly)." - _ -> return () - where - isUnreachableFunction :: Token -> Bool - isUnreachableFunction f = - case f of - T_Function id _ _ _ t -> isUnreachable t - _ -> False - isUnreachable t = fromMaybe False $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga (getId t) - return . not $ CF.stateIsReachable state - - -prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]" -prop_checkOverwrittenExitCode2 = verifyNot checkOverwrittenExitCode "x; [ $? -eq 1 ]" -prop_checkOverwrittenExitCode3 = verify checkOverwrittenExitCode "x; echo \"Exit is $?\"; [ $? -eq 0 ]" -prop_checkOverwrittenExitCode4 = verifyNot checkOverwrittenExitCode "x; [ $? -eq 0 ] && echo Success" -prop_checkOverwrittenExitCode5 = verify checkOverwrittenExitCode "x; if [ $? -eq 0 ]; then var=$?; fi" -prop_checkOverwrittenExitCode6 = verify checkOverwrittenExitCode "x; [ $? -gt 0 ] && fail=$?" -prop_checkOverwrittenExitCode7 = verifyNot checkOverwrittenExitCode "[ 1 -eq 2 ]; status=$?" -prop_checkOverwrittenExitCode8 = verifyNot checkOverwrittenExitCode "[ 1 -eq 2 ]; exit $?" -checkOverwrittenExitCode params t = - case t of - T_DollarBraced id _ val | getLiteralString val == Just "?" -> check id - _ -> return () - where - check id = sequence_ $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id - let exitCodeIds = CF.exitCodes state - guard . not $ S.null exitCodeIds - - let idToToken = idMap params - exitCodeTokens <- traverse (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds - return $ do - when (all isCondition exitCodeTokens && not (usedUnconditionally cfga t exitCodeIds)) $ - warn id 2319 "This $? refers to a condition, not a command. Assign to a variable to avoid it being overwritten." - when (all isPrinting exitCodeTokens) $ - warn id 2320 "This $? refers to echo/printf, not a previous command. Assign to variable to avoid it being overwritten." - - isCondition t = - case t of - T_Condition {} -> True - T_SimpleCommand {} -> getCommandName t == Just "test" - _ -> False - - -- If we don't do anything based on the condition, assume we wanted the condition itself - -- This helps differentiate `x; [ $? -gt 0 ] && exit $?` vs `[ cond ]; exit $?` - usedUnconditionally cfga t testIds = - all (\c -> CF.doesPostDominate cfga (getId t) c) testIds - - isPrinting t = - case getCommandBasename t of - Just "echo" -> True - Just "printf" -> True - _ -> False - - -prop_checkUnnecessaryArithmeticExpansionIndex1 = verify checkUnnecessaryArithmeticExpansionIndex "a[$((1+1))]=n" -prop_checkUnnecessaryArithmeticExpansionIndex2 = verifyNot checkUnnecessaryArithmeticExpansionIndex "a[1+1]=n" -prop_checkUnnecessaryArithmeticExpansionIndex3 = verifyNot checkUnnecessaryArithmeticExpansionIndex "a[$(echo $((1+1)))]=n" -prop_checkUnnecessaryArithmeticExpansionIndex4 = verifyNot checkUnnecessaryArithmeticExpansionIndex "declare -A a; a[$((1+1))]=val" -checkUnnecessaryArithmeticExpansionIndex params t = - case t of - T_Assignment _ mode var [TA_Sequence _ [ TA_Expansion _ [expansion@(T_DollarArithmetic id _)]]] val -> - styleWithFix id 2321 "Array indices are already arithmetic contexts. Prefer removing the $(( and ))." $ fix id - _ -> return () - - where - fix id = - fixWith [ - replaceStart id params 3 "", -- Remove "$((" - replaceEnd id params 2 "" -- Remove "))" - ] - - -prop_checkUnnecessaryParens1 = verify checkUnnecessaryParens "echo $(( ((1+1)) ))" -prop_checkUnnecessaryParens2 = verify checkUnnecessaryParens "x[((1+1))+1]=1" -prop_checkUnnecessaryParens3 = verify checkUnnecessaryParens "x[(1+1)]=1" -prop_checkUnnecessaryParens4 = verify checkUnnecessaryParens "$(( (x) ))" -prop_checkUnnecessaryParens5 = verify checkUnnecessaryParens "(( (x) ))" -prop_checkUnnecessaryParens6 = verifyNot checkUnnecessaryParens "x[(1+1)+1]=1" -prop_checkUnnecessaryParens7 = verifyNot checkUnnecessaryParens "(( (1*1)+1 ))" -prop_checkUnnecessaryParens8 = verifyNot checkUnnecessaryParens "(( (1)+1 ))" -checkUnnecessaryParens params t = - case t of - T_DollarArithmetic _ t -> checkLeading "$(( (x) )) is the same as $(( x ))" t - T_ForArithmetic _ x y z _ -> mapM_ (checkLeading "for (((x); (y); (z))) is the same as for ((x; y; z))") [x,y,z] - T_Assignment _ _ _ [t] _ -> checkLeading "a[(x)] is the same as a[x]" t - T_Arithmetic _ t -> checkLeading "(( (x) )) is the same as (( x ))" t - TA_Parenthesis _ (TA_Sequence _ [ TA_Parenthesis id _ ]) -> - styleWithFix id 2322 "In arithmetic contexts, ((x)) is the same as (x). Prefer only one layer of parentheses." $ fix id - _ -> return () - where - - checkLeading str t = - case t of - TA_Sequence _ [TA_Parenthesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id - _ -> return () - - fix id = - fixWith [ - replaceStart id params 1 "", -- Remove "(" - replaceEnd id params 1 "" -- Remove ")" - ] - - -prop_checkPlusEqualsNumber1 = verify checkPlusEqualsNumber "x+=1" -prop_checkPlusEqualsNumber2 = verify checkPlusEqualsNumber "x+=42" -prop_checkPlusEqualsNumber3 = verifyNot checkPlusEqualsNumber "(( x += 1 ))" -prop_checkPlusEqualsNumber4 = verifyNot checkPlusEqualsNumber "declare -i x=0; x+=1" -prop_checkPlusEqualsNumber5 = verifyNot checkPlusEqualsNumber "x+='1'" -prop_checkPlusEqualsNumber6 = verifyNot checkPlusEqualsNumber "n=foo; x+=n" -prop_checkPlusEqualsNumber7 = verify checkPlusEqualsNumber "n=4; x+=n" -prop_checkPlusEqualsNumber8 = verify checkPlusEqualsNumber "n=4; x+=$n" -prop_checkPlusEqualsNumber9 = verifyNot checkPlusEqualsNumber "declare -ia var; var[x]+=1" -checkPlusEqualsNumber params t = - case t of - T_Assignment id Append var _ word -> sequence_ $ do - cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id - guard $ isNumber state word - guard . not $ fromMaybe False $ CF.variableMayBeDeclaredInteger state var - -- Recommend "typeset" because ksh does not have "declare". - return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), typeset -i var, or quote number to silence." - _ -> return () - - where - isNumber state word = - let - unquotedLiteral = getUnquotedLiteral word - isEmpty = unquotedLiteral == Just "" - isUnquotedNumber = not isEmpty && maybe False (all isDigit) unquotedLiteral - isNumericalVariableName = fromMaybe False $ do - str <- unquotedLiteral - CF.variableMayBeAssignedInteger state str - isNumericalVariableExpansion = - case word of - T_NormalWord _ [part] -> fromMaybe False $ do - str <- getUnmodifiedParameterExpansion part - CF.variableMayBeAssignedInteger state str - _ -> False - in - isUnquotedNumber || isNumericalVariableName || isNumericalVariableExpansion - - - -prop_checkExpansionWithRedirection1 = verify checkExpansionWithRedirection "var=$(foo > bar)" -prop_checkExpansionWithRedirection2 = verify checkExpansionWithRedirection "var=`foo 1> bar`" -prop_checkExpansionWithRedirection3 = verify checkExpansionWithRedirection "var=${ foo >> bar; }" -prop_checkExpansionWithRedirection4 = verify checkExpansionWithRedirection "var=$(foo | bar > baz)" -prop_checkExpansionWithRedirection5 = verifyNot checkExpansionWithRedirection "stderr=$(foo 2>&1 > /dev/null)" -prop_checkExpansionWithRedirection6 = verifyNot checkExpansionWithRedirection "var=$(foo; bar > baz)" -prop_checkExpansionWithRedirection7 = verifyNot checkExpansionWithRedirection "var=$(foo > bar; baz)" -prop_checkExpansionWithRedirection8 = verifyNot checkExpansionWithRedirection "var=$(cat <&3)" -checkExpansionWithRedirection params t = - case t of - T_DollarExpansion id [cmd] -> check id cmd - T_Backticked id [cmd] -> check id cmd - T_DollarBraceCommandExpansion id _ [cmd] -> check id cmd - _ -> return () - where - check id pipe = - case pipe of - (T_Pipeline _ _ t@(_:_)) -> checkCmd id (last t) - _ -> return () - - checkCmd captureId (T_Redirecting _ redirs _) = foldr (walk captureId) (return ()) redirs - - walk captureId t acc = - case t of - T_FdRedirect _ _ (T_IoDuplicate _ _ "1") -> return () - T_FdRedirect id "1" (T_IoDuplicate _ _ _) -> return () - T_FdRedirect id "" (T_IoDuplicate _ op _) | op `elem` [T_GREATAND (Id 0), T_Greater (Id 0)] -> emit id captureId True - T_FdRedirect id str (T_IoFile _ op file) | str `elem` ["", "1"] && op `elem` [ T_DGREAT (Id 0), T_Greater (Id 0) ] -> - emit id captureId $ getLiteralString file /= Just "/dev/null" - _ -> acc - - emit redirectId captureId suggestTee = do - warn captureId 2327 "This command substitution will be empty because the command's output gets redirected away." - err redirectId 2328 $ "This redirection takes output away from the command substitution" ++ if suggestTee then " (use tee to duplicate)." else "." - - -prop_checkUnaryTestA1 = verify checkUnaryTestA "[ -a foo ]" -prop_checkUnaryTestA2 = verify checkUnaryTestA "[ ! -a foo ]" -prop_checkUnaryTestA3 = verifyNot checkUnaryTestA "[ foo -a bar ]" -checkUnaryTestA params t = - case t of - TC_Unary id _ "-a" _ -> - styleWithFix id 2331 "For file existence, prefer standard -e over legacy -a." $ - fixWith [replaceStart id params 2 "-e"] - _ -> return () - -return [] -runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/Analyzer.hs b/src/ShellCheck/Analyzer.hs deleted file mode 100644 index 53717ed..0000000 --- a/src/ShellCheck/Analyzer.hs +++ /dev/null @@ -1,55 +0,0 @@ -{- - Copyright 2012-2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -module ShellCheck.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where - -import ShellCheck.Analytics -import ShellCheck.AnalyzerLib -import ShellCheck.Interface -import Data.List -import Data.Monoid -import qualified ShellCheck.Checks.Commands -import qualified ShellCheck.Checks.ControlFlow -import qualified ShellCheck.Checks.Custom -import qualified ShellCheck.Checks.ShellSupport - - --- TODO: Clean up the cruft this is layered on -analyzeScript :: AnalysisSpec -> AnalysisResult -analyzeScript spec = newAnalysisResult { - arComments = - filterByAnnotation spec params . nub $ - runChecker params (checkers spec params) -} - where - params = makeParameters spec - -checkers spec params = mconcat $ map ($ params) [ - ShellCheck.Analytics.checker spec, - ShellCheck.Checks.Commands.checker spec, - ShellCheck.Checks.ControlFlow.checker spec, - ShellCheck.Checks.Custom.checker, - ShellCheck.Checks.ShellSupport.checker - ] - -optionalChecks = mconcat $ [ - ShellCheck.Analytics.optionalChecks, - ShellCheck.Checks.Commands.optionalChecks, - ShellCheck.Checks.ControlFlow.optionalChecks - ] diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs deleted file mode 100644 index da528a4..0000000 --- a/src/ShellCheck/AnalyzerLib.hs +++ /dev/null @@ -1,944 +0,0 @@ -{- - Copyright 2012-2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE TemplateHaskell #-} -module ShellCheck.AnalyzerLib where - -import ShellCheck.AST -import ShellCheck.ASTLib -import qualified ShellCheck.CFGAnalysis as CF -import ShellCheck.Data -import ShellCheck.Interface -import ShellCheck.Parser -import ShellCheck.Prelude -import ShellCheck.Regex - -import Control.Arrow (first) -import Control.DeepSeq -import Control.Monad -import Control.Monad.Identity -import Control.Monad.RWS -import Control.Monad.State -import Control.Monad.Writer -import Data.Char -import Data.List -import Data.Maybe -import Data.Semigroup -import qualified Data.List.NonEmpty as NE -import qualified Data.Map as Map - -import Test.QuickCheck.All (forAllProperties) -import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs) - -type Analysis = AnalyzerM () -type AnalyzerM a = RWS Parameters [TokenComment] Cache a -nullCheck = const $ return () - - -data Checker = Checker { - perScript :: Root -> Analysis, - perToken :: Token -> Analysis -} - -runChecker :: Parameters -> Checker -> [TokenComment] -runChecker params checker = notes - where - root = rootNode params - check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x) - notes = snd $ evalRWS (check $ Root root) params Cache - -instance Semigroup Checker where - (<>) x y = Checker { - perScript = perScript x `composeAnalyzers` perScript y, - perToken = perToken x `composeAnalyzers` perToken y - } - -instance Monoid Checker where - mempty = Checker { - perScript = nullCheck, - perToken = nullCheck - } - mappend = (Data.Semigroup.<>) - -composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis -composeAnalyzers f g x = f x >> g x - -data Parameters = Parameters { - -- Whether this script has the 'lastpipe' option set/default. - hasLastpipe :: Bool, - -- Whether this script has the 'inherit_errexit' option set/default. - hasInheritErrexit :: Bool, - -- Whether this script has 'set -e' anywhere. - hasSetE :: Bool, - -- Whether this script has 'set -o pipefail' anywhere. - hasPipefail :: Bool, - -- Whether this script has 'shopt -s execfail' anywhere. - hasExecfail :: Bool, - -- A linear (bad) analysis of data flow - variableFlow :: [StackData], - -- A map from Id to Token - idMap :: Map.Map Id Token, - -- A map from Id to parent Token - parentMap :: Map.Map Id Token, - -- The shell type, such as Bash or Ksh - shellType :: Shell, - -- True if shell type was forced via flags - shellTypeSpecified :: Bool, - -- The root node of the AST - rootNode :: Token, - -- map from token id to start and end position - tokenPositions :: Map.Map Id (Position, Position), - -- Result from Control Flow Graph analysis (including data flow analysis) - cfgAnalysis :: Maybe CF.CFGAnalysis - } deriving (Show) - --- TODO: Cache results of common AST ops here -data Cache = Cache {} - -data Scope = SubshellScope String | NoneScope deriving (Show, Eq) -data StackData = - StackScope Scope - | StackScopeEnd - -- (Base expression, specific position, var name, assigned values) - | Assignment (Token, Token, String, DataType) - | Reference (Token, Token, String) - deriving (Show) - -data DataType = DataString DataSource | DataArray DataSource - deriving (Show) - -data DataSource = - SourceFrom [Token] - | SourceExternal - | SourceDeclaration - | SourceInteger - | SourceChecked - deriving (Show) - -data VariableState = Dead Token String | Alive deriving (Show) - -defaultSpec pr = spec { - asShellType = Nothing, - asCheckSourced = False, - asExecutionMode = Executed, - asTokenPositions = prTokenPositions pr -} where spec = newAnalysisSpec (fromJust $ prRoot pr) - -pScript s = - let - pSpec = newParseSpec { - psFilename = "script", - psScript = s - } - in runIdentity $ parseScript (mockedSystemInterface []) pSpec - --- For testing. If parsed, returns whether there are any comments -producesComments :: Checker -> String -> Maybe Bool -producesComments c s = do - let pr = pScript s - prRoot pr - let spec = defaultSpec pr - let params = makeParameters spec - return . not . null $ filterByAnnotation spec params $ runChecker params c - -makeComment :: Severity -> Id -> Code -> String -> TokenComment -makeComment severity id code note = - newTokenComment { - tcId = id, - tcComment = newComment { - cSeverity = severity, - cCode = code, - cMessage = note - } - } - -addComment note = note `deepseq` tell [note] - -warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m () -warn id code str = addComment $ makeComment WarningC id code str -err id code str = addComment $ makeComment ErrorC id code str -info id code str = addComment $ makeComment InfoC id code str -style id code str = addComment $ makeComment StyleC id code str - -errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () -errWithFix = addCommentWithFix ErrorC -warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () -warnWithFix = addCommentWithFix WarningC -infoWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () -infoWithFix = addCommentWithFix InfoC -styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () -styleWithFix = addCommentWithFix StyleC - -addCommentWithFix :: MonadWriter [TokenComment] m => Severity -> Id -> Code -> String -> Fix -> m () -addCommentWithFix severity id code str fix = - addComment $ makeCommentWithFix severity id code str fix - -makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment -makeCommentWithFix severity id code str fix = - let comment = makeComment severity id code str - withFix = comment { - -- If fix is empty, pretend it wasn't there. - tcFix = if null (fixReplacements fix) then Nothing else Just fix - } - in force withFix - --- makeParameters :: CheckSpec -> Parameters -makeParameters spec = params - where - extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root] - params = Parameters { - rootNode = root, - shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec, - hasSetE = containsSetE root, - hasLastpipe = - case shellType params of - Bash -> isOptionSet "lastpipe" root - Dash -> False - BusyboxSh -> False - Sh -> False - Ksh -> True, - hasInheritErrexit = - case shellType params of - Bash -> isOptionSet "inherit_errexit" root - Dash -> True - BusyboxSh -> True - Sh -> True - Ksh -> False, - hasPipefail = - case shellType params of - Bash -> isOptionSet "pipefail" root - Dash -> True - BusyboxSh -> isOptionSet "pipefail" root - Sh -> True - Ksh -> isOptionSet "pipefail" root, - hasExecfail = - case shellType params of - Bash -> isOptionSet "execfail" root - _ -> False, - shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), - idMap = getTokenMap root, - parentMap = getParentTree root, - variableFlow = getVariableFlow params root, - tokenPositions = asTokenPositions spec, - cfgAnalysis = do - guard extendedAnalysis - return $ CF.analyzeControlFlow cfParams root - } - cfParams = CF.CFGParameters { - CF.cfLastpipe = hasLastpipe params, - CF.cfPipefail = hasPipefail params - } - root = asScript spec - - --- Does this script mention 'set -e' anywhere? --- Used as a hack to disable certain warnings. -containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root - where - isSetE t = - case t of - T_Script _ (T_Literal _ str) _ -> str `matches` re - T_SimpleCommand {} -> - t `isUnqualifiedCommand` "set" && - ("errexit" `elem` oversimplify t || - "e" `elem` map snd (getAllFlags t)) - _ -> False - re = mkRegex "[[:space:]]-[^-]*e" - - -containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root - where - isPipefail t = - case t of - T_SimpleCommand {} -> - t `isUnqualifiedCommand` "set" && - (opt `elem` oversimplify t || - "o" `elem` map snd (getAllFlags t)) - _ -> False - -containsShopt shopt root = - isNothing $ doAnalysis (guard . not . isShoptLastPipe) root - where - isShoptLastPipe t = - case t of - T_SimpleCommand {} -> - t `isUnqualifiedCommand` "shopt" && - (shopt `elem` oversimplify t) - _ -> False - --- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere? -isOptionSet opt root = containsShopt opt root || containsSetOption opt root - - -prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh -prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh -prop_determineShell2 = determineShellTest "" == Bash -prop_determineShell3 = determineShellTest "#!/bin/sh -e" == Sh -prop_determineShell4 = determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh -prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh -prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh -prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash -prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh -prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash -prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash -prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh -prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh - -determineShellTest = determineShellTest' Nothing -determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript -determineShell fallbackShell t = fromMaybe Bash $ - shellForExecutable shellString `mplus` fallbackShell - where - shellString = getCandidate t - getCandidate :: Token -> String - getCandidate t@T_Script {} = fromShebang t - getCandidate (T_Annotation _ annotations s) = - headOrDefault (fromShebang s) [s | ShellOverride s <- annotations] - fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s - --- Given a root node, make a map from Id to parent Token. --- This is used to populate parentMap in Parameters -getParentTree :: Token -> Map.Map Id Token -getParentTree t = - snd $ execState (doStackAnalysis pre post t) ([], Map.empty) - where - pre t = modify (first ((:) t)) - post t = do - (x, map) <- get - case x of - _:rest -> case rest of [] -> put (rest, map) - (x:_) -> put (rest, Map.insert (getId t) x map) - --- Given a root node, make a map from Id to Token -getTokenMap :: Token -> Map.Map Id Token -getTokenMap t = - execState (doAnalysis f t) Map.empty - where - f t = modify (Map.insert (getId t) t) - - --- Is this token in a quoting free context? (i.e. would variable expansion split) --- True: Assignments, [[ .. ]], here docs, already in double quotes --- False: Regular words -isStrictlyQuoteFree = isQuoteFreeNode True - --- Like above, but also allow some cases where splitting may be desired. --- True: Like above + for loops --- False: Like above -isQuoteFree = isQuoteFreeNode False - - -isQuoteFreeNode strict shell tree t = - isQuoteFreeElement t || - (fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t) - where - -- Is this node self-quoting in itself? - isQuoteFreeElement t = - case t of - T_Assignment id _ _ _ _ -> assignmentIsQuoting id - T_FdRedirect {} -> True - _ -> False - - -- Are any subnodes inherently self-quoting? - isQuoteFreeContext t = - case t of - TC_Nullary _ DoubleBracket _ -> return True - TC_Unary _ DoubleBracket _ _ -> return True - TC_Binary _ DoubleBracket _ _ _ -> return True - TA_Sequence {} -> return True - T_Arithmetic {} -> return True - T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id - T_Redirecting {} -> return False - T_DoubleQuoted _ _ -> return True - T_DollarDoubleQuoted _ _ -> return True - T_CaseExpression {} -> return True - T_HereDoc {} -> return True - T_DollarBraced {} -> return True - -- When non-strict, pragmatically assume it's desirable to split here - T_ForIn {} -> return (not strict) - T_SelectIn {} -> return (not strict) - _ -> Nothing - - -- Check whether this assignment is self-quoting due to being a recognized - -- assignment passed to a Declaration Utility. This will soon be required - -- by POSIX: https://austingroupbugs.net/view.php?id=351 - assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id) - shellParsesParamsAsAssignments = shell /= Sh - - -- Is this assignment a parameter to a command like export/typeset/etc? - isAssignmentParamToCommand id = - case Map.lookup id tree of - Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args) - _ -> False - --- Check if a token is a parameter to a certain command by name: --- Example: isParamTo (parentMap params) "sed" t -isParamTo :: Map.Map Id Token -> String -> Token -> Bool -isParamTo tree cmd = - go - where - go x = case Map.lookup (getId x) tree of - Nothing -> False - Just parent -> check parent - check t = - case t of - T_SingleQuoted _ _ -> go t - T_DoubleQuoted _ _ -> go t - T_NormalWord _ _ -> go t - T_SimpleCommand {} -> isCommand t cmd - T_Redirecting {} -> isCommand t cmd - _ -> False - --- Get the parent command (T_Redirecting) of a Token, if any. -getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token -getClosestCommand tree t = - findFirst findCommand $ NE.toList $ getPath tree t - where - findCommand t = - case t of - T_Redirecting {} -> return True - T_Script {} -> return False - _ -> Nothing - --- Like above, if koala_man knew Haskell when starting this project. -getClosestCommandM t = do - params <- ask - return $ getClosestCommand (parentMap params) t - --- Is the token used as a command name (the first word in a T_SimpleCommand)? -usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token) - where - go currentId (T_NormalWord id [word]:rest) - | currentId == getId word = go id rest - go currentId (T_DoubleQuoted id [word]:rest) - | currentId == getId word = go id rest - go currentId (t@(T_SimpleCommand _ _ (word:_)):_) = - getId word == currentId || getId (getCommandTokenOrThis t) == currentId - go _ _ = False - --- Version of the above taking the map from the current context --- Todo: give this the name "getPath" -getPathM t = do - params <- ask - return $ getPath (parentMap params) t - -isParentOf tree parent child = - any (\t -> parentId == getId t) (getPath tree child) - where - parentId = getId parent - -parents params = getPath (parentMap params) - --- Find the first match in a list where the predicate is Just True. --- Stops if it's Just False and ignores Nothing. -findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a -findFirst p = foldr go Nothing - where - go x acc = - case p x of - Just True -> return x - Just False -> Nothing - Nothing -> acc - --- Check whether a word is entirely output from a single command -tokenIsJustCommandOutput t = case t of - T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds - T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds - T_NormalWord id [T_Backticked _ cmds] -> check cmds - T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds - _ -> False - where - check [x] = not $ isOnlyRedirection x - check _ = False - --- TODO: Replace this with a proper Control Flow Graph -getVariableFlow params t = - reverse $ execState (doStackAnalysis startScope endScope t) [] - where - startScope t = - let scopeType = leadType params t - in do - when (scopeType /= NoneScope) $ modify (StackScope scopeType:) - when (assignFirst t) $ setWritten t - - endScope t = - let scopeType = leadType params t - in do - setRead t - unless (assignFirst t) $ setWritten t - when (scopeType /= NoneScope) $ modify (StackScopeEnd:) - - assignFirst T_ForIn {} = True - assignFirst T_SelectIn {} = True - assignFirst (T_BatsTest {}) = True - assignFirst _ = False - - setRead t = - let read = getReferencedVariables (parentMap params) t - in mapM_ (\v -> modify (Reference v:)) read - - setWritten t = - let written = getModifiedVariables t - in mapM_ (\v -> modify (Assignment v:)) written - - -leadType params t = - case t of - T_DollarExpansion _ _ -> SubshellScope "$(..) expansion" - T_Backticked _ _ -> SubshellScope "`..` expansion" - T_Backgrounded _ _ -> SubshellScope "backgrounding &" - T_Subshell _ _ -> SubshellScope "(..) group" - T_BatsTest {} -> SubshellScope "@bats test" - T_CoProcBody _ _ -> SubshellScope "coproc" - T_Redirecting {} -> - if causesSubshell == Just True - then SubshellScope "pipeline" - else NoneScope - _ -> NoneScope - where - parentPipeline = do - parent <- Map.lookup (getId t) (parentMap params) - case parent of - T_Pipeline {} -> return parent - _ -> Nothing - - causesSubshell = do - (T_Pipeline _ _ list) <- parentPipeline - return $ case list of - _:_:_ -> not (hasLastpipe params) || getId (last list) /= getId t - _ -> False - -getModifiedVariables t = - case t of - T_SimpleCommand _ vars [] -> - [(x, x, name, dataTypeFrom DataString w) | x@(T_Assignment id _ name _ w) <- vars] - T_SimpleCommand {} -> - getModifiedVariableCommand t - - TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op -> - [(t, v, name, DataString SourceInteger)] - TA_Assignment _ op (TA_Variable _ name _) rhs -> do - guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] - return (t, t, name, DataString SourceInteger) - - T_BatsTest {} -> [ - (t, t, "lines", DataArray SourceExternal), - (t, t, "status", DataString SourceInteger), - (t, t, "output", DataString SourceExternal), - (t, t, "stderr", DataString SourceExternal), - (t, t, "stderr_lines", DataArray SourceExternal) - ] - - -- Count [[ -v foo ]] as an "assignment". - -- This is to prevent [ -v foo ] being unassigned or unused. - TC_Unary id _ "-v" token -> maybeToList $ do - str <- getVariableForTestDashV token - return (t, token, str, DataString SourceChecked) - - TC_Unary _ _ "-n" token -> markAsChecked t token - TC_Unary _ _ "-z" token -> markAsChecked t token - TC_Nullary _ _ token -> markAsChecked t token - - T_DollarBraced _ _ l -> maybeToList $ do - let string = concat $ oversimplify l - let modifier = getBracedModifier string - guard $ any (`isPrefixOf` modifier) ["=", ":="] - return (t, t, getBracedReference string, DataString $ SourceFrom [l]) - - T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo - [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] - - T_CoProc _ Nothing _ -> - [(t, t, "COPROC", DataArray SourceInteger)] - - T_CoProc _ (Just token) _ -> do - name <- maybeToList $ getLiteralString token - [(t, t, name, DataArray SourceInteger)] - - --Points to 'for' rather than variable - T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)] - T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] - T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] - _ -> [] - where - markAsChecked place token = mapMaybe (f place) $ getWordParts token - f place t = case t of - T_DollarBraced _ _ l -> - let str = getBracedReference $ concat $ oversimplify l in do - guard $ isVariableName str - return (place, t, str, DataString SourceChecked) - _ -> Nothing - - --- Consider 'export/declare -x' a reference, since it makes the var available -getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) = - case x of - "declare" -> forDeclare - "typeset" -> forDeclare - - "export" -> if "f" `elem` flags - then [] - else concatMap getReference rest - "local" -> if "x" `elem` flags - then concatMap getReference rest - else [] - "trap" -> - case rest of - head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head - _ -> [] - "alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token] - _ -> [] - where - forDeclare = - if - any (`elem` flags) ["x", "p"] && - (not $ any (`elem` flags) ["f", "F"]) - then concatMap getReference rest - else [] - - getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)] - getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)] - getReference _ = [] - flags = map snd $ getAllFlags base - -getReferencedVariableCommand _ = [] - --- The function returns a tuple consisting of four items describing an assignment. --- Given e.g. declare foo=bar --- ( --- BaseCommand :: Token, -- The command/structure assigning the variable, i.e. declare foo=bar --- AssignmentToken :: Token, -- The specific part that assigns this variable, i.e. foo=bar --- VariableName :: String, -- The variable name, i.e. foo --- VariableValue :: DataType -- A description of the value being assigned, i.e. "Literal string with value foo" --- ) -getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T_Literal _ x:_):rest)) = - filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $ - case x of - "builtin" -> - getModifiedVariableCommand $ T_SimpleCommand id cmdPrefix rest - "read" -> - let fallback = catMaybes $ takeWhile isJust (reverse $ map getLiteral rest) - in fromMaybe fallback $ do - parsed <- getGnuOpts flagsForRead rest - case lookup "a" parsed of - Just (_, var) -> (:[]) <$> getLiteralArray var - Nothing -> return $ catMaybes $ - map (getLiteral . snd . snd) $ filter (null . fst) parsed - - "getopts" -> - case rest of - opts:var:_ -> maybeToList $ getLiteral var - _ -> [] - - "let" -> concatMap letParamToLiteral rest - - "export" -> - if "f" `elem` flags then [] else concatMap getModifierParamString rest - - "declare" -> forDeclare - "typeset" -> forDeclare - - "local" -> concatMap getModifierParamString rest - "readonly" -> - if any (`elem` flags) ["f", "p"] - then [] - else concatMap getModifierParamString rest - "set" -> maybeToList $ do - params <- getSetParams rest - return (base, base, "@", DataString $ SourceFrom params) - - "printf" -> maybeToList $ getPrintfVariable rest - "wait" -> maybeToList $ getWaitVariable rest - - "mapfile" -> maybeToList $ getMapfileArray base rest - "readarray" -> maybeToList $ getMapfileArray base rest - - "DEFINE_boolean" -> maybeToList $ getFlagVariable rest - "DEFINE_float" -> maybeToList $ getFlagVariable rest - "DEFINE_integer" -> maybeToList $ getFlagVariable rest - "DEFINE_string" -> maybeToList $ getFlagVariable rest - - _ -> [] - where - flags = map snd $ getAllFlags base - stripEquals s = drop 1 $ dropWhile (/= '=') s - stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) = - T_NormalWord id1 (T_Literal id2 (stripEquals s):rs) - stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) = - T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]] - stripEqualsFrom t = t - - forDeclare = if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars - - declaredVars = concatMap (getModifierParam defaultType) rest - where - defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString - - getLiteralOfDataType t d = do - s <- getLiteralString t - when ("-" `isPrefixOf` s) $ fail "argument" - return (base, t, s, d) - - getLiteral t = getLiteralOfDataType t (DataString SourceExternal) - - getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal) - - getModifierParamString = getModifierParam DataString - - getModifierParam def t@(T_Assignment _ _ name _ value) = - [(base, t, name, dataTypeFrom def value)] - getModifierParam def t@T_NormalWord {} = maybeToList $ do - name <- getLiteralString t - guard $ isVariableName name - return (base, t, name, def SourceDeclaration) - getModifierParam _ _ = [] - - letParamToLiteral token = - if null var - then [] - else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])] - where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token - - getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest - getSetParams (t:rest) = - let s = getLiteralString t in - case s of - Just "--" -> return rest - Just ('-':_) -> getSetParams rest - _ -> return (t:fromMaybe [] (getSetParams rest)) - getSetParams [] = Nothing - - getPrintfVariable list = getFlagAssignedVariable "v" (SourceFrom list) $ getBsdOpts "v:" list - getWaitVariable list = getFlagAssignedVariable "p" SourceInteger $ return $ getGenericOpts list - - getFlagAssignedVariable str dataSource maybeFlags = do - flags <- maybeFlags - (_, (flag, value)) <- find ((== str) . fst) flags - variableName <- getLiteralStringExt (const $ return "!") value - let (baseName, index) = span (/= '[') variableName - return (base, value, baseName, (if null index then DataString else DataArray) dataSource) - - -- mapfile has some curious syntax allowing flags plus 0..n variable names - -- where only the first non-option one is used if any. - getMapfileArray base rest = parseArgs `mplus` fallback - where - parseArgs :: Maybe (Token, Token, String, DataType) - parseArgs = do - args <- getGnuOpts "d:n:O:s:u:C:c:t" rest - case [y | ("",(_,y)) <- args] of - [] -> - return (base, base, "MAPFILE", DataArray SourceExternal) - first:_ -> do - name <- getLiteralString first - guard $ isVariableName name - return (base, first, name, DataArray SourceExternal) - -- If arg parsing fails (due to bad or new flags), get the last variable name - fallback :: Maybe (Token, Token, String, DataType) - fallback = do - (name, token) <- listToMaybe . mapMaybe f $ reverse rest - return (base, token, name, DataArray SourceExternal) - f arg = do - name <- getLiteralString arg - guard $ isVariableName name - return (name, arg) - - -- get the FLAGS_ variable created by a shflags DEFINE_ call - getFlagVariable (n:v:_) = do - name <- getLiteralString n - return (base, n, "FLAGS_" ++ name, DataString $ SourceExternal) - getFlagVariable _ = Nothing - -getModifiedVariableCommand _ = [] - --- Given a NormalWord like foo or foo[$bar], get foo. --- Primarily used to get references for [[ -v foo[bar] ]] -getVariableForTestDashV :: Token -> Maybe String -getVariableForTestDashV t = do - str <- takeWhile ('[' /=) <$> getLiteralStringExt toStr t - guard $ isVariableName str - return str - where - -- foo[bar] gets parsed with [bar] as a glob, so undo that - toStr (T_Glob _ s) = return s - -- Turn foo[$x] into foo[\0] so that we can get the constant array name - -- in a non-constant expression (while filtering out foo$x[$y]) - toStr _ = return "\0" - -getReferencedVariables parents t = - case t of - T_DollarBraced id _ l -> let str = concat $ oversimplify l in - (t, t, getBracedReference str) : - map (\x -> (l, l, x)) ( - getIndexReferences str - ++ getOffsetReferences (getBracedModifier str)) - TA_Variable id name _ -> - if isArithmeticAssignment t - then [] - else [(t, t, name)] - T_Assignment id mode str _ word -> - [(t, t, str) | mode == Append] ++ specialReferences str t word - - TC_Unary id _ "-v" token -> getIfReference t token - TC_Unary id _ "-R" token -> getIfReference t token - TC_Binary id DoubleBracket op lhs rhs -> - if isDereferencingBinaryOp op - then concatMap (getIfReference t) [lhs, rhs] - else [] - - T_BatsTest {} -> [ -- pretend @test references vars to avoid warnings - (t, t, "lines"), - (t, t, "status"), - (t, t, "output") - ] - - T_FdRedirect _ ('{':var) op -> -- {foo}>&- references and closes foo - [(t, t, takeWhile (/= '}') var) | isClosingFileOp op] - x -> getReferencedVariableCommand x - where - -- Try to reduce false positives for unused vars only referenced from evaluated vars - specialReferences name base word = - if name `elem` [ - "PS1", "PS2", "PS3", "PS4", - "PROMPT_COMMAND" - ] - then - map (\x -> (base, base, x)) $ - getVariablesFromLiteralToken word - else [] - - literalizer t = case t of - T_Glob _ s -> return s -- Also when parsed as globs - _ -> [] - - getIfReference context token = maybeToList $ do - str <- getVariableForTestDashV token - return (context, token, getBracedReference str) - - isArithmeticAssignment t = case getPath parents t of - this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t - _ -> False - -isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]) - -dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v] - - ---- Command specific checks - --- Compare a command to a string: t `isCommand` "sed" (also matches /usr/bin/sed) -isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd) - --- Compare a command to a literal. Like above, but checks full path. -isUnqualifiedCommand token str = isCommandMatch token (== str) - -isCommandMatch token matcher = maybe False - matcher (getCommandName token) - --- Does this regex look like it was intended as a glob? --- True: *foo* --- False: .*foo.* -isConfusedGlobRegex :: String -> Bool -isConfusedGlobRegex ('*':_) = True -isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True -isConfusedGlobRegex _ = False - -getVariablesFromLiteralToken token = - getVariablesFromLiteral (getLiteralStringDef " " token) - --- Try to get referenced variables from a literal string like "$foo" --- Ignores tons of cases like arithmetic evaluation and array indices. -prop_getVariablesFromLiteral1 = - getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"] -getVariablesFromLiteral string = - map head $ matchAllSubgroups variableRegex string - where - variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" - - --- Run a command if the shell is in the given list -whenShell l c = do - params <- ask - when (shellType params `elem` l ) c - - -filterByAnnotation asSpec params = - filter (not . shouldIgnore) - where - token = asScript asSpec - shouldIgnore note = - any (shouldIgnoreFor (getCode note)) $ - getPath parents (T_Bang $ tcId note) - shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec - shouldIgnoreFor code t = isAnnotationIgnoringCode code t - parents = parentMap params - getCode = cCode . tcComment - -shouldIgnoreCode params code t = - any (isAnnotationIgnoringCode code) $ - getPath (parentMap params) t - --- Is this a ${#anything}, to get string length or array count? -isCountingReference (T_DollarBraced id _ token) = - case concat $ oversimplify token of - '#':_ -> True - _ -> False -isCountingReference _ = False - --- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"} -isQuotedAlternativeReference t = - case t of - T_DollarBraced _ _ l -> - getBracedModifier (concat $ oversimplify l) `matches` re - _ -> False - where - re = mkRegex "(^|\\]):?\\+" - -supportsArrays Bash = True -supportsArrays Ksh = True -supportsArrays _ = False - -isTrueAssignmentSource c = - case c of - DataString SourceChecked -> False - DataString SourceDeclaration -> False - DataArray SourceChecked -> False - DataArray SourceDeclaration -> False - _ -> True - -modifiesVariable params token name = - or $ map check flow - where - flow = getVariableFlow params token - check t = - case t of - Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name - _ -> False - -isTestCommand t = - case t of - T_Condition {} -> True - T_SimpleCommand {} -> t `isCommand` "test" - T_Redirecting _ _ t -> isTestCommand t - T_Annotation _ _ t -> isTestCommand t - T_Pipeline _ _ [t] -> isTestCommand t - _ -> False - -return [] -runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs deleted file mode 100644 index c235cb7..0000000 --- a/src/ShellCheck/CFG.hs +++ /dev/null @@ -1,1319 +0,0 @@ -{- - Copyright 2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE DeriveAnyClass, DeriveGeneric #-} - --- Constructs a Control Flow Graph from an AST -module ShellCheck.CFG ( - CFNode (..), - CFEdge (..), - CFEffect (..), - CFStringPart (..), - CFVariableProp (..), - CFGResult (..), - CFValue (..), - CFGraph, - CFGParameters (..), - IdTagged (..), - Scope (..), - buildGraph - , ShellCheck.CFG.runTests -- STRIP - ) - where - -import GHC.Generics (Generic) -import ShellCheck.AST -import ShellCheck.ASTLib -import ShellCheck.Data -import ShellCheck.Interface -import ShellCheck.Prelude -import ShellCheck.Regex -import Control.DeepSeq -import Control.Monad -import Control.Monad.Identity -import Data.Array.Unboxed -import Data.Array.ST -import Data.List hiding (map) -import qualified Data.List.NonEmpty as NE -import Data.Maybe -import qualified Data.Map as M -import qualified Data.Set as S -import Control.Monad.RWS.Lazy -import Data.Graph.Inductive.Graph -import Data.Graph.Inductive.Query.DFS -import Data.Graph.Inductive.Basic -import Data.Graph.Inductive.Query.Dominators -import Data.Graph.Inductive.PatriciaTree as G -import Debug.Trace -- STRIP - -import Test.QuickCheck.All (forAllProperties) -import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) - - --- Our basic Graph type -type CFGraph = G.Gr CFNode CFEdge - --- Node labels in a Control Flow Graph -data CFNode = - -- A no-op node for structural purposes - CFStructuralNode - -- A no-op for graph inspection purposes - | CFEntryPoint String - -- Drop current prefix assignments - | CFDropPrefixAssignments - -- A node with a certain effect on program state - | CFApplyEffects [IdTagged CFEffect] - -- The execution of a command or function by literal string if possible - | CFExecuteCommand (Maybe String) - -- Execute a subshell. These are represented by disjoint graphs just like - -- functions, but they don't require any form of name resolution - | CFExecuteSubshell String Node Node - -- Assignment of $? - | CFSetExitCode Id - -- The virtual 'exit' at the natural end of a subshell - | CFImpliedExit - -- An exit statement resolvable at CFG build time - | CFResolvedExit - -- An exit statement only resolvable at DFA time - | CFUnresolvedExit - -- An unreachable node, serving as the unconnected end point of a range - | CFUnreachable - -- Assignment of $! - | CFSetBackgroundPid Id - deriving (Eq, Ord, Show, Generic, NFData) - --- Edge labels in a Control Flow Graph -data CFEdge = - CFEErrExit - -- Regular control flow edge - | CFEFlow - -- An edge that a human might think exists (e.g. from a backgrounded process to its parent) - | CFEFalseFlow - -- An edge followed on exit - | CFEExit - deriving (Eq, Ord, Show, Generic, NFData) - --- Actions we track -data CFEffect = - CFSetProps (Maybe Scope) String (S.Set CFVariableProp) - | CFUnsetProps (Maybe Scope) String (S.Set CFVariableProp) - | CFReadVariable String - | CFWriteVariable String CFValue - | CFWriteGlobal String CFValue - | CFWriteLocal String CFValue - | CFWritePrefix String CFValue - | CFDefineFunction String Id Node Node - | CFUndefine String - | CFUndefineVariable String - | CFUndefineFunction String - | CFUndefineNameref String - -- Usage implies that this is an array (e.g. it's expanded with index) - | CFHintArray String - -- Operation implies that the variable will be defined (e.g. [ -z "$var" ]) - | CFHintDefined String - deriving (Eq, Ord, Show, Generic, NFData) - -data IdTagged a = IdTagged Id a - deriving (Eq, Ord, Show, Generic, NFData) - --- Where a variable's value comes from -data CFValue = - -- The special 'uninitialized' value - CFValueUninitialized - -- An arbitrary array value - | CFValueArray - -- An arbitrary string value - | CFValueString - -- An arbitrary integer - | CFValueInteger - -- Token 'Id' concatenates and assigns the given parts - | CFValueComputed Id [CFStringPart] - deriving (Eq, Ord, Show, Generic, NFData) - --- Simplified computed strings -data CFStringPart = - -- A known literal string value, like 'foo' - CFStringLiteral String - -- The contents of a variable, like $foo (may not be a string) - | CFStringVariable String - -- An value that is unknown but an integer - | CFStringInteger - -- An unknown string value, for things we can't handle - | CFStringUnknown - deriving (Eq, Ord, Show, Generic, NFData) - --- The properties of a variable -data CFVariableProp = CFVPExport | CFVPArray | CFVPAssociative | CFVPInteger - deriving (Eq, Ord, Show, Generic, NFData) - --- Options when generating CFG -data CFGParameters = CFGParameters { - -- Whether the last element in a pipeline runs in the current shell - cfLastpipe :: Bool, - -- Whether all elements in a pipeline count towards the exit status - cfPipefail :: Bool -} - -data CFGResult = CFGResult { - -- The graph itself - cfGraph :: CFGraph, - -- Map from Id to nominal start&end node (i.e. assuming normal execution without exits) - cfIdToRange :: M.Map Id (Node, Node), - -- A set of all nodes belonging to an Id, recursively - cfIdToNodes :: M.Map Id (S.Set Node), - -- An array (from,to) saying whether 'from' postdominates 'to' - cfPostDominators :: Array Node [Node] -} - deriving (Show) - -buildGraph :: CFGParameters -> Token -> CFGResult -buildGraph params root = - let - (nextNode, base) = execRWS (buildRoot root) (newCFContext params) 0 - (nodes, edges, mapping, association) = --- renumberTopologically $ - removeUnnecessaryStructuralNodes - base - - idToRange = M.fromList mapping - isRealEdge (from, to, edge) = case edge of CFEFlow -> True; CFEExit -> True; _ -> False - onlyRealEdges = filter isRealEdge edges - (_, mainExit) = fromJust $ M.lookup (getId root) idToRange - - result = CFGResult { - cfGraph = mkGraph nodes edges, - cfIdToRange = idToRange, - cfIdToNodes = M.fromListWith S.union $ map (\(id, n) -> (id, S.singleton n)) association, - cfPostDominators = findPostDominators mainExit $ mkGraph nodes onlyRealEdges - } - in - result - -remapGraph :: M.Map Node Node -> CFW -> CFW -remapGraph remap (nodes, edges, mapping, assoc) = - ( - map (remapNode remap) nodes, - map (remapEdge remap) edges, - map (\(id, (a,b)) -> (id, (remapHelper remap a, remapHelper remap b))) mapping, - map (\(id, n) -> (id, remapHelper remap n)) assoc - ) - -prop_testRenumbering = - let - s = CFStructuralNode - before = ( - [(1,s), (3,s), (4, s), (8,s)], - [(1,3,CFEFlow), (3,4, CFEFlow), (4,8,CFEFlow)], - [(Id 0, (3,4))], - [(Id 1, 3), (Id 2, 4)] - ) - after = ( - [(0,s), (1,s), (2,s), (3,s)], - [(0,1,CFEFlow), (1,2, CFEFlow), (2,3,CFEFlow)], - [(Id 0, (1,2))], - [(Id 1, 1), (Id 2, 2)] - ) - in after == renumberGraph before - --- Renumber the graph for prettiness, so there are no gaps in node numbers -renumberGraph :: CFW -> CFW -renumberGraph g@(nodes, edges, mapping, assoc) = - let renumbering = M.fromList (flip zip [0..] $ sort $ map fst nodes) - in remapGraph renumbering g - -prop_testRenumberTopologically = - let - s = CFStructuralNode - before = ( - [(4,s), (2,s), (3, s)], - [(4,2,CFEFlow), (2,3, CFEFlow)], - [(Id 0, (4,2))], - [] - ) - after = ( - [(0,s), (1,s), (2,s)], - [(0,1,CFEFlow), (1,2, CFEFlow)], - [(Id 0, (0,1))], - [] - ) - in after == renumberTopologically before - --- Renumber the graph in topological order -renumberTopologically g@(nodes, edges, mapping, assoc) = - let renumbering = M.fromList (flip zip [0..] $ topsort (mkGraph nodes edges :: CFGraph)) - in remapGraph renumbering g - -prop_testRemoveStructural = - let - s = CFStructuralNode - before = ( - [(1,s), (2,s), (3, s), (4,s)], - [(1,2,CFEFlow), (2,3, CFEFlow), (3,4,CFEFlow)], - [(Id 0, (2,3))], - [(Id 0, 3)] - ) - after = ( - [(1,s), (2,s), (4,s)], - [(1,2,CFEFlow), (2,4,CFEFlow)], - [(Id 0, (2,2))], - [(Id 0, 2)] - ) - in after == removeUnnecessaryStructuralNodes before - --- Collapse structural nodes that just form long chains like x->x->x. --- This way we can generate them with abandon, without making DFA slower. --- --- Note in particular that we can't remove a structural node x in --- foo -> x -> bar , because then the pre/post-condition for tokens --- previously pointing to x would be wrong. -removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) = - remapGraph recursiveRemapping - ( - filter (\(n, _) -> n `M.notMember` recursiveRemapping) nodes, - filter (`S.notMember` edgesToCollapse) edges, - mapping, - association - ) - where - regularEdges = filter isRegularEdge edges - inDegree = counter $ map (\(from,to,_) -> from) regularEdges - outDegree = counter $ map (\(from,to,_) -> to) regularEdges - structuralNodes = S.fromList [node | (node, CFStructuralNode) <- nodes] - candidateNodes = S.filter isLinear structuralNodes - edgesToCollapse = S.fromList $ filter filterEdges regularEdges - - remapping :: M.Map Node Node - remapping = M.fromList $ map orderEdge $ S.toList edgesToCollapse - recursiveRemapping = M.mapWithKey (\c _ -> recursiveLookup remapping c) remapping - - filterEdges (a,b,_) = - a `S.member` candidateNodes && b `S.member` candidateNodes - - orderEdge (a,b,_) = if a < b then (b,a) else (a,b) - counter = M.fromListWith (+) . map (\key -> (key, 1)) - isRegularEdge (_, _, CFEFlow) = True - isRegularEdge _ = False - - recursiveLookup :: M.Map Node Node -> Node -> Node - recursiveLookup map node = - case M.lookup node map of - Nothing -> node - Just x -> recursiveLookup map x - - isLinear node = - M.findWithDefault 0 node inDegree == 1 - && M.findWithDefault 0 node outDegree == 1 - - -remapNode :: M.Map Node Node -> LNode CFNode -> LNode CFNode -remapNode m (node, label) = - (remapHelper m node, newLabel) - where - newLabel = case label of - CFApplyEffects effects -> CFApplyEffects (map (remapEffect m) effects) - CFExecuteSubshell s a b -> CFExecuteSubshell s (remapHelper m a) (remapHelper m b) - _ -> label - -remapEffect map old@(IdTagged id effect) = - case effect of - CFDefineFunction name id start end -> IdTagged id $ CFDefineFunction name id (remapHelper map start) (remapHelper map end) - _ -> old - -remapEdge :: M.Map Node Node -> LEdge CFEdge -> LEdge CFEdge -remapEdge map (from, to, label) = (remapHelper map from, remapHelper map to, label) -remapHelper map n = M.findWithDefault n n map - -data Range = Range Node Node - deriving (Eq, Show) - -data CFContext = CFContext { - cfIsCondition :: Bool, - cfIsFunction :: Bool, - cfLoopStack :: [(Node, Node)], - cfTokenStack :: [Id], - cfExitTarget :: Maybe Node, - cfReturnTarget :: Maybe Node, - cfParameters :: CFGParameters -} -newCFContext params = CFContext { - cfIsCondition = False, - cfIsFunction = False, - cfLoopStack = [], - cfTokenStack = [], - cfExitTarget = Nothing, - cfReturnTarget = Nothing, - cfParameters = params -} - --- The monad we generate a graph in -type CFM a = RWS CFContext CFW Int a -type CFW = ([LNode CFNode], [LEdge CFEdge], [(Id, (Node, Node))], [(Id, Node)]) - -newNode :: CFNode -> CFM Node -newNode label = do - n <- get - stack <- asks cfTokenStack - put (n+1) - tell ([(n, label)], [], [], map (\c -> (c, n)) stack) - return n - -newNodeRange :: CFNode -> CFM Range --- newNodeRange label = nodeToRange <$> newNode label -newNodeRange label = nodeToRange <$> newNode label - --- Build a disjoint piece of the graph and return a CFExecuteSubshell. The Id is used purely for debug naming. -subshell :: Id -> String -> CFM Range -> CFM Range -subshell id reason p = do - start <- newNode $ CFEntryPoint $ "Subshell " ++ show id ++ ": " ++ reason - end <- newNode CFStructuralNode - middle <- local (\c -> c { cfExitTarget = Just end, cfReturnTarget = Just end}) p - linkRanges [nodeToRange start, middle, nodeToRange end] - newNodeRange $ CFExecuteSubshell reason start end - - -withFunctionScope p = do - end <- newNode CFStructuralNode - body <- local (\c -> c { cfReturnTarget = Just end, cfIsFunction = True }) p - linkRanges [body, nodeToRange end] - --- Anything that happens recursively in f will be attributed to this id -under :: Id -> CFM a -> CFM a -under id f = local (\c -> c { cfTokenStack = id:(cfTokenStack c) }) f - -nodeToRange :: Node -> Range -nodeToRange n = Range n n - -link :: Node -> Node -> CFEdge -> CFM () -link from to label = do - tell ([], [(from, to, label)], [], []) - -registerNode :: Id -> Range -> CFM () -registerNode id (Range start end) = tell ([], [], [(id, (start, end))], []) - -linkRange :: Range -> Range -> CFM Range -linkRange = linkRangeAs CFEFlow - -linkRangeAs :: CFEdge -> Range -> Range -> CFM Range -linkRangeAs label (Range start mid1) (Range mid2 end) = do - link mid1 mid2 label - return (Range start end) - --- Like linkRange but without actually linking -spanRange :: Range -> Range -> Range -spanRange (Range start mid1) (Range mid2 end) = Range start end - -linkRanges :: [Range] -> CFM Range -linkRanges [] = error "Empty range" -linkRanges (first:rest) = foldM linkRange first rest - -sequentially :: [Token] -> CFM Range -sequentially list = do - first <- newStructuralNode - rest <- mapM build list - linkRanges (first:rest) - -withContext :: (CFContext -> CFContext) -> CFM a -> CFM a -withContext = local - -withReturn :: Range -> CFM a -> CFM a -withReturn _ p = p - -asCondition :: CFM Range -> CFM Range -asCondition = withContext (\c -> c { cfIsCondition = True }) - -newStructuralNode = newNodeRange CFStructuralNode - -buildRoot :: Token -> CFM Range -buildRoot t = under (getId t) $ do - entry <- newNodeRange $ CFEntryPoint "MAIN" - impliedExit <- newNode CFImpliedExit - end <- newNode CFStructuralNode - start <- local (\c -> c { cfExitTarget = Just end, cfReturnTarget = Just impliedExit}) $ build t - range <- linkRanges [entry, start, nodeToRange impliedExit, nodeToRange end] - registerNode (getId t) range - return range - -applySingle e = CFApplyEffects [e] - --- Build the CFG. -build :: Token -> CFM Range -build t = do - range <- under (getId t) $ build' t - registerNode (getId t) range - return range - where - build' t = case t of - T_Annotation _ _ list -> build list - T_Script _ _ list -> do - sequentially list - - TA_Assignment id op var@(TA_Variable _ name indices) rhs -> do - -- value first: (( var[x=1] = (x=2) )) runs x=1 last - value <- build rhs - subscript <- sequentially indices - read <- - if op == "=" - then none - -- This is += or something - else newNodeRange $ applySingle $ IdTagged id $ CFReadVariable name - - write <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name $ - if null indices - then CFValueInteger - else CFValueArray - - linkRanges [value, subscript, read, write] - - TA_Assignment id op lhs rhs -> do - -- This is likely an invalid assignment like (( 1 = 2 )), but it - -- could be e.g. x=y; (( $x = 3 )); echo $y, so expand both sides - -- without updating anything - sequentially [lhs, rhs] - - TA_Binary _ _ a b -> sequentially [a,b] - TA_Expansion _ list -> sequentially list - TA_Sequence _ list -> sequentially list - TA_Parenthesis _ t -> build t - - TA_Trinary _ cond a b -> do - condition <- build cond - ifthen <- build a - elsethen <- build b - end <- newStructuralNode - linkRanges [condition, ifthen, end] - linkRanges [condition, elsethen, end] - - TA_Variable id name indices -> do - subscript <- sequentially indices - hint <- - if null indices - then none - else nodeToRange <$> newNode (applySingle $ IdTagged id $ CFHintArray name) - read <- nodeToRange <$> newNode (applySingle $ IdTagged id $ CFReadVariable name) - linkRanges [subscript, hint, read] - - TA_Unary id op (TA_Variable _ name indices) | "--" `isInfixOf` op || "++" `isInfixOf` op -> do - subscript <- sequentially indices - read <- newNodeRange $ applySingle $ IdTagged id $ CFReadVariable name - write <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name $ - if null indices - then CFValueInteger - else CFValueArray - linkRanges [subscript, read, write] - TA_Unary _ _ arg -> build arg - - TC_And _ SingleBracket _ lhs rhs -> do - sequentially [lhs, rhs] - - TC_And _ DoubleBracket _ lhs rhs -> do - left <- build lhs - right <- build rhs - end <- newStructuralNode - -- complete - linkRanges [left, right, end] - -- short circuit - linkRange left end - - -- TODO: Handle integer ops - TC_Binary _ mode str lhs rhs -> do - left <- build lhs - right <- build rhs - linkRange left right - - TC_Empty {} -> newStructuralNode - - TC_Group _ _ t -> build t - - -- TODO: Mark as checked - TC_Nullary _ _ arg -> build arg - - TC_Or _ SingleBracket _ lhs rhs -> sequentially [lhs, rhs] - - TC_Or _ DoubleBracket _ lhs rhs -> do - left <- build lhs - right <- build rhs - end <- newStructuralNode - -- complete - linkRanges [left, right, end] - -- short circuit - linkRange left end - - -- TODO: Handle -v, -z, -n - TC_Unary _ _ op arg -> do - build arg - - T_Arithmetic id root -> do - exe <- build root - status <- newNodeRange (CFSetExitCode id) - linkRange exe status - - T_AndIf _ lhs rhs -> do - left <- build lhs - right <- build rhs - end <- newStructuralNode - linkRange left right - linkRange right end - linkRange left end - - T_Array _ list -> sequentially list - - T_Assignment {} -> buildAssignment Nothing t - - T_Backgrounded id body -> do - start <- newStructuralNode - fork <- subshell id "backgrounding '&'" $ build body - pid <- newNodeRange $ CFSetBackgroundPid id - status <- newNodeRange $ CFSetExitCode id - - linkRange start fork - -- Add a join from the fork to warn about variable changes - linkRangeAs CFEFalseFlow fork pid - linkRanges [start, pid, status] - - T_Backticked id body -> - subshell id "`..` expansion" $ sequentially body - - T_Banged id cmd -> do - main <- build cmd - status <- newNodeRange (CFSetExitCode id) - linkRange main status - - T_BatsTest id _ body -> do - -- These are technically set by the 'run' command, but we'll just define them - -- up front to avoid figuring out which commands named "run" belong to Bats. - status <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable "status" CFValueInteger - output <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable "output" CFValueString - main <- build body - linkRanges [status, output, main] - - T_BraceExpansion _ list -> sequentially list - - T_BraceGroup id body -> - sequentially body - - T_CaseExpression id t [] -> build t - - T_CaseExpression id t list@(hd:tl) -> do - start <- newStructuralNode - token <- build t - branches <- mapM buildBranch (hd NE.:| tl) - end <- newStructuralNode - - let neighbors = zip (NE.toList branches) $ NE.tail branches - let (_, firstCond, _) = NE.head branches - let (_, lastCond, lastBody) = NE.last branches - - linkRange start token - linkRange token firstCond - mapM_ (uncurry $ linkBranch end) neighbors - linkRange lastBody end - - unless (any hasCatchAll list) $ - -- There's no *) branch, so assume we can fall through - void $ linkRange token end - - return $ spanRange start end - - where - -- for a | b | c, evaluate each in turn and allow short circuiting - buildCond list = do - start <- newStructuralNode - conds <- mapM build list - end <- newStructuralNode - linkRanges (start:conds) - mapM_ (`linkRange` end) conds - return $ spanRange start end - - buildBranch (typ, cond, body) = do - c <- buildCond cond - b <- sequentially body - linkRange c b - return (typ, c, b) - - linkBranch end (typ, cond, body) (_, nextCond, nextBody) = do - -- Failure case - linkRange cond nextCond - -- After body - case typ of - CaseBreak -> linkRange body end - CaseFallThrough -> linkRange body nextBody - CaseContinue -> linkRange body nextCond - - -- Find a *) if any - - hasCatchAll (_,cond,_) = any isCatchAll cond - isCatchAll c = fromMaybe False $ do - pg <- wordToExactPseudoGlob c - return $ pg `pseudoGlobIsSuperSetof` [PGMany] - - T_Condition id _ op -> do - cond <- build op - status <- newNodeRange $ CFSetExitCode id - linkRange cond status - - T_CoProc id maybeNameToken t -> do - -- If unspecified, "COPROC". If not a constant string, Nothing. - let maybeName = case maybeNameToken of - Just x -> getLiteralString x - Nothing -> Just "COPROC" - - let parentNode = case maybeName of - Just str -> applySingle $ IdTagged id $ CFWriteVariable str CFValueArray - Nothing -> CFStructuralNode - - start <- newStructuralNode - parent <- newNodeRange parentNode - child <- subshell id "coproc" $ build t - end <- newNodeRange $ CFSetExitCode id - - linkRange start parent - linkRange start child - linkRange parent end - linkRangeAs CFEFalseFlow child end - - return $ spanRange start end - T_CoProcBody _ t -> build t - - T_DollarArithmetic _ arith -> build arith - T_DollarDoubleQuoted _ list -> sequentially list - T_DollarSingleQuoted _ _ -> none - T_DollarBracket _ t -> build t - - T_DollarBraced id _ t -> do - let str = concat $ oversimplify t - let modifier = getBracedModifier str - let reference = getBracedReference str - let indices = getIndexReferences str - let offsets = getOffsetReferences str - vals <- build t - others <- mapM (\x -> nodeToRange <$> newNode (applySingle $ IdTagged id $ CFReadVariable x)) (indices ++ offsets) - deps <- linkRanges (vals:others) - read <- nodeToRange <$> newNode (applySingle $ IdTagged id $ CFReadVariable reference) - totalRead <- linkRange deps read - - if any (`isPrefixOf` modifier) ["=", ":="] - then do - optionalAssign <- newNodeRange (applySingle $ IdTagged id $ CFWriteVariable reference CFValueString) - result <- newStructuralNode - linkRange optionalAssign result - linkRange totalRead result - else return totalRead - - T_DollarBraceCommandExpansion id _ body -> - sequentially body - - T_DoubleQuoted _ list -> sequentially list - - T_DollarExpansion id body -> - subshell id "$(..) expansion" $ sequentially body - - T_Extglob _ _ list -> sequentially list - - T_FdRedirect id ('{':identifier) op -> do - let name = takeWhile (/= '}') identifier - expression <- build op - rw <- newNodeRange $ - if isClosingFileOp op - then applySingle $ IdTagged id $ CFReadVariable name - else applySingle $ IdTagged id $ CFWriteVariable name CFValueInteger - - linkRange expression rw - - - T_FdRedirect _ name t -> do - build t - - T_ForArithmetic _ initT condT incT bodyT -> do - init <- build initT - cond <- build condT - body <- sequentially bodyT - inc <- build incT - end <- newStructuralNode - - -- Forward edges - linkRanges [init, cond, body, inc] - linkRange cond end - -- Backward edge - linkRange inc cond - return $ spanRange init end - - T_ForIn id name words body -> forInHelper id name words body - - -- For functions we generate an unlinked subgraph, and mention that in its definition node - T_Function id _ _ name body -> do - range <- local (\c -> c { cfExitTarget = Nothing }) $ do - entry <- newNodeRange $ CFEntryPoint $ "function " ++ name - f <- withFunctionScope $ build body - linkRange entry f - let (Range entry exit) = range - definition <- newNodeRange (applySingle $ IdTagged id $ CFDefineFunction name id entry exit) - exe <- newNodeRange (CFSetExitCode id) - linkRange definition exe - - T_Glob {} -> none - - T_HereString _ t -> build t - T_HereDoc _ _ _ _ list -> sequentially list - - T_IfExpression id ifs elses -> do - start <- newStructuralNode - branches <- doBranches start ifs elses [] - end <- newStructuralNode - mapM_ (`linkRange` end) branches - return $ spanRange start end - where - doBranches start ((conds, thens):rest) elses result = do - cond <- asCondition $ sequentially conds - action <- sequentially thens - linkRange start cond - linkRange cond action - doBranches cond rest elses (action:result) - doBranches start [] elses result = do - rest <- - if null elses - then newNodeRange (CFSetExitCode id) - else sequentially elses - linkRange start rest - return (rest:result) - - T_Include _ t -> build t - - T_IndexedElement _ indicesT valueT -> do - indices <- sequentially indicesT - value <- build valueT - linkRange indices value - - T_IoDuplicate _ op _ -> build op - - T_IoFile _ op t -> do - exp <- build t - doesntDoMuch <- build op - linkRange exp doesntDoMuch - - T_Literal {} -> none - - T_NormalWord _ list -> sequentially list - - T_OrIf _ lhs rhs -> do - left <- build lhs - right <- build rhs - end <- newStructuralNode - linkRange left right - linkRange right end - linkRange left end - - T_Pipeline _ _ [cmd] -> build cmd - T_Pipeline id _ cmds -> do - start <- newStructuralNode - hasLastpipe <- reader $ cfLastpipe . cfParameters - (leading, last) <- buildPipe hasLastpipe cmds - -- Ideally we'd let this exit code be that of the last command in the pipeline but ok - end <- newNodeRange $ CFSetExitCode id - - mapM_ (linkRange start) leading - mapM_ (\c -> linkRangeAs CFEFalseFlow c end) leading - linkRanges $ [start] ++ last ++ [end] - where - buildPipe True [x] = do - last <- build x - return ([], [last]) - buildPipe lp (first:rest) = do - this <- subshell id "pipeline" $ build first - (leading, last) <- buildPipe lp rest - return (this:leading, last) - buildPipe _ [] = return ([], []) - - T_ProcSub id op cmds -> do - start <- newStructuralNode - body <- subshell id (op ++ "() process substitution") $ sequentially cmds - end <- newStructuralNode - - linkRange start body - linkRangeAs CFEFalseFlow body end - linkRange start end - - T_Redirecting _ redirs cmd -> do - -- For simple commands, this is the other way around in bash - -- We do it in this order for comound commands like { x=name; } > "$x" - redir <- sequentially redirs - body <- build cmd - linkRange redir body - - T_SelectIn id name words body -> forInHelper id name words body - - T_SimpleCommand id vars [] -> do - -- Vars can also be empty, as in the command "> foo" - assignments <- sequentially vars - status <- newNodeRange (CFSetExitCode id) - linkRange assignments status - - T_SimpleCommand id vars (cmd:args) -> - handleCommand t vars (cmd NE.:| args) $ getUnquotedLiteral cmd - - T_SingleQuoted _ _ -> none - - T_SourceCommand _ originalCommand inlinedSource -> do - cmd <- build originalCommand - end <- newStructuralNode - inline <- withReturn end $ build inlinedSource - linkRange cmd inline - linkRange inline end - return $ spanRange cmd inline - - T_Subshell id body -> do - main <- subshell id "explicit (..) subshell" $ sequentially body - status <- newNodeRange (CFSetExitCode id) - linkRange main status - - T_UntilExpression id cond body -> whileHelper id cond body - T_WhileExpression id cond body -> whileHelper id cond body - - T_CLOBBER _ -> none - T_GREATAND _ -> none - T_LESSAND _ -> none - T_LESSGREAT _ -> none - T_DGREAT _ -> none - T_Greater _ -> none - T_Less _ -> none - T_ParamSubSpecialChar _ _ -> none - - x -> do - error ("Unimplemented: " ++ show x) -- STRIP - none - --- Still in `where` clause - forInHelper id name words body = do - entry <- newStructuralNode - expansion <- sequentially words - assignmentChoice <- newStructuralNode - assignments <- - if null words || any willSplit words - then (:[]) <$> (newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueString) - else mapM (\t -> newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name $ CFValueComputed (getId t) $ tokenToParts t) words - body <- sequentially body - exit <- newStructuralNode - -- Forward edges - linkRanges [entry, expansion, assignmentChoice] - mapM_ (\t -> linkRanges [assignmentChoice, t, body]) assignments - linkRange body exit - linkRange expansion exit - -- Backward edge - linkRange body assignmentChoice - return $ spanRange entry exit - - whileHelper id cond body = do - condRange <- asCondition $ sequentially cond - bodyRange <- sequentially body - end <- newNodeRange (CFSetExitCode id) - - linkRange condRange bodyRange - linkRange bodyRange condRange - linkRange condRange end - - -handleCommand cmd vars args literalCmd = do - -- TODO: Handle assignments in declaring commands - - case literalCmd of - Just "exit" -> regularExpansion vars (NE.toList args) $ handleExit - Just "return" -> regularExpansion vars (NE.toList args) $ handleReturn - Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args - - Just "declare" -> handleDeclare args - Just "local" -> handleDeclare args - Just "typeset" -> handleDeclare args - - Just "printf" -> regularExpansionWithStatus vars args $ handlePrintf args - Just "wait" -> regularExpansionWithStatus vars args $ handleWait args - - Just "mapfile" -> regularExpansionWithStatus vars args $ handleMapfile args - Just "readarray" -> regularExpansionWithStatus vars args $ handleMapfile args - - Just "read" -> regularExpansionWithStatus vars args $ handleRead args - - Just "DEFINE_boolean" -> regularExpansionWithStatus vars args $ handleDEFINE args - Just "DEFINE_float" -> regularExpansionWithStatus vars args $ handleDEFINE args - Just "DEFINE_integer" -> regularExpansionWithStatus vars args $ handleDEFINE args - Just "DEFINE_string" -> regularExpansionWithStatus vars args $ handleDEFINE args - - -- This will mostly behave like 'command' but ok - Just "builtin" -> - case args of - _ NE.:| [] -> regular - (_ NE.:| newcmd:newargs) -> - handleCommand newcmd vars (newcmd NE.:| newargs) $ getLiteralString newcmd - Just "command" -> - case args of - _ NE.:| [] -> regular - (_ NE.:| newcmd:newargs) -> - handleOthers (getId newcmd) vars (newcmd NE.:| newargs) $ getLiteralString newcmd - _ -> regular - - where - regular = handleOthers (getId cmd) vars args literalCmd - handleExit = do - exitNode <- reader cfExitTarget - case exitNode of - Just target -> do - exit <- newNode CFResolvedExit - link exit target CFEExit - unreachable <- newNode CFUnreachable - return $ Range exit unreachable - Nothing -> do - exit <- newNode CFUnresolvedExit - unreachable <- newNode CFUnreachable - return $ Range exit unreachable - - handleReturn = do - returnTarget <- reader cfReturnTarget - case returnTarget of - Nothing -> error $ pleaseReport "missing return target" - Just target -> do - ret <- newNode CFStructuralNode - link ret target CFEFlow - unreachable <- newNode CFUnreachable - return $ Range ret unreachable - - handleUnset (cmd NE.:| args) = do - case () of - _ | "n" `elem` flagNames -> unsetWith CFUndefineNameref - _ | "v" `elem` flagNames -> unsetWith CFUndefineVariable - _ | "f" `elem` flagNames -> unsetWith CFUndefineFunction - _ -> unsetWith CFUndefine - where - pairs :: [(String, Token)] -- [(Flag string, token)] e.g. [("-f", t), ("", myfunc)] - pairs = map (\(str, (flag, val)) -> (str, flag)) $ fromMaybe (map (\c -> ("", (c,c))) args) $ getGnuOpts "vfn" args - (names, flags) = partition (null . fst) pairs - flagNames = map fst flags - literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")] - literalNames = mapMaybe (\(_, t) -> (,) t <$> getLiteralString t) names - -- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id - unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames - - - variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)=" - - handleDeclare (cmd NE.:| args) = do - isFunc <- asks cfIsFunction - -- This is a bit of a kludge: we don't have great support for things like - -- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x - let (evaluated, assignments, added, removed) = mconcat $ map (toEffects isFunc) args - before <- sequentially $ evaluated - assignments <- newNodeRange $ CFApplyEffects assignments - addedProps <- if null added then newStructuralNode else newNodeRange $ CFApplyEffects added - removedProps <- if null removed then newStructuralNode else newNodeRange $ CFApplyEffects removed - result <- newNodeRange $ CFSetExitCode (getId cmd) - linkRanges [before, assignments, addedProps, removedProps, result] - where - opts = map fst $ getGenericOpts args - array = "a" `elem` opts || associative - associative = "A" `elem` opts - integer = "i" `elem` opts - func = "f" `elem` opts || "F" `elem` opts - global = "g" `elem` opts - export = "x" `elem` opts - writer isFunc = - case () of - _ | global -> CFWriteGlobal - _ | isFunc -> CFWriteLocal - _ -> CFWriteVariable - - scope isFunc = - case () of - _ | global -> Just GlobalScope - _ | isFunc -> Just LocalScope - _ -> Nothing - - addedProps = S.fromList $ concat $ [ - [ CFVPArray | array ], - [ CFVPInteger | integer ], - [ CFVPExport | export ], - [ CFVPAssociative | associative ] - ] - - removedProps = S.fromList $ concat $ [ - -- Array property can't be unset - [ CFVPInteger | 'i' `elem` unsetOptions ], - [ CFVPExport | 'e' `elem` unsetOptions ] - ] - - toEffects isFunc (T_Assignment id mode var idx t) = - let - pre = idx ++ [t] - val = [ IdTagged id $ (writer isFunc) var $ CFValueComputed (getId t) $ [ CFStringVariable var | mode == Append ] ++ tokenToParts t ] - added = [ IdTagged id $ CFSetProps (scope isFunc) var addedProps | not $ S.null addedProps ] - removed = [ IdTagged id $ CFUnsetProps (scope isFunc) var addedProps | not $ S.null removedProps ] - in - (pre, val, added, removed) - - toEffects isFunc t = - let - id = getId t - pre = [t] - literal = getLiteralStringDef "\0" t - isKnown = '\0' `notElem` literal - match = fmap head $ variableAssignRegex `matchRegex` literal - name = fromMaybe literal match - - asLiteral = - IdTagged id $ (writer isFunc) name $ - CFValueComputed (getId t) [ CFStringLiteral $ drop 1 $ dropWhile (/= '=') $ literal ] - asUnknown = - IdTagged id $ (writer isFunc) name $ - CFValueString - - added = [ IdTagged id $ CFSetProps (scope isFunc) name addedProps ] - removed = [ IdTagged id $ CFUnsetProps (scope isFunc) name removedProps ] - - in - case () of - _ | not (isVariableName name) -> (pre, [], [], []) - _ | isJust match && isKnown -> (pre, [asLiteral], added, removed) - _ | isJust match -> (pre, [asUnknown], added, removed) - -- e.g. declare -i x - _ -> (pre, [], added, removed) - - -- find "ia" from `define +i +a` - unsetOptions :: String - unsetOptions = - let - strings = mapMaybe getLiteralString args - plusses = filter ("+" `isPrefixOf`) strings - in - concatMap (drop 1) plusses - - handlePrintf (cmd NE.:| args) = - newNodeRange $ CFApplyEffects $ maybeToList findVar - where - findVar = do - flags <- getBsdOpts "v:" args - (flag, arg) <- lookup "v" flags - name <- getLiteralString arg - return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString - - handleWait (cmd NE.:| args) = - newNodeRange $ CFApplyEffects $ maybeToList findVar - where - findVar = do - let flags = getGenericOpts args - (flag, arg) <- lookup "p" flags - name <- getLiteralString arg - return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger - - handleMapfile (cmd NE.:| args) = - newNodeRange $ CFApplyEffects [findVar] - where - findVar = - let (id, name) = fromMaybe (getId cmd, "MAPFILE") $ getFromArg `mplus` getFromFallback - in IdTagged id $ CFWriteVariable name CFValueArray - - getFromArg = do - flags <- getGnuOpts flagsForMapfile args - (_, arg) <- lookup "" flags - name <- getLiteralString arg - return (getId arg, name) - - getFromFallback = - listToMaybe $ mapMaybe getIfVar $ reverse args - getIfVar c = do - name <- getLiteralString c - guard $ isVariableName name - return (getId c, name) - - handleRead (cmd NE.:| args) = newNodeRange $ CFApplyEffects main - where - main = fromMaybe fallback $ do - flags <- getGnuOpts flagsForRead args - return $ fromMaybe (withFields flags) $ withArray flags - - withArray :: [(String, (Token, Token))] -> Maybe [IdTagged CFEffect] - withArray flags = do - (_, token) <- lookup "a" flags - return $ fromMaybe [] $ do - name <- getLiteralString token - return [ IdTagged (getId token) $ CFWriteVariable name CFValueArray ] - - withFields flags = mapMaybe getAssignment flags - - getAssignment :: (String, (Token, Token)) -> Maybe (IdTagged CFEffect) - getAssignment f = do - ("", (t, _)) <- return f - name <- getLiteralString t - return $ IdTagged (getId t) $ CFWriteVariable name CFValueString - - fallback = - let - names = reverse $ map fromJust $ takeWhile isJust $ map (\c -> sequence (getId c, getLiteralString c)) $ reverse args - namesOrDefault = if null names then [(getId cmd, "REPLY")] else names - hasDashA = any (== "a") $ map fst $ getGenericOpts args - value = if hasDashA then CFValueArray else CFValueString - in - map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault - - handleDEFINE (cmd NE.:| args) = - newNodeRange $ CFApplyEffects $ maybeToList findVar - where - findVar = do - name <- listToMaybe $ drop 1 args - str <- getLiteralString name - guard $ isVariableName str - return $ IdTagged (getId name) $ CFWriteVariable str CFValueString - - handleOthers id vars args cmd = - regularExpansion vars (NE.toList args) $ do - exe <- newNodeRange $ CFExecuteCommand cmd - status <- newNodeRange $ CFSetExitCode id - linkRange exe status - - regularExpansion vars args p = do - args <- sequentially args - assignments <- mapM (buildAssignment (Just PrefixScope)) vars - exe <- p - dropAssignments <- - if null vars - then - return [] - else do - drop <- newNodeRange CFDropPrefixAssignments - return [drop] - - linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments - - regularExpansionWithStatus vars args@(cmd NE.:| _) p = do - initial <- regularExpansion vars (NE.toList args) p - status <- newNodeRange $ CFSetExitCode (getId cmd) - linkRange initial status - - -none = newStructuralNode - -data Scope = GlobalScope | LocalScope | PrefixScope - deriving (Eq, Ord, Show, Generic, NFData) - -buildAssignment scope t = do - op <- case t of - T_Assignment id mode var indices value -> do - expand <- build value - index <- sequentially indices - read <- case mode of - Append -> newNodeRange (applySingle $ IdTagged id $ CFReadVariable var) - Assign -> none - let valueType = if null indices then f id value else CFValueArray - let scoper = - case scope of - Just PrefixScope -> CFWritePrefix - Just LocalScope -> CFWriteLocal - Just GlobalScope -> CFWriteGlobal - Nothing -> CFWriteVariable - write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType - linkRanges [expand, index, read, write] - where - f :: Id -> Token -> CFValue - f id t@T_NormalWord {} = CFValueComputed id $ [CFStringVariable var | mode == Append] ++ tokenToParts t - f id t@(T_Literal _ str) = CFValueComputed id $ [CFStringVariable var | mode == Append] ++ tokenToParts t - f _ T_Array {} = CFValueArray - - registerNode (getId t) op - return op - - -tokenToParts t = - case t of - T_NormalWord _ list -> concatMap tokenToParts list - T_DoubleQuoted _ list -> concatMap tokenToParts list - T_SingleQuoted _ str -> [ CFStringLiteral str ] - T_Literal _ str -> [ CFStringLiteral str ] - T_DollarArithmetic {} -> [ CFStringInteger ] - T_DollarBracket {} -> [ CFStringInteger ] - T_DollarBraced _ _ list | isUnmodifiedParameterExpansion t -> [ CFStringVariable (getBracedReference $ concat $ oversimplify list) ] - -- Check if getLiteralString can handle it, if not it's unknown - _ -> [maybe CFStringUnknown CFStringLiteral $ getLiteralString t] - - --- Like & but well defined when the node already exists -safeUpdate ctx@(_,node,_,_) graph = ctx & (delNode node graph) - --- Change all subshell invocations to instead link directly to their contents. --- This is used for producing dominator trees. -inlineSubshells :: CFGraph -> CFGraph -inlineSubshells graph = relinkedGraph - where - subshells = ufold find [] graph - find (incoming, node, label, outgoing) acc = - case label of - CFExecuteSubshell _ start end -> (node, label, start, end, incoming, outgoing):acc - _ -> acc - - relinkedGraph = foldl' relink graph subshells - relink graph (node, label, start, end, incoming, outgoing) = - let - -- Link CFExecuteSubshell to the CFEntryPoint - subshellToStart = (incoming, node, label, [(CFEFlow, start)]) - -- Link the subshell exit to the - endToNexts = (endIncoming, endNode, endLabel, outgoing) - (endIncoming, endNode, endLabel, _) = context graph end - in - subshellToStart `safeUpdate` (endToNexts `safeUpdate` graph) - -findEntryNodes :: CFGraph -> [Node] -findEntryNodes graph = ufold find [] graph - where - find (incoming, node, label, _) list = - case label of - CFEntryPoint {} | null incoming -> node:list - _ -> list - -findDominators main graph = asSetMap - where - inlined = inlineSubshells graph - entryNodes = main : findEntryNodes graph - asLists = concatMap (dom inlined) entryNodes - asSetMap = M.fromList $ map (\(node, list) -> (node, S.fromList list)) asLists - -findTerminalNodes :: CFGraph -> [Node] -findTerminalNodes graph = ufold find [] graph - where - find (_, node, label, _) list = - case label of - CFUnresolvedExit -> node:list - CFApplyEffects effects -> f effects list - _ -> list - - f [] list = list - f (IdTagged _ (CFDefineFunction _ id start end):rest) list = f rest (end:list) - f (_:rest) list = f rest list - -findPostDominators :: Node -> CFGraph -> Array Node [Node] -findPostDominators mainexit graph = asArray - where - inlined = inlineSubshells graph - terminals = findTerminalNodes inlined - (incoming, _, label, outgoing) = context graph mainexit - withExitEdges = (incoming ++ map (\c -> (CFEFlow, c)) terminals, mainexit, label, outgoing) `safeUpdate` inlined - reversed = grev withExitEdges - postDoms = dom reversed mainexit - (_, maxNode) = nodeRange graph - -- Holes in the array cause "Exception: (Array.!): undefined array element" while - -- inspecting/debugging, so fill the array first and then update. - initializedArray = listArray (0, maxNode) $ repeat [] - asArray = initializedArray // postDoms - -return [] -runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/CFGAnalysis.hs b/src/ShellCheck/CFGAnalysis.hs deleted file mode 100644 index cf982e0..0000000 --- a/src/ShellCheck/CFGAnalysis.hs +++ /dev/null @@ -1,1439 +0,0 @@ -{- - Copyright 2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE DeriveAnyClass, DeriveGeneric #-} -{-# LANGUAGE CPP #-} - -{- - Data Flow Analysis on a Control Flow Graph. - - This module implements a pretty standard iterative Data Flow Analysis. - For an overview of the process, see Wikipedia. - - Since shell scripts rely heavily on global variables, this DFA includes - tracking the value of globals across calls. Each function invocation is - treated as a separate DFA problem, and a caching mechanism (hopefully) - avoids any exponential explosions. - - To do efficient DFA join operations (or merges, as the code calls them), - some of the data structures have an integer version attached. On update, - the version is changed. If two states have the same version number, - a merge is skipped on the grounds that they are identical. It is easy - to unintentionally forget to update/invalidate the version number, - and bugs will ensure. - - For performance reasons, the entire code runs in plain ST, with a manual - context object Ctx being passed around. It relies heavily on mutable - STRefs. However, this turned out to be literally thousands of times faster - than my several attempts using RWST, so it can't be helped. --} - -module ShellCheck.CFGAnalysis ( - analyzeControlFlow - ,CFGParameters (..) - ,CFGAnalysis (..) - ,ProgramState (..) - ,VariableState (..) - ,VariableValue (..) - ,VariableProperties - ,SpaceStatus (..) - ,NumericalStatus (..) - ,getIncomingState - ,getOutgoingState - ,doesPostDominate - ,variableMayBeDeclaredInteger - ,variableMayBeAssignedInteger - ,ShellCheck.CFGAnalysis.runTests -- STRIP - ) where - -import Control.DeepSeq -import Control.Monad -import Control.Monad.ST -import Data.Array.Unboxed -import Data.Char -import Data.Graph.Inductive.Graph -import Data.Graph.Inductive.Query.DFS -import Data.List hiding (map) -import Data.Maybe -import Data.STRef -import Debug.Trace -- STRIP -import GHC.Generics (Generic) -import qualified Data.Map as M -import qualified Data.Set as S -import qualified ShellCheck.Data as Data -import ShellCheck.AST -import ShellCheck.CFG -import ShellCheck.Prelude - -import Test.QuickCheck - - --- The number of iterations for DFA to stabilize -iterationCount = 1000000 --- There have been multiple bugs where bad caching caused oscillations. --- As a precaution, disable caching if there's this many iterations left. -fallbackThreshold = 10000 --- The number of cache entries to keep per node -cacheEntries = 10 - -logVerbose log = do - -- traceShowM log - return () -logInfo log = do - -- traceShowM log - return () - --- The result of the data flow analysis -data CFGAnalysis = CFGAnalysis { - graph :: CFGraph, - tokenToRange :: M.Map Id (Node, Node), - tokenToNodes :: M.Map Id (S.Set Node), - postDominators :: Array Node [Node], - nodeToData :: M.Map Node (ProgramState, ProgramState) -} deriving (Show) - --- The program state we expose externally -data ProgramState = ProgramState { - -- internalState :: InternalState, -- For debugging - variablesInScope :: M.Map String VariableState, - exitCodes :: S.Set Id, - stateIsReachable :: Bool -} deriving (Show, Eq, Generic, NFData) - -internalToExternal :: InternalState -> ProgramState -internalToExternal s = - ProgramState { - -- Censor the literal value to avoid introducing dependencies on it. It's just for debugging. - variablesInScope = M.map censor flatVars, - -- internalState = s, -- For debugging - exitCodes = fromMaybe S.empty $ sExitCodes s, - stateIsReachable = fromMaybe True $ sIsReachable s - } - where - censor s = s { - variableValue = (variableValue s) { - literalValue = Nothing - } - } - flatVars = M.unions $ map mapStorage [sPrefixValues s, sLocalValues s, sGlobalValues s] - --- Conveniently get the state before a token id -getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState -getIncomingState analysis id = do - (start,end) <- M.lookup id $ tokenToRange analysis - fst <$> M.lookup start (nodeToData analysis) - --- Conveniently get the state after a token id -getOutgoingState :: CFGAnalysis -> Id -> Maybe ProgramState -getOutgoingState analysis id = do - (start,end) <- M.lookup id $ tokenToRange analysis - snd <$> M.lookup end (nodeToData analysis) - --- Conveniently determine whether one node postdominates another, --- i.e. whether 'target' always unconditionally runs after 'base'. -doesPostDominate :: CFGAnalysis -> Id -> Id -> Bool -doesPostDominate analysis target base = fromMaybe False $ do - (_, baseEnd) <- M.lookup base $ tokenToRange analysis - (targetStart, _) <- M.lookup target $ tokenToRange analysis - return $ targetStart `elem` (postDominators analysis ! baseEnd) - --- See if any execution path results in the variable containing a state -variableMayHaveState :: ProgramState -> String -> CFVariableProp -> Maybe Bool -variableMayHaveState state var property = do - value <- M.lookup var $ variablesInScope state - return $ any (S.member property) $ variableProperties value - --- See if any execution path declares the variable an integer (declare -i). -variableMayBeDeclaredInteger state var = variableMayHaveState state var CFVPInteger - --- See if any execution path suggests the variable may contain an integer value -variableMayBeAssignedInteger state var = do - value <- M.lookup var $ variablesInScope state - return $ (numericalStatus $ variableValue value) >= NumericalStatusMaybe - -getDataForNode analysis node = M.lookup node $ nodeToData analysis - --- The current state of data flow at a point in the program, potentially as a diff -data InternalState = InternalState { - sVersion :: Integer, - sGlobalValues :: VersionedMap String VariableState, - sLocalValues :: VersionedMap String VariableState, - sPrefixValues :: VersionedMap String VariableState, - sFunctionTargets :: VersionedMap String FunctionValue, - sExitCodes :: Maybe (S.Set Id), - sIsReachable :: Maybe Bool -} deriving (Show, Generic, NFData) - -newInternalState = InternalState { - sVersion = 0, - sGlobalValues = vmEmpty, - sLocalValues = vmEmpty, - sPrefixValues = vmEmpty, - sFunctionTargets = vmEmpty, - sExitCodes = Nothing, - sIsReachable = Nothing -} - -unreachableState = modified newInternalState { - sIsReachable = Just False -} - --- The default state we assume we get from the environment -createEnvironmentState :: InternalState -createEnvironmentState = do - foldl' (flip ($)) newInternalState $ concat [ - addVars Data.internalVariables unknownVariableState, - addVars Data.variablesWithoutSpaces spacelessVariableState, - addVars Data.specialIntegerVariables integerVariableState - ] - where - addVars names val = map (\name -> insertGlobal name val) names - spacelessVariableState = unknownVariableState { - variableValue = VariableValue { - literalValue = Nothing, - spaceStatus = SpaceStatusClean, - numericalStatus = NumericalStatusUnknown - } - } - integerVariableState = unknownVariableState { - variableValue = unknownIntegerValue - } - - -modified s = s { sVersion = -1 } - -insertGlobal :: String -> VariableState -> InternalState -> InternalState -insertGlobal name value state = modified state { - sGlobalValues = vmInsert name value $ sGlobalValues state -} - -insertLocal :: String -> VariableState -> InternalState -> InternalState -insertLocal name value state = modified state { - sLocalValues = vmInsert name value $ sLocalValues state -} - -insertPrefix :: String -> VariableState -> InternalState -> InternalState -insertPrefix name value state = modified state { - sPrefixValues = vmInsert name value $ sPrefixValues state -} - -insertFunction :: String -> FunctionValue -> InternalState -> InternalState -insertFunction name value state = modified state { - sFunctionTargets = vmInsert name value $ sFunctionTargets state -} - -addProperties :: S.Set CFVariableProp -> VariableState -> VariableState -addProperties props state = state { - variableProperties = S.map (S.union props) $ variableProperties state -} - -removeProperties :: S.Set CFVariableProp -> VariableState -> VariableState -removeProperties props state = state { - variableProperties = S.map (\s -> S.difference s props) $ variableProperties state -} - -setExitCode id = setExitCodes (S.singleton id) -setExitCodes set state = modified state { - sExitCodes = Just $ set -} - --- Dependencies on values, e.g. "if there is a global variable named 'foo' without spaces" --- This is used to see if the DFA of a function would result in the same state, so anything --- that affects DFA must be tracked. -data StateDependency = - -- Complete variable state - DepState Scope String VariableState - -- Only variable properties (we need properties but not values for x=1) - | DepProperties Scope String VariableProperties - -- Function definition - | DepFunction String (S.Set FunctionDefinition) - -- Whether invoking the node would result in recursion (i.e., is the function on the stack?) - | DepIsRecursive Node Bool - -- The set of commands that could have provided the exit code $? - | DepExitCodes (S.Set Id) - deriving (Show, Eq, Ord, Generic, NFData) - --- A function definition, or lack thereof -data FunctionDefinition = FunctionUnknown | FunctionDefinition String Node Node - deriving (Show, Eq, Ord, Generic, NFData) - --- The Set of places a command name can point (it's a Set to handle conditionally defined functions) -type FunctionValue = S.Set FunctionDefinition - --- Create an InternalState that fulfills the given dependencies -depsToState :: S.Set StateDependency -> InternalState -depsToState set = foldl insert newInternalState $ S.toList set - where - insert :: InternalState -> StateDependency -> InternalState - insert state dep = - case dep of - DepFunction name val -> insertFunction name val state - DepState scope name val -> insertIn True scope name val state - -- State includes properties and more, so don't overwrite a state with properties - DepProperties scope name props -> insertIn False scope name unknownVariableState { variableProperties = props } state - DepIsRecursive _ _ -> state - DepExitCodes s -> setExitCodes s state - - insertIn overwrite scope name val state = - let - (mapToCheck, inserter) = - case scope of - PrefixScope -> (sPrefixValues, insertPrefix) - LocalScope -> (sLocalValues, insertLocal) - GlobalScope -> (sGlobalValues, insertGlobal) - - alreadyExists = isJust $ vmLookup name $ mapToCheck state - in - if overwrite || not alreadyExists - then inserter name val state - else state - -unknownFunctionValue = S.singleton FunctionUnknown - --- The information about the value of a single variable -data VariableValue = VariableValue { - literalValue :: Maybe String, -- TODO: For debugging. Remove me. - spaceStatus :: SpaceStatus, - numericalStatus :: NumericalStatus -} - deriving (Show, Eq, Ord, Generic, NFData) - -data VariableState = VariableState { - variableValue :: VariableValue, - variableProperties :: VariableProperties -} - deriving (Show, Eq, Ord, Generic, NFData) - --- Whether or not the value needs quoting (has spaces/globs), or we don't know -data SpaceStatus = SpaceStatusEmpty | SpaceStatusClean | SpaceStatusDirty deriving (Show, Eq, Ord, Generic, NFData) --- --- Whether or not the value needs quoting (has spaces/globs), or we don't know -data NumericalStatus = NumericalStatusUnknown | NumericalStatusEmpty | NumericalStatusMaybe | NumericalStatusDefinitely deriving (Show, Eq, Ord, Generic, NFData) - --- The set of possible sets of properties for this variable -type VariableProperties = S.Set (S.Set CFVariableProp) - -defaultProperties = S.singleton S.empty - -unknownVariableState = VariableState { - variableValue = unknownVariableValue, - variableProperties = defaultProperties -} - -unknownVariableValue = VariableValue { - literalValue = Nothing, - spaceStatus = SpaceStatusDirty, - numericalStatus = NumericalStatusUnknown -} - -emptyVariableValue = unknownVariableValue { - literalValue = Just "", - spaceStatus = SpaceStatusEmpty, - numericalStatus = NumericalStatusEmpty -} - -unsetVariableState = VariableState { - variableValue = emptyVariableValue, - variableProperties = defaultProperties -} - -mergeVariableState a b = VariableState { - variableValue = mergeVariableValue (variableValue a) (variableValue b), - variableProperties = S.union (variableProperties a) (variableProperties b) -} - -mergeVariableValue a b = VariableValue { - literalValue = if literalValue a == literalValue b then literalValue a else Nothing, - spaceStatus = mergeSpaceStatus (spaceStatus a) (spaceStatus b), - numericalStatus = mergeNumericalStatus (numericalStatus a) (numericalStatus b) -} - -mergeSpaceStatus a b = - case (a,b) of - (SpaceStatusEmpty, y) -> y - (x, SpaceStatusEmpty) -> x - (SpaceStatusClean, SpaceStatusClean) -> SpaceStatusClean - _ -> SpaceStatusDirty - -mergeNumericalStatus a b = - case (a,b) of - (NumericalStatusDefinitely, NumericalStatusDefinitely) -> NumericalStatusDefinitely - (NumericalStatusDefinitely, _) -> NumericalStatusMaybe - (_, NumericalStatusDefinitely) -> NumericalStatusMaybe - (NumericalStatusMaybe, _) -> NumericalStatusMaybe - (_, NumericalStatusMaybe) -> NumericalStatusMaybe - (NumericalStatusEmpty, NumericalStatusEmpty) -> NumericalStatusEmpty - _ -> NumericalStatusUnknown - --- A VersionedMap is a Map that keeps an additional integer version to quickly determine if it has changed. --- * Version -1 means it's unknown (possibly and presumably changed) --- * Version 0 means it's empty --- * Version N means it's equal to any other map with Version N (this is required but not enforced) -data VersionedMap k v = VersionedMap { - mapVersion :: Integer, - mapStorage :: M.Map k v -} - deriving (Generic, NFData) - --- This makes states more readable but inhibits copy-paste -instance (Show k, Show v) => Show (VersionedMap k v) where - show m = (if mapVersion m >= 0 then "V" ++ show (mapVersion m) else "U") ++ " " ++ show (mapStorage m) - -instance Eq InternalState where - (==) a b = stateIsQuickEqual a b || stateIsSlowEqual a b - -instance (Eq k, Eq v) => Eq (VersionedMap k v) where - (==) a b = vmIsQuickEqual a b || mapStorage a == mapStorage b - -instance (Ord k, Ord v) => Ord (VersionedMap k v) where - compare a b = - if vmIsQuickEqual a b - then EQ - else mapStorage a `compare` mapStorage b - - --- A context with STRefs manually passed around to function. --- This is done because it was dramatically much faster than any RWS type stack -data Ctx s = Ctx { - -- The current node - cNode :: STRef s Node, - -- The current input state - cInput :: STRef s InternalState, - -- The current output state - cOutput :: STRef s InternalState, - - -- The current functions/subshells stack - cStack :: [StackEntry s], - -- The input graph - cGraph :: CFGraph, - -- An incrementing counter to version maps - cCounter :: STRef s Integer, - -- A cache of input state dependencies to output effects - cCache :: STRef s (M.Map Node [(S.Set StateDependency, InternalState)]), - -- Whether the cache is enabled (see fallbackThreshold) - cEnableCache :: STRef s Bool, - -- The states resulting from data flows per invocation path - cInvocations :: STRef s (M.Map [Node] (S.Set StateDependency, M.Map Node (InternalState, InternalState))) -} - --- Whenever a function (or subshell) is invoked, a value like this is pushed onto the stack -data StackEntry s = StackEntry { - -- The entry point of this stack entry for the purpose of detecting recursion - entryPoint :: Node, - -- Whether this is a function call (as opposed to a subshell) - isFunctionCall :: Bool, - -- The node where this entry point was invoked - callSite :: Node, - -- A mutable set of dependencies we fetched from here or higher in the stack - dependencies :: STRef s (S.Set StateDependency), - -- The original input state for this stack entry - stackState :: InternalState -} - deriving (Eq, Generic, NFData) - -#if MIN_VERSION_deepseq(1,4,2) --- Our deepseq already has a STRef instance -#else --- Older deepseq (for GHC < 8) lacks this instance -instance NFData (STRef s a) where - rnf = (`seq` ()) -#endif - --- Overwrite a base state with the contents of a diff state --- This is unrelated to join/merge. -patchState :: InternalState -> InternalState -> InternalState -patchState base diff = - case () of - _ | sVersion diff == 0 -> base - _ | sVersion base == 0 -> diff - _ | stateIsQuickEqual base diff -> diff - _ -> - InternalState { - sVersion = -1, - sGlobalValues = vmPatch (sGlobalValues base) (sGlobalValues diff), - sLocalValues = vmPatch (sLocalValues base) (sLocalValues diff), - sPrefixValues = vmPatch (sPrefixValues base) (sPrefixValues diff), - sFunctionTargets = vmPatch (sFunctionTargets base) (sFunctionTargets diff), - sExitCodes = sExitCodes diff `mplus` sExitCodes base, - sIsReachable = sIsReachable diff `mplus` sIsReachable base - } - -patchOutputM ctx diff = do - let cOut = cOutput ctx - oldState <- readSTRef cOut - let newState = patchState oldState diff - writeSTRef cOut newState - --- Merge (aka Join) two states. This is monadic because it requires looking up --- values from the current context. For example: --- --- f() { --- foo || x=2 --- HERE # This merge requires looking up the value of $x in the parent frame --- } --- x=1 --- f -mergeState :: forall s. Ctx s -> InternalState -> InternalState -> ST s InternalState -mergeState ctx a b = do - -- Kludge: we want `readVariable` & friends not to read from an intermediate state, - -- so temporarily set a blank input. - let cin = cInput ctx - old <- readSTRef cin - writeSTRef cin newInternalState - x <- merge a b - writeSTRef cin old - return x - - where - - merge a b = - case () of - _ | sIsReachable a == Just True && sIsReachable b == Just False - || sIsReachable a == Just False && sIsReachable b == Just True -> - error $ pleaseReport "Unexpected merge of reachable and unreachable state" - _ | sIsReachable a == Just False && sIsReachable b == Just False -> - return unreachableState - _ | sVersion a >= 0 && sVersion b >= 0 && sVersion a == sVersion b -> return a - _ -> do - globals <- mergeMaps ctx mergeVariableState readGlobal (sGlobalValues a) (sGlobalValues b) - locals <- mergeMaps ctx mergeVariableState readVariable (sLocalValues a) (sLocalValues b) - prefix <- mergeMaps ctx mergeVariableState readVariable (sPrefixValues a) (sPrefixValues b) - funcs <- mergeMaps ctx S.union readFunction (sFunctionTargets a) (sFunctionTargets b) - exitCodes <- mergeMaybes ctx S.union readExitCodes (sExitCodes a) (sExitCodes b) - return $ InternalState { - sVersion = -1, - sGlobalValues = globals, - sLocalValues = locals, - sPrefixValues = prefix, - sFunctionTargets = funcs, - sExitCodes = exitCodes, - sIsReachable = liftM2 (&&) (sIsReachable a) (sIsReachable b) - } - --- Merge a number of states, or return a default if there are no states --- (it can't fold from newInternalState because this would be equivalent of adding a new input edge). -mergeStates :: forall s. Ctx s -> InternalState -> [InternalState] -> ST s InternalState -mergeStates ctx def list = - case list of - [] -> return def - (first:rest) -> foldM (mergeState ctx) first rest - --- Merge two maps, key by key. If both maps have a key, the 'merger' is used. --- If only one has the key, the 'reader' is used to fetch a second, and the two are merged as above. -mergeMaps :: (Ord k) => forall s. - Ctx s -> - (v -> v -> v) -> - (Ctx s -> k -> ST s v) -> - (VersionedMap k v) -> - (VersionedMap k v) -> - ST s (VersionedMap k v) -mergeMaps ctx merger reader a b = - if vmIsQuickEqual a b - then return a - else do - new <- M.fromDistinctAscList <$> reverse <$> f [] (M.toAscList $ mapStorage a) (M.toAscList $ mapStorage b) - vmFromMap ctx new - where - f l [] [] = return l - f l [] b = f l b [] - f l ((k,v):rest1) [] = do - other <- reader ctx k - f ((k, merger v other):l) rest1 [] - f l l1@((k1, v1):rest1) l2@((k2, v2):rest2) = - case k1 `compare` k2 of - EQ -> - f ((k1, merger v1 v2):l) rest1 rest2 - LT -> do - nv2 <- reader ctx k1 - f ((k1, merger v1 nv2):l) rest1 l2 - GT -> do - nv1 <- reader ctx k2 - f ((k2, merger nv1 v2):l) l1 rest2 - --- Merge two Maybes, like mergeMaps for a single element -mergeMaybes ctx merger reader a b = - case (a, b) of - (Nothing, Nothing) -> return Nothing - (Just v1, Nothing) -> single v1 - (Nothing, Just v2) -> single v2 - (Just v1, Just v2) -> return $ Just $ merger v1 v2 - where - single val = do - result <- merger val <$> reader ctx - return $ Just result - -vmFromMap ctx map = return $ VersionedMap { - mapVersion = -1, - mapStorage = map -} - --- Give a VersionedMap a version if it does not already have one. -versionMap ctx map = - if mapVersion map >= 0 - then return map - else do - v <- nextVersion ctx - return map { - mapVersion = v - } - --- Give an InternalState a version if it does not already have one. -versionState ctx state = - if sVersion state >= 0 - then return state - else do - self <- nextVersion ctx - ssGlobalValues <- versionMap ctx $ sGlobalValues state - ssLocalValues <- versionMap ctx $ sLocalValues state - ssFunctionTargets <- versionMap ctx $ sFunctionTargets state - return state { - sVersion = self, - sGlobalValues = ssGlobalValues, - sLocalValues = ssLocalValues, - sFunctionTargets = ssFunctionTargets - } - --- Like 'not null' but for 2+ elements -is2plus :: [a] -> Bool -is2plus l = case l of - _:_:_ -> True - _ -> False - --- Use versions to see if two states are trivially identical -stateIsQuickEqual a b = - let - va = sVersion a - vb = sVersion b - in - va >= 0 && vb >= 0 && va == vb - --- A manual slow path 'Eq' (it's not derived because it's part of the custom Eq instance) -stateIsSlowEqual a b = - check sGlobalValues - && check sLocalValues - && check sPrefixValues - && check sFunctionTargets - && check sIsReachable - where - check f = f a == f b - --- Check if two VersionedMaps are trivially equal -vmIsQuickEqual :: VersionedMap k v -> VersionedMap k v -> Bool -vmIsQuickEqual a b = - let - va = mapVersion a - vb = mapVersion b - in - va >= 0 && vb >= 0 && va == vb - --- A new, empty VersionedMap -vmEmpty = VersionedMap { - mapVersion = 0, - mapStorage = M.empty -} - --- Map.null for VersionedMaps -vmNull :: VersionedMap k v -> Bool -vmNull m = mapVersion m == 0 || (M.null $ mapStorage m) - --- Map.lookup for VersionedMaps -vmLookup name map = M.lookup name $ mapStorage map - --- Map.insert for VersionedMaps -vmInsert key val map = VersionedMap { - mapVersion = -1, - mapStorage = M.insert key val $ mapStorage map -} - --- Overwrite all keys in the first map with values from the second -vmPatch :: (Ord k) => VersionedMap k v -> VersionedMap k v -> VersionedMap k v -vmPatch base diff = - case () of - _ | mapVersion base == 0 -> diff - _ | mapVersion diff == 0 -> base - _ | vmIsQuickEqual base diff -> diff - _ -> VersionedMap { - mapVersion = -1, - mapStorage = M.union (mapStorage diff) (mapStorage base) - } - --- Set a variable. This includes properties. Applies it to the appropriate scope. -writeVariable :: forall s. Ctx s -> String -> VariableState -> ST s () -writeVariable ctx name val = do - typ <- readVariableScope ctx name - case typ of - GlobalScope -> writeGlobal ctx name val - LocalScope -> writeLocal ctx name val - -- Prefixed variables actually become local variables in the invoked function - PrefixScope -> writeLocal ctx name val - -writeGlobal ctx name val = do - modifySTRef (cOutput ctx) $ insertGlobal name val - -writeLocal ctx name val = do - modifySTRef (cOutput ctx) $ insertLocal name val - -writePrefix ctx name val = do - modifySTRef (cOutput ctx) $ insertPrefix name val - -updateVariableValue ctx name val = do - (props, scope) <- readVariablePropertiesWithScope ctx name - let f = case scope of - GlobalScope -> writeGlobal - LocalScope -> writeLocal - PrefixScope -> writeLocal -- Updates become local - f ctx name $ VariableState { variableValue = val, variableProperties = props } - -updateGlobalValue ctx name val = do - props <- readGlobalProperties ctx name - writeGlobal ctx name VariableState { variableValue = val, variableProperties = props } - -updateLocalValue ctx name val = do - props <- readLocalProperties ctx name - writeLocal ctx name VariableState { variableValue = val, variableProperties = props } - -updatePrefixValue ctx name val = do - -- Prefix variables don't inherit properties - writePrefix ctx name VariableState { variableValue = val, variableProperties = defaultProperties } - - --- Look up a variable value, and also return its scope -readVariableWithScope :: forall s. Ctx s -> String -> ST s (VariableState, Scope) -readVariableWithScope ctx name = lookupStack get dep def ctx name - where - def = (unknownVariableState, GlobalScope) - get = getVariableWithScope - dep k (val, scope) = DepState scope k val - --- Look up the variable's properties. This can be done independently to avoid incurring a dependency on the value. -readVariablePropertiesWithScope :: forall s. Ctx s -> String -> ST s (VariableProperties, Scope) -readVariablePropertiesWithScope ctx name = lookupStack get dep def ctx name - where - def = (defaultProperties, GlobalScope) - get s k = do - (val, scope) <- getVariableWithScope s k - return (variableProperties val, scope) - dep k (val, scope) = DepProperties scope k val - -readVariableScope ctx name = snd <$> readVariablePropertiesWithScope ctx name - -getVariableWithScope :: InternalState -> String -> Maybe (VariableState, Scope) -getVariableWithScope s name = - case (vmLookup name $ sPrefixValues s, vmLookup name $ sLocalValues s, vmLookup name $ sGlobalValues s) of - (Just var, _, _) -> return (var, PrefixScope) - (_, Just var, _) -> return (var, LocalScope) - (_, _, Just var) -> return (var, GlobalScope) - _ -> Nothing - -undefineFunction ctx name = - writeFunction ctx name $ FunctionUnknown - -undefineVariable ctx name = - writeVariable ctx name $ unsetVariableState - -readVariable ctx name = fst <$> readVariableWithScope ctx name -readVariableProperties ctx name = fst <$> readVariablePropertiesWithScope ctx name - -readGlobal ctx name = lookupStack get dep def ctx name - where - def = unknownVariableState -- could come from the environment - get s name = vmLookup name $ sGlobalValues s - dep k v = DepState GlobalScope k v - - -readGlobalProperties ctx name = lookupStack get dep def ctx name - where - def = defaultProperties - get s name = variableProperties <$> (vmLookup name $ sGlobalValues s) - -- This dependency will fail to match if it's shadowed by a local variable, - -- such as in x=1; f() { local -i x; declare -ag x; } because we'll look at - -- x and find it to be local and not global. FIXME? - dep k v = DepProperties GlobalScope k v - -readLocal ctx name = lookupStackUntilFunction get dep def ctx name - where - def = unsetVariableState -- can't come from the environment - get s name = vmLookup name $ sLocalValues s - dep k v = DepState LocalScope k v - --- We only want to look up the local properties of the current function, --- though preferably even if we're in a subshell. FIXME? -readLocalProperties ctx name = fst <$> lookupStackUntilFunction get dep def ctx name - where - def = (defaultProperties, LocalScope) - with tag f = do - val <- variableProperties <$> f - return (val, tag) - - get s name = (with LocalScope $ vmLookup name $ sLocalValues s) `mplus` (with PrefixScope $ vmLookup name $ sPrefixValues s) - dep k (val, scope) = DepProperties scope k val - -readFunction ctx name = lookupStack get dep def ctx name - where - def = unknownFunctionValue - get s name = vmLookup name $ sFunctionTargets s - dep k v = DepFunction k v - -writeFunction ctx name val = do - modifySTRef (cOutput ctx) $ insertFunction name $ S.singleton val - -readExitCodes ctx = lookupStack get dep def ctx () - where - get s () = sExitCodes s - def = S.empty - dep () v = DepExitCodes v - --- Look up each state on the stack until a value is found (or the default is used), --- then add this value as a StateDependency. -lookupStack' :: forall s k v. - -- Whether to stop at function boundaries - Bool - -- A function that maybe finds a value from a state - -> (InternalState -> k -> Maybe v) - -- A function that creates a dependency on what was found - -> (k -> v -> StateDependency) - -- A default value, if the value can't be found anywhere - -> v - -- Context - -> Ctx s - -- The key to look up - -> k - -- Returning the result - -> ST s v -lookupStack' functionOnly get dep def ctx key = do - top <- readSTRef $ cInput ctx - case get top key of - Just v -> return v - Nothing -> f (cStack ctx) - where - f [] = return def - f (s:_) | functionOnly && isFunctionCall s = return def - f (s:rest) = do - -- Go up the stack until we find the value, and add - -- a dependency on each state (including where it was found) - res <- maybe (f rest) return (get (stackState s) key) - modifySTRef (dependencies s) $ S.insert $ dep key res - return res - -lookupStack = lookupStack' False -lookupStackUntilFunction = lookupStack' True - --- Like lookupStack but without adding dependencies -peekStack get def ctx key = do - top <- readSTRef $ cInput ctx - case get top key of - Just v -> return v - Nothing -> f (cStack ctx) - where - f [] = return def - f (s:rest) = - case get (stackState s) key of - Just v -> return v - Nothing -> f rest - --- Check if the current context fulfills a StateDependency if entering `entry` -fulfillsDependency ctx entry dep = - case dep of - DepState scope name val -> (== (val, scope)) <$> peek scope ctx name - DepProperties scope name props -> do - (state, s) <- peek scope ctx name - return $ scope == s && variableProperties state == props - DepFunction name val -> (== val) <$> peekFunc ctx name - -- Hack. Since we haven't pushed the soon-to-be invoked function on the stack, - -- it won't be found by the normal check. - DepIsRecursive node val | node == entry -> return True - DepIsRecursive node val -> return $ val == any (\f -> entryPoint f == node) (cStack ctx) - DepExitCodes val -> (== val) <$> peekStack (\s k -> sExitCodes s) S.empty ctx () - -- _ -> error $ "Unknown dep " ++ show dep - where - peek scope = peekStack getVariableWithScope $ if scope == GlobalScope then (unknownVariableState, GlobalScope) else (unsetVariableState, LocalScope) - peekFunc = peekStack (\state name -> vmLookup name $ sFunctionTargets state) unknownFunctionValue - --- Check if the current context fulfills all StateDependencies -fulfillsDependencies ctx entry deps = - f $ S.toList deps - where - f [] = return True - f (dep:rest) = do - res <- fulfillsDependency ctx entry dep - if res - then f rest - else return False - --- Create a brand new Ctx given a Control Flow Graph (CFG) -newCtx g = do - c <- newSTRef 1 - input <- newSTRef undefined - output <- newSTRef undefined - node <- newSTRef undefined - cache <- newSTRef M.empty - enableCache <- newSTRef True - invocations <- newSTRef M.empty - return $ Ctx { - cCounter = c, - cInput = input, - cOutput = output, - cNode = node, - cCache = cache, - cEnableCache = enableCache, - cStack = [], - cInvocations = invocations, - cGraph = g - } - --- The next incrementing version for VersionedMaps -nextVersion ctx = do - let ctr = cCounter ctx - n <- readSTRef ctr - writeSTRef ctr $! n+1 - return n - --- Create a new StackEntry -newStackEntry ctx point isCall = do - deps <- newSTRef S.empty - state <- readSTRef $ cOutput ctx - callsite <- readSTRef $ cNode ctx - return $ StackEntry { - entryPoint = point, - isFunctionCall = isCall, - callSite = callsite, - dependencies = deps, - stackState = state - } - --- Call a function with a new stack entry on the stack -withNewStackFrame ctx node isCall f = do - newEntry <- newStackEntry ctx node isCall - newInput <- newSTRef newInternalState - newOutput <- newSTRef newInternalState - newNode <- newSTRef node - let newCtx = ctx { - cInput = newInput, - cOutput = newOutput, - cNode = newNode, - cStack = newEntry : cStack ctx - } - x <- f newCtx - - {- - deps <- readSTRef $ dependencies newEntry - selfcheck <- fulfillsDependencies newCtx deps - unless selfcheck $ error $ pleaseReport $ "Unmet stack dependencies on " ++ show (node, deps) - -} - - return (x, newEntry) - --- Check if invoking this function would be a recursive loop --- (i.e. we already have the function on the stack) -wouldBeRecursive ctx node = f (cStack ctx) - where - f [] = return False - f (s:rest) = do - res <- - if entryPoint s == node - then return True - else f rest - modifySTRef (dependencies s) $ S.insert $ DepIsRecursive node res - return res - --- The main DFA 'transfer' function, applying the effects of a node to the output state -transfer ctx label = - --traceShow ("Transferring", label) $ - case label of - CFStructuralNode -> return () - CFEntryPoint _ -> return () - CFImpliedExit -> return () - CFResolvedExit {} -> return () - - CFExecuteCommand cmd -> transferCommand ctx cmd - CFExecuteSubshell reason entry exit -> transferSubshell ctx reason entry exit - CFApplyEffects effects -> mapM_ (\(IdTagged _ f) -> transferEffect ctx f) effects - CFSetExitCode id -> transferExitCode ctx id - - CFUnresolvedExit -> patchOutputM ctx unreachableState - CFUnreachable -> patchOutputM ctx unreachableState - - -- TODO - CFSetBackgroundPid _ -> return () - CFDropPrefixAssignments {} -> - modifySTRef (cOutput ctx) $ \c -> modified c { sPrefixValues = vmEmpty } --- _ -> error $ "Unknown " ++ show label - - --- Transfer the effects of a subshell invocation. This is similar to a function call --- to allow easily discarding the effects (otherwise the InternalState would have --- to represent subshell depth, while this way it can simply use the function stack). -transferSubshell ctx reason entry exit = do - let cout = cOutput ctx - initial <- readSTRef cout - runCached ctx entry (f entry exit) - res <- readSTRef cout - -- Clear subshell changes. TODO: track this to warn about modifications. - writeSTRef cout $ initial { - sExitCodes = sExitCodes res - } - where - f entry exit ctx = do - (states, frame) <- withNewStackFrame ctx entry False (flip dataflow $ entry) - let (_, res) = fromMaybe (error $ pleaseReport "Subshell has no exit") $ M.lookup exit states - deps <- readSTRef $ dependencies frame - registerFlowResult ctx entry states deps - return (deps, res) - --- Transfer the effects of executing a command, i.e. the merged union of all possible function definitions. -transferCommand ctx Nothing = return () -transferCommand ctx (Just name) = do - targets <- readFunction ctx name - logVerbose ("Transferring ",name,targets) - transferMultiple ctx $ map (flip transferFunctionValue) $ S.toList targets - --- Transfer a set of function definitions and merge the output states. -transferMultiple ctx funcs = do - logVerbose ("Transferring set of ", length funcs) - original <- readSTRef out - branches <- mapM (apply ctx original) funcs - merged <- mergeStates ctx original branches - let patched = patchState original merged - writeSTRef out patched - where - out = cOutput ctx - apply ctx original f = do - writeSTRef out original - f ctx - readSTRef out - --- Transfer the effects of a single function definition. -transferFunctionValue ctx funcVal = - case funcVal of - FunctionUnknown -> return () - FunctionDefinition name entry exit -> do - isRecursive <- wouldBeRecursive ctx entry - if isRecursive - then return () -- TODO: Find a better strategy for recursion - else runCached ctx entry (f name entry exit) - where - f name entry exit ctx = do - (states, frame) <- withNewStackFrame ctx entry True (flip dataflow $ entry) - deps <- readSTRef $ dependencies frame - let res = - case M.lookup exit states of - Just (input, output) -> do - -- Discard local variables. TODO: track&retain variables declared local in previous scopes? - modified output { sLocalValues = vmEmpty } - Nothing -> do - -- e.g. f() { exit; } - unreachableState - registerFlowResult ctx entry states deps - return (deps, res) - -transferExitCode ctx id = do - modifySTRef (cOutput ctx) $ setExitCode id - --- Register/save the result of a dataflow of a function. --- At the end, all the different values from different flows are merged together. -registerFlowResult ctx entry states deps = do - -- This function is called in the context of a CFExecuteCommand and not its invoked function, - -- so manually add the current node to the stack. - current <- readSTRef $ cNode ctx - let parents = map callSite $ cStack ctx - -- A unique path to this flow context. The specific value doesn't matter, as long as it's - -- unique per invocation of the function. This is required so that 'x=1; f; x=2; f' won't - -- overwrite each other. - let path = entry : current : parents - modifySTRef (cInvocations ctx) $ M.insert path (deps, states) - - --- Look up a node in the cache and see if the dependencies of any entries are matched. --- In that case, reuse the previous result instead of doing a new data flow. -runCached :: forall s. Ctx s -> Node -> (Ctx s -> ST s (S.Set StateDependency, InternalState)) -> ST s () -runCached ctx node f = do - cache <- getCache ctx node - case cache of - Just v -> do - logInfo ("Running cached", node) - -- do { (deps, diff) <- f ctx; unless (v == diff) $ traceShowM ("Cache FAILED to match actual result", node, deps, diff); } - patchOutputM ctx v - - Nothing -> do - logInfo ("Cache failed", node) - (deps, diff) <- f ctx - modifySTRef (cCache ctx) (M.insertWith (\_ old -> (deps, diff):(take cacheEntries old)) node [(deps,diff)]) - logVerbose ("Recomputed cache for", node, deps) - -- do { f <- fulfillsDependencies ctx node deps; unless (f) $ traceShowM ("New dependencies FAILED to match", node, deps); } - patchOutputM ctx diff - --- Get a cached version whose dependencies are currently fulfilled, if any. -getCache :: forall s. Ctx s -> Node -> ST s (Maybe InternalState) -getCache ctx node = do - cache <- readSTRef $ cCache ctx - enable <- readSTRef $ cEnableCache ctx - logVerbose ("Cache for", node, "length", length $ M.findWithDefault [] node cache, M.lookup node cache) - if enable - then f $ M.findWithDefault [] node cache - else return Nothing - where - f [] = return Nothing - f ((deps, value):rest) = do - match <- fulfillsDependencies ctx node deps - if match - then return $ Just value - else f rest - --- Transfer a single CFEffect to the output state. -transferEffect ctx effect = - case effect of - CFReadVariable name -> - case name of - "?" -> void $ readExitCodes ctx - _ -> void $ readVariable ctx name - CFWriteVariable name value -> do - val <- cfValueToVariableValue ctx value - updateVariableValue ctx name val - CFWriteGlobal name value -> do - val <- cfValueToVariableValue ctx value - updateGlobalValue ctx name val - CFWriteLocal name value -> do - val <- cfValueToVariableValue ctx value - updateLocalValue ctx name val - CFWritePrefix name value -> do - val <- cfValueToVariableValue ctx value - updatePrefixValue ctx name val - - CFSetProps scope name props -> - case scope of - Nothing -> do - state <- readVariable ctx name - writeVariable ctx name $ addProperties props state - Just GlobalScope -> do - state <- readGlobal ctx name - writeGlobal ctx name $ addProperties props state - Just LocalScope -> do - out <- readSTRef (cOutput ctx) - state <- readLocal ctx name - writeLocal ctx name $ addProperties props state - Just PrefixScope -> do - -- Prefix values become local - state <- readLocal ctx name - writeLocal ctx name $ addProperties props state - - CFUnsetProps scope name props -> - case scope of - Nothing -> do - state <- readVariable ctx name - writeVariable ctx name $ removeProperties props state - Just GlobalScope -> do - state <- readGlobal ctx name - writeGlobal ctx name $ removeProperties props state - Just LocalScope -> do - out <- readSTRef (cOutput ctx) - state <- readLocal ctx name - writeLocal ctx name $ removeProperties props state - Just PrefixScope -> do - -- Prefix values become local - state <- readLocal ctx name - writeLocal ctx name $ removeProperties props state - - - CFUndefineVariable name -> undefineVariable ctx name - CFUndefineFunction name -> undefineFunction ctx name - CFUndefine name -> do - -- This should really just unset one or the other - undefineVariable ctx name - undefineFunction ctx name - CFDefineFunction name id entry exit -> - writeFunction ctx name $ FunctionDefinition name entry exit - - -- TODO - CFUndefineNameref name -> undefineVariable ctx name - CFHintArray name -> return () - CFHintDefined name -> return () --- _ -> error $ "Unknown effect " ++ show effect - - --- Transfer the CFG's idea of a value into our VariableState -cfValueToVariableValue ctx val = - case val of - CFValueArray -> return unknownVariableValue -- TODO: Track array status - CFValueComputed _ parts -> foldM f emptyVariableValue parts - CFValueInteger -> return unknownIntegerValue - CFValueString -> return unknownVariableValue - CFValueUninitialized -> return emptyVariableValue --- _ -> error $ "Unknown value: " ++ show val - where - f val part = do - next <- computeValue ctx part - return $ val `appendVariableValue` next - --- A value can be computed from 0 or more parts, such as x="literal$y$z" -computeValue ctx part = - case part of - CFStringLiteral str -> return $ literalToVariableValue str - CFStringInteger -> return unknownIntegerValue - CFStringUnknown -> return unknownVariableValue - CFStringVariable name -> variableStateToValue <$> readVariable ctx name - where - variableStateToValue state = - case () of - _ | all (CFVPInteger `S.member`) $ variableProperties state -> unknownIntegerValue - _ -> variableValue state - --- Append two VariableValues as if with z="$x$y" -appendVariableValue :: VariableValue -> VariableValue -> VariableValue -appendVariableValue a b = - unknownVariableValue { - literalValue = liftM2 (++) (literalValue a) (literalValue b), - spaceStatus = appendSpaceStatus (spaceStatus a) (spaceStatus b), - numericalStatus = appendNumericalStatus (numericalStatus a) (numericalStatus b) - } - -appendSpaceStatus a b = - case (a,b) of - (SpaceStatusEmpty, _) -> b - (_, SpaceStatusEmpty) -> a - (SpaceStatusClean, SpaceStatusClean) -> a - _ ->SpaceStatusDirty - -appendNumericalStatus a b = - case (a,b) of - (NumericalStatusEmpty, x) -> x - (x, NumericalStatusEmpty) -> x - (NumericalStatusDefinitely, NumericalStatusDefinitely) -> NumericalStatusDefinitely - (NumericalStatusUnknown, _) -> NumericalStatusUnknown - (_, NumericalStatusUnknown) -> NumericalStatusUnknown - _ -> NumericalStatusMaybe - -unknownIntegerValue = unknownVariableValue { - literalValue = Nothing, - spaceStatus = SpaceStatusClean, - numericalStatus = NumericalStatusDefinitely -} - -literalToVariableValue str = unknownVariableValue { - literalValue = Just str, - spaceStatus = literalToSpaceStatus str, - numericalStatus = literalToNumericalStatus str -} - -withoutChanges ctx f = do - let inp = cInput ctx - let out = cOutput ctx - prevInput <- readSTRef inp - prevOutput <- readSTRef out - res <- f - writeSTRef inp prevInput - writeSTRef out prevOutput - return res - --- Get the SpaceStatus for a literal string, i.e. if it needs quoting -literalToSpaceStatus str = - case str of - "" -> SpaceStatusEmpty - _ | all (`notElem` " \t\n*?[") str -> SpaceStatusClean - _ -> SpaceStatusDirty - --- Get the NumericalStatus for a literal string, i.e. whether it's an integer -literalToNumericalStatus str = - case str of - "" -> NumericalStatusEmpty - '-':rest -> if isNumeric rest then NumericalStatusDefinitely else NumericalStatusUnknown - rest -> if isNumeric rest then NumericalStatusDefinitely else NumericalStatusUnknown - where - isNumeric = all isDigit - -type StateMap = M.Map Node (InternalState, InternalState) - --- Classic, iterative Data Flow Analysis. See Wikipedia for a description of the process. -dataflow :: forall s. Ctx s -> Node -> ST s StateMap -dataflow ctx entry = do - pending <- newSTRef $ S.singleton entry - states <- newSTRef $ M.empty - -- Should probably be done via a stack frame instead - withoutChanges ctx $ - f iterationCount pending states - readSTRef states - where - graph = cGraph ctx - f 0 _ _ = error $ pleaseReport "DFA did not reach fix point" - f n pending states = do - ps <- readSTRef pending - - when (n == fallbackThreshold) $ do - -- This should never happen, but has historically been due to caching bugs. - -- Try disabling the cache and continuing. - logInfo "DFA is not stabilizing! Disabling cache." - writeSTRef (cEnableCache ctx) False - - if S.null ps - then return () - else do - let (next, rest) = S.deleteFindMin ps - nexts <- process states next - writeSTRef pending $ S.union (S.fromList nexts) rest - f (n-1) pending states - - process states node = do - stateMap <- readSTRef states - let inputs = filter (\c -> sIsReachable c /= Just False) $ mapMaybe (\c -> fmap snd $ M.lookup c stateMap) incoming - input <- - case incoming of - [] -> return newInternalState - _ -> - case inputs of - [] -> return unreachableState - (x:rest) -> foldM (mergeState ctx) x rest - writeSTRef (cInput ctx) $ input - writeSTRef (cOutput ctx) $ input - writeSTRef (cNode ctx) $ node - transfer ctx label - newOutput <- readSTRef $ cOutput ctx - result <- - if is2plus outgoing - then - -- Version the state because we split and will probably merge later - versionState ctx newOutput - else return newOutput - writeSTRef states $ M.insert node (input, result) stateMap - case M.lookup node stateMap of - Nothing -> return outgoing - Just (oldInput, oldOutput) -> - if oldOutput == result - then return [] - else return outgoing - where - (incomingL, _, label, outgoingL) = context graph $ node - incoming = map snd $ filter isRegular $ incomingL - outgoing = map snd outgoingL - isRegular = ((== CFEFlow) . fst) - -runRoot ctx env entry exit = do - writeSTRef (cInput ctx) $ env - writeSTRef (cOutput ctx) $ env - writeSTRef (cNode ctx) $ entry - (states, frame) <- withNewStackFrame ctx entry False $ \c -> dataflow c entry - deps <- readSTRef $ dependencies frame - registerFlowResult ctx entry states deps - -- Return the final state, used to invoke functions that were declared but not invoked - return $ snd $ fromMaybe (error $ pleaseReport "Missing exit state") $ M.lookup exit states - - -analyzeControlFlow :: CFGParameters -> Token -> CFGAnalysis -analyzeControlFlow params t = - let - cfg = buildGraph params t - (entry, exit) = M.findWithDefault (error $ pleaseReport "Missing root") (getId t) (cfIdToRange cfg) - in - runST $ f cfg entry exit - where - f cfg entry exit = do - let env = createEnvironmentState - ctx <- newCtx $ cfGraph cfg - -- Do a dataflow analysis starting on the root node - exitState <- runRoot ctx env entry exit - - -- All nodes we've touched - invocations <- readSTRef $ cInvocations ctx - let invokedNodes = M.fromSet (const ()) $ S.unions $ map (M.keysSet . snd) $ M.elems invocations - - -- Invoke all functions that were declared but not invoked - -- This is so that we still get warnings for dead code - -- (it's probably not actually dead, just used by a script that sources ours) - let declaredFunctions = getFunctionTargets exitState - let uninvoked = M.difference declaredFunctions invokedNodes - - let stragglerInput = - (env `patchState` exitState) { - -- We don't want `die() { exit $?; }; echo "Sourced"` to assume $? is always echo - sExitCodes = Nothing - } - - analyzeStragglers ctx stragglerInput uninvoked - - -- Now round up all the states from all data flows - -- (FIXME: this excludes functions that were defined in straggling functions) - invocations <- readSTRef $ cInvocations ctx - invokedStates <- flattenByNode ctx $ groupByNode $ M.map addDeps invocations - - -- Fill in the map with unreachable states for anything we didn't get to - let baseStates = M.fromDistinctAscList $ map (\c -> (c, (unreachableState, unreachableState))) $ uncurry enumFromTo $ nodeRange $ cfGraph cfg - let allStates = M.union invokedStates baseStates - - -- Convert to external states - let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates - - return $ nodeToData `deepseq` CFGAnalysis { - graph = cfGraph cfg, - tokenToRange = cfIdToRange cfg, - tokenToNodes = cfIdToNodes cfg, - nodeToData = nodeToData, - postDominators = cfPostDominators cfg - } - - - -- Include the dependencies in the state of each function, e.g. if it depends on `x=foo` then add that. - addDeps :: (S.Set StateDependency, M.Map Node (InternalState, InternalState)) -> M.Map Node (InternalState, InternalState) - addDeps (deps, m) = let base = depsToState deps in M.map (\(a,b) -> (base `patchState` a, base `patchState` b)) m - - -- Collect all the states that each node has resulted in. - groupByNode :: forall k v. M.Map k (M.Map Node v) -> M.Map Node [v] - groupByNode pathMap = M.fromListWith (++) $ map (\(k,v) -> (k,[v])) $ concatMap M.toList $ M.elems pathMap - - -- Merge all the pre/post states for each node. This would have been a foldM if Map had one. - flattenByNode ctx m = M.fromDistinctAscList <$> (mapM (mergePair ctx) $ M.toList m) - - mergeAllStates ctx pairs = - let - (pres, posts) = unzip pairs - in do - pre <- mergeStates ctx (error $ pleaseReport "Null node states") pres - post <- mergeStates ctx (error $ pleaseReport "Null node states") posts - return (pre, post) - - mergePair ctx (node, list) = do - merged <- mergeAllStates ctx list - return (node, merged) - - -- Get the all the functions defined in an InternalState - getFunctionTargets :: InternalState -> M.Map Node FunctionDefinition - getFunctionTargets state = - let - declaredFuncs = S.unions $ M.elems $ mapStorage $ sFunctionTargets state - getFunc d = - case d of - FunctionDefinition _ entry _ -> Just (entry, d) - _ -> Nothing - funcs = mapMaybe getFunc $ S.toList declaredFuncs - in - M.fromList funcs - - -analyzeStragglers ctx state stragglers = do - mapM_ f $ M.elems stragglers - where - f def@(FunctionDefinition name entry exit) = do - writeSTRef (cInput ctx) state - writeSTRef (cOutput ctx) state - writeSTRef (cNode ctx) entry - transferFunctionValue ctx def - - - -return [] -runTests = $quickCheckAll diff --git a/src/ShellCheck/Checker.hs b/src/ShellCheck/Checker.hs deleted file mode 100644 index dd39921..0000000 --- a/src/ShellCheck/Checker.hs +++ /dev/null @@ -1,567 +0,0 @@ -{- - Copyright 2012-2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where - -import ShellCheck.Analyzer -import ShellCheck.ASTLib -import ShellCheck.Interface -import ShellCheck.Parser - -import Debug.Trace -- DO NOT SUBMIT -import Data.Either -import Data.Functor -import Data.List -import Data.Maybe -import Data.Ord -import Control.Monad.Identity -import qualified Data.Map as Map -import qualified System.IO -import Prelude hiding (readFile) -import Control.Monad - -import Test.QuickCheck.All - -tokenToPosition startMap t = fromMaybe fail $ do - span <- Map.lookup (tcId t) startMap - return $ newPositionedComment { - pcStartPos = fst span, - pcEndPos = snd span, - pcComment = tcComment t, - pcFix = tcFix t - } - where - fail = error "Internal shellcheck error: id doesn't exist. Please report!" - -shellFromFilename filename = listToMaybe candidates - where - shellExtensions = [(".ksh", Ksh) - ,(".bash", Bash) - ,(".bats", Bash) - ,(".dash", Dash)] - -- The `.sh` is too generic to determine the shell: - -- We fallback to Bash in this case and emit SC2148 if there is no shebang - candidates = - [sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename] - -checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult -checkScript sys spec = do - results <- checkScript (csScript spec) - return emptyCheckResult { - crFilename = csFilename spec, - crComments = results - } - where - checkScript contents = do - result <- parseScript sys newParseSpec { - psFilename = csFilename spec, - psScript = contents, - psCheckSourced = csCheckSourced spec, - psIgnoreRC = csIgnoreRC spec, - psShellTypeOverride = csShellTypeOverride spec - } - let parseMessages = prComments result - let tokenPositions = prTokenPositions result - let analysisSpec root = - as { - asScript = root, - asShellType = csShellTypeOverride spec, - asFallbackShell = shellFromFilename $ csFilename spec, - asCheckSourced = csCheckSourced spec, - asExecutionMode = Executed, - asTokenPositions = tokenPositions, - asExtendedAnalysis = csExtendedAnalysis spec, - asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec - } where as = newAnalysisSpec root - let analysisMessages = - maybe [] - (arComments . analyzeScript . analysisSpec) - $ prRoot result - let translator = tokenToPosition tokenPositions - return . nub . sortMessages . filter shouldInclude $ - (parseMessages ++ map translator analysisMessages) - - shouldInclude pc = - severity <= csMinSeverity spec && - case csIncludedWarnings spec of - Nothing -> code `notElem` csExcludedWarnings spec - Just includedWarnings -> code `elem` includedWarnings - where - code = cCode (pcComment pc) - severity = cSeverity (pcComment pc) - - sortMessages = sortOn order - order pc = - let pos = pcStartPos pc - comment = pcComment pc in - (posFile pos, - posLine pos, - posColumn pos, - cSeverity comment, - cCode comment, - cMessage comment) - getPosition = pcStartPos - - -getErrors sys spec = - sort . map getCode . crComments $ - runIdentity (checkScript sys spec) - where - getCode = cCode . pcComment - -check = checkWithIncludes [] - -checkWithSpec includes = - getErrors (mockedSystemInterface includes) - -checkWithIncludes includes src = - checkWithSpec includes emptyCheckSpec { - csScript = src, - csExcludedWarnings = [2148] - } - -checkRecursive includes src = - checkWithSpec includes emptyCheckSpec { - csScript = src, - csExcludedWarnings = [2148], - csCheckSourced = True - } - -checkOptionIncludes includes src = - checkWithSpec [] emptyCheckSpec { - csScript = src, - csIncludedWarnings = includes, - csCheckSourced = True - } - -checkWithRc rc = getErrors - (mockRcFile rc $ mockedSystemInterface []) - -checkWithIncludesAndSourcePath includes mapper = getErrors - (mockedSystemInterface includes) { - siFindSource = mapper - } - -checkWithRcIncludesAndSourcePath rc includes mapper = getErrors - (mockRcFile rc $ mockedSystemInterface includes) { - siFindSource = mapper - } - -prop_findsParseIssue = check "echo \"$12\"" == [1037] - -prop_commentDisablesParseIssue1 = - null $ check "#shellcheck disable=SC1037\necho \"$12\"" -prop_commentDisablesParseIssue2 = - null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\"" - -prop_findsAnalysisIssue = - check "echo $1" == [2086] -prop_commentDisablesAnalysisIssue1 = - null $ check "#shellcheck disable=SC2086\necho $1" -prop_commentDisablesAnalysisIssue2 = - null $ check "#shellcheck disable=SC2086\n#lol\necho $1" - -prop_optionDisablesIssue1 = - null $ getErrors - (mockedSystemInterface []) - emptyCheckSpec { - csScript = "echo $1", - csExcludedWarnings = [2148, 2086] - } - -prop_optionDisablesIssue2 = - null $ getErrors - (mockedSystemInterface []) - emptyCheckSpec { - csScript = "echo \"$10\"", - csExcludedWarnings = [2148, 1037] - } - -prop_wontParseBadShell = - [1071] == check "#!/usr/bin/python\ntrue $1\n" - -prop_optionDisablesBadShebang = - null $ getErrors - (mockedSystemInterface []) - emptyCheckSpec { - csScript = "#!/usr/bin/python\ntrue\n", - csShellTypeOverride = Just Sh - } - -prop_annotationDisablesBadShebang = - null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n" - - -prop_canParseDevNull = - null $ check "source /dev/null" - -prop_failsWhenNotSourcing = - [1091, 2154] == check "source lol; echo \"$bar\"" - -prop_worksWhenSourcing = - null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\"" - -prop_worksWhenSourcingWithDashDash = - null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\"" - -prop_worksWhenSourcingWithDashP = - null $ checkWithIncludes [("lib", "bar=1")] "source -p \"$MYPATH\" lib; echo \"$bar\"" - -prop_worksWhenDotting = - null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\"" - --- FIXME: This should really be giving [1093], "recursively sourced" -prop_noInfiniteSourcing = - null $ checkWithIncludes [("lib", "source lib")] "source lib" - -prop_canSourceBadSyntax = - [1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1" - -prop_cantSourceDynamic = - [1090] == checkWithIncludes [("lib", "")] ". \"$1\"" - -prop_cantSourceDynamic2 = - [1090] == checkWithIncludes [("lib", "")] "source ~/foo" - -prop_canStripPrefixAndSource = - null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\"" - -prop_canStripPrefixAndSource2 = - null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\"" - -prop_canSourceDynamicWhenRedirected = - null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" - -prop_canRedirectWithSpaces = - null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\"" - -prop_recursiveAnalysis = - [2086] == checkRecursive [("lib", "echo $1")] "source lib" - -prop_recursiveParsing = - [1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib" - -prop_nonRecursiveAnalysis = - null $ checkWithIncludes [("lib", "echo $1")] "source lib" - -prop_nonRecursiveParsing = - null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib" - -prop_sourceDirectiveDoesntFollowFile = - null $ checkWithIncludes - [("foo", "source bar"), ("bar", "baz=3")] - "#shellcheck source=foo\n. \"$1\"; echo \"$baz\"" - -prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1" -prop_filewideAnnotation1 = null $ - check "#!/bin/sh\n# shellcheck disable=2086\necho $1" -prop_filewideAnnotation2 = null $ - check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1" -prop_filewideAnnotation3 = null $ - check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1" -prop_filewideAnnotation4 = null $ - check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" -prop_filewideAnnotation5 = null $ - check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1" -prop_filewideAnnotation6 = null $ - check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1" -prop_filewideAnnotation7 = null $ - check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" - -prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1" -prop_filewideAnnotation8 = null $ - check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1" - -prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source' - 3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh" - -prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n" - -prop_deducesTypeFromExtension = null result - where - result = checkWithSpec [] emptyCheckSpec { - csFilename = "file.ksh", - csScript = "(( 3.14 ))" - } - -prop_deducesTypeFromExtension2 = result == [2079] - where - result = checkWithSpec [] emptyCheckSpec { - csFilename = "file.bash", - csScript = "(( 3.14 ))" - } - -prop_canDisableShebangWarning = null $ result - where - result = checkWithSpec [] emptyCheckSpec { - csFilename = "file.sh", - csScript = "#shellcheck disable=SC2148\nfoo" - } - -prop_canDisableAllWarnings = result == [2086] - where - result = checkWithSpec [] emptyCheckSpec { - csFilename = "file.sh", - csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`" - } - -prop_canDisableParseErrors = null $ result - where - result = checkWithSpec [] emptyCheckSpec { - csFilename = "file.sh", - csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()" - } - -prop_shExtensionDoesntMatter = result == [2148] - where - result = checkWithSpec [] emptyCheckSpec { - csFilename = "file.sh", - csScript = "echo 'hello world'" - } - -prop_sourcedFileUsesOriginalShellExtension = result == [2079] - where - result = checkWithSpec [("file.ksh", "(( 3.14 ))")] emptyCheckSpec { - csFilename = "file.bash", - csScript = "source file.ksh", - csCheckSourced = True - } - -prop_canEnableOptionalsWithSpec = result == [2244] - where - result = checkWithSpec [] emptyCheckSpec { - csFilename = "file.sh", - csScript = "#!/bin/sh\n[ \"$1\" ]", - csOptionalChecks = ["avoid-nullary-conditions"] - } - -prop_optionIncludes1 = - -- expect 2086, but not included, so nothing reported - null $ checkOptionIncludes (Just [2080]) "#!/bin/sh\n var='a b'\n echo $var" - -prop_optionIncludes2 = - -- expect 2086, included, so it is reported - [2086] == checkOptionIncludes (Just [2086]) "#!/bin/sh\n var='a b'\n echo $var" - -prop_optionIncludes3 = - -- expect 2086, no inclusions provided, so it is reported - [2086] == checkOptionIncludes Nothing "#!/bin/sh\n var='a b'\n echo $var" - -prop_optionIncludes4 = - -- expect 2086 & 2154, only 2154 included, so only that's reported - [2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar" - - -prop_readsRcFile = null result - where - result = checkWithRc "disable=2086" emptyCheckSpec { - csScript = "#!/bin/sh\necho $1", - csIgnoreRC = False - } - -prop_canUseNoRC = result == [2086] - where - result = checkWithRc "disable=2086" emptyCheckSpec { - csScript = "#!/bin/sh\necho $1", - csIgnoreRC = True - } - -prop_NoRCWontLookAtFile = result == [2086] - where - result = checkWithRc (error "Fail") emptyCheckSpec { - csScript = "#!/bin/sh\necho $1", - csIgnoreRC = True - } - -prop_brokenRcGetsWarning = result == [1134, 2086] - where - result = checkWithRc "rofl" emptyCheckSpec { - csScript = "#!/bin/sh\necho $1", - csIgnoreRC = False - } - -prop_canEnableOptionalsWithRc = result == [2244] - where - result = checkWithRc "enable=avoid-nullary-conditions" emptyCheckSpec { - csScript = "#!/bin/sh\n[ \"$1\" ]" - } - -prop_sourcePathRedirectsName = result == [2086] - where - f "dir/myscript" _ _ "lib" = return "foo/lib" - result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { - csScript = "#!/bin/bash\nsource lib", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_sourcePathAddsAnnotation = result == [2086] - where - f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib" - result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { - csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_sourcePathWorksWithSpaces = result == [2086] - where - f "dir/myscript" _ ["my path"] "lib" = return "foo/lib" - result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { - csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_sourcePathRedirectsDirective = result == [2086] - where - f "dir/myscript" _ _ "lib" = return "foo/lib" - f _ _ _ _ = return "/dev/null" - result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec { - csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_rcCanAllowExternalSources = result == [2086] - where - f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib" - f a b c d = error $ show ("Unexpected", a, b, c, d) - result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec { - csScript = "#!/bin/bash\nsource mylib", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_rcCanDenyExternalSources = result == [2086] - where - f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib" - f a b c d = error $ show ("Unexpected", a, b, c, d) - result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec { - csScript = "#!/bin/bash\nsource mylib", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_rcCanLeaveExternalSourcesUnspecified = result == [2086] - where - f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib" - f a b c d = error $ show ("Unexpected", a, b, c, d) - result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec { - csScript = "#!/bin/bash\nsource mylib", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_fileCanDisableExternalSources = result == [2006, 2086] - where - f "dir/myscript" (Just True) _ "withExternal" = return "withExternal" - f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal" - f a b c d = error $ show ("Unexpected", a, b, c, d) - result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec { - csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_fileCannotEnableExternalSources = result == [1144] - where - f "dir/myscript" Nothing _ "foo" = return "foo" - f a b c d = error $ show ("Unexpected", a, b, c, d) - result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec { - csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_fileCannotEnableExternalSources2 = result == [1144] - where - f "dir/myscript" (Just False) _ "foo" = return "foo" - f a b c d = error $ show ("Unexpected", a, b, c, d) - result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec { - csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo", - csFilename = "dir/myscript", - csCheckSourced = True - } - -prop_rcCanSuppressEarlyProblems1 = null result - where - result = checkWithRc "disable=1071" emptyCheckSpec { - csScript = "#!/bin/zsh\necho $1" - } - -prop_rcCanSuppressEarlyProblems2 = null result - where - result = checkWithRc "disable=1104" emptyCheckSpec { - csScript = "!/bin/bash\necho 'hello world'" - } - -prop_sourceWithHereDocWorks = null result - where - result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof" - -prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result - where - result = check "cat << eof" - -prop_hereDocsWillHaveParsedIndices = null result - where - result = check "#!/bin/bash\nmy_array=(a b)\ncat <> ./test\n $(( 1 + my_array[1] ))\nEOF" - -prop_rcCanSuppressDfa = null result - where - result = checkWithRc "extended-analysis=false" emptyCheckSpec { - csScript = "#!/bin/sh\nexit; foo;" - } - -prop_fileCanSuppressDfa = null $ traceShowId result - where - result = checkWithRc "" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;" - } - -prop_fileWinsWhenSuppressingDfa1 = null result - where - result = checkWithRc "extended-analysis=true" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;" - } - -prop_fileWinsWhenSuppressingDfa2 = result == [2317] - where - result = checkWithRc "extended-analysis=false" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;" - } - -prop_flagWinsWhenSuppressingDfa1 = result == [2317] - where - result = checkWithRc "extended-analysis=false" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;", - csExtendedAnalysis = Just True - } - -prop_flagWinsWhenSuppressingDfa2 = null result - where - result = checkWithRc "extended-analysis=true" emptyCheckSpec { - csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;", - csExtendedAnalysis = Just False - } - -return [] -runTests = $quickCheckAll diff --git a/src/ShellCheck/Checks/Commands.hs b/src/ShellCheck/Checks/Commands.hs deleted file mode 100644 index 0583566..0000000 --- a/src/ShellCheck/Checks/Commands.hs +++ /dev/null @@ -1,1475 +0,0 @@ -{- - Copyright 2012-2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE PatternGuards #-} - --- This module contains checks that examine specific commands by name. -module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where - -import ShellCheck.AST -import ShellCheck.ASTLib -import ShellCheck.AnalyzerLib -import ShellCheck.CFG -import qualified ShellCheck.CFGAnalysis as CF -import ShellCheck.Data -import ShellCheck.Interface -import ShellCheck.Parser -import ShellCheck.Prelude -import ShellCheck.Regex - -import Control.Monad -import Control.Monad.RWS -import Data.Char -import Data.Functor.Identity -import qualified Data.Graph.Inductive.Graph as G -import Data.List -import Data.Maybe -import qualified Data.List.NonEmpty as NE -import qualified Data.Map.Strict as M -import qualified Data.Set as S -import Test.QuickCheck.All (forAllProperties) -import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) - -import Debug.Trace -- STRIP - -data CommandName = Exactly String | Basename String - deriving (Eq, Ord) - -data CommandCheck = - CommandCheck CommandName (Token -> Analysis) - - -verify :: CommandCheck -> String -> Bool -verify f s = producesComments (getChecker [f]) s == Just True -verifyNot f s = producesComments (getChecker [f]) s == Just False - -commandChecks :: [CommandCheck] -commandChecks = [ - checkTr - ,checkFindNameGlob - ,checkExpr - ,checkGrepRe - ,checkTrapQuotes - ,checkReturn - ,checkExit - ,checkFindExecWithSingleArgument - ,checkUnusedEchoEscapes - ,checkInjectableFindSh - ,checkFindActionPrecedence - ,checkMkdirDashPM - ,checkNonportableSignals - ,checkInteractiveSu - ,checkSshCommandString - ,checkPrintfVar - ,checkUuoeCmd - ,checkSetAssignment - ,checkExportedExpansions - ,checkAliasesUsesArgs - ,checkAliasesExpandEarly - ,checkUnsetGlobs - ,checkFindWithoutPath - ,checkTimeParameters - ,checkTimedCommand - ,checkLocalScope - ,checkDeprecatedTempfile - ,checkDeprecatedEgrep - ,checkDeprecatedFgrep - ,checkWhileGetoptsCase - ,checkCatastrophicRm - ,checkLetUsage - ,checkMvArguments, checkCpArguments, checkLnArguments - ,checkFindRedirections - ,checkReadExpansions - ,checkSudoRedirect - ,checkSudoArgs - ,checkSourceArgs - ,checkChmodDashr - ,checkXargsDashi - ,checkUnquotedEchoSpaces - ,checkEvalArray - ] - ++ map checkArgComparison ("alias" : declaringCommands) - ++ map checkMaskedReturns declaringCommands - ++ map checkMultipleDeclaring declaringCommands - ++ map checkBackreferencingDeclaration declaringCommands - - -optionalChecks = map fst optionalCommandChecks -optionalCommandChecks :: [(CheckDescription, CommandCheck)] -optionalCommandChecks = [ - (newCheckDescription { - cdName = "deprecate-which", - cdDescription = "Suggest 'command -v' instead of 'which'", - cdPositive = "which javac", - cdNegative = "command -v javac" - }, checkWhich) - ] -optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks - -prop_verifyOptionalExamples = all check optionalCommandChecks - where - check (desc, check) = - verify check (cdPositive desc) - && verifyNot check (cdNegative desc) - --- Run a check against the getopt parser. If it fails, the lists are empty. -checkGetOpts str flags args f = - flags == actualFlags && args == actualArgs - where - toTokens = map (T_Literal (Id 0)) . words - opts = fromMaybe [] $ f (toTokens str) - actualFlags = filter (not . null) $ map fst opts - actualArgs = [onlyLiteralString x | ("", (_, x)) <- opts] - --- Short options -prop_checkGetOptsS1 = checkGetOpts "-f x" ["f"] [] $ getOpts (True, True) "f:" [] -prop_checkGetOptsS2 = checkGetOpts "-fx" ["f"] [] $ getOpts (True, True) "f:" [] -prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True) "fx" [] -prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" [] -prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" [] - -prop_checkGenericOptsS1 = checkGetOpts "-f x" ["f"] [] $ return . getGenericOpts -prop_checkGenericOptsS2 = checkGetOpts "-abc x" ["a", "b", "c"] [] $ return . getGenericOpts -prop_checkGenericOptsS3 = checkGetOpts "-abc -x" ["a", "b", "c", "x"] [] $ return . getGenericOpts -prop_checkGenericOptsS4 = checkGetOpts "-x" ["x"] [] $ return . getGenericOpts - --- Long options -prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)] -prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)] -prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" [] -prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" [] - -prop_checkGenericOptsL1 = checkGetOpts "--foo=bar" ["foo"] [] $ return . getGenericOpts -prop_checkGenericOptsL2 = checkGetOpts "--foo bar" ["foo"] ["bar"] $ return . getGenericOpts -prop_checkGenericOptsL3 = checkGetOpts "-x --foo" ["x", "foo"] [] $ return . getGenericOpts - --- Know when to terminate -prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" [] -prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" [] -prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" [] -prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" [] - -prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGenericOpts -prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts - - -buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis) -buildCommandMap = foldl' addCheck M.empty - where - addCheck map (CommandCheck name function) = - M.insertWith composeAnalyzers name function map - - -checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis -checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do - name <- getLiteralString cmd - return $ - if | '/' `elem` name -> - M.findWithDefault nullCheck (Basename $ basename name) map t - | name == "builtin", (h:_) <- rest -> - let t' = T_SimpleCommand id cmdPrefix rest - selectedBuiltin = onlyLiteralString h - in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t' - | otherwise -> do - M.findWithDefault nullCheck (Exactly name) map t - M.findWithDefault nullCheck (Basename name) map t - - where - basename = reverse . takeWhile (/= '/') . reverse -checkCommand _ _ = return () - -getChecker :: [CommandCheck] -> Checker -getChecker list = Checker { - perScript = const $ return (), - perToken = checkCommand map - } - where - map = buildCommandMap list - - -checker :: AnalysisSpec -> Parameters -> Checker -checker spec params = getChecker $ commandChecks ++ optionals - where - keys = asOptionalChecks spec - optionals = - if "all" `elem` keys - then map snd optionalCommandChecks - else mapMaybe (\x -> M.lookup x optionalCheckMap) keys - -prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" -prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'" -prop_checkTr2a = verify checkTr "tr '[a-z]' '[A-Z]'" -prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'" -prop_checkTr3a = verifyNot checkTr "tr -d '[:upper:]'" -prop_checkTr3b = verifyNot checkTr "tr -d '|/_[:upper:]'" -prop_checkTr4 = verifyNot checkTr "ls [a-z]" -prop_checkTr5 = verify checkTr "tr foo bar" -prop_checkTr6 = verify checkTr "tr 'hello' 'world'" -prop_checkTr8 = verifyNot checkTr "tr aeiou _____" -prop_checkTr9 = verifyNot checkTr "a-z n-za-m" -prop_checkTr10 = verifyNot checkTr "tr --squeeze-repeats rl lr" -prop_checkTr11 = verifyNot checkTr "tr abc '[d*]'" -prop_checkTr12 = verifyNot checkTr "tr '[=e=]' 'e'" -checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments) - where - f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme? - warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion." - f word = - case getLiteralString word of - Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets." - Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets." - Just s -> do -- Eliminate false positives by only looking for dupes in SET2? - when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $ - info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)." - unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $ - when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $ - info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets." - Nothing -> return () - - duplicated s = - let relevant = filter isAlpha s - in relevant /= nub relevant - -prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php" -prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)" -prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'" -checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where - acceptsGlob s = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ] - f [] = return () - f (x:xs) = foldr g (const $ return ()) xs x - g b acc a = do - forM_ (getLiteralString a) $ \s -> when (acceptsGlob s && isGlob b) $ - warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it." - acc b - - -prop_checkExpr = verify checkExpr "foo=$(expr 3 + 2)" -prop_checkExpr2 = verify checkExpr "foo=`echo \\`expr 3 + 2\\``" -prop_checkExpr3 = verifyNot checkExpr "foo=$(expr foo : regex)" -prop_checkExpr4 = verifyNot checkExpr "foo=$(expr foo \\< regex)" -prop_checkExpr5 = verify checkExpr "# shellcheck disable=SC2003\nexpr match foo bar" -prop_checkExpr6 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo : fo*" -prop_checkExpr7 = verify checkExpr "# shellcheck disable=SC2003\nexpr 5 -3" -prop_checkExpr8 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr \"$@\"" -prop_checkExpr9 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr 5 $rest" -prop_checkExpr10 = verify checkExpr "# shellcheck disable=SC2003\nexpr length \"$var\"" -prop_checkExpr11 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo > bar" -prop_checkExpr12 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 | 2" -prop_checkExpr13 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 * 2" -prop_checkExpr14 = verify checkExpr "# shellcheck disable=SC2003\nexpr \"$x\" >= \"$y\"" - -checkExpr = CommandCheck (Basename "expr") f where - f t = do - when (all (`notElem` exceptions) (words $ arguments t)) $ - style (getId $ getCommandTokenOrThis t) 2003 - "expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]." - - case arguments t of - [lhs, op, rhs] -> do - checkOp lhs - case getWordParts op of - [T_Glob _ "*"] -> - err (getId op) 2304 - "* must be escaped to multiply: \\*. Modern $((x * y)) avoids this issue." - [T_Literal _ ":"] | isGlob rhs -> - warn (getId rhs) 2305 - "Quote regex argument to expr to avoid it expanding as a glob." - _ -> return () - - [single] | not (willSplit single) -> - warn (getId single) 2307 - "'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|." - - [first, second] | - onlyLiteralString first /= "length" - && not (willSplit first || willSplit second) -> do - checkOp first - warn (getId t) 2307 - "'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|." - - (first:rest) -> do - checkOp first - forM_ rest $ \t -> - -- We already find 95%+ of multiplication and regex earlier, so don't bother classifying this further. - when (isGlob t) $ warn (getId t) 2306 "Escape glob characters in arguments to expr to avoid pathname expansion." - - _ -> return () - - -- These operators are hard to replicate in POSIX - exceptions = [ ":", "<", ">", "<=", ">=", - -- We can offer better suggestions for these - "match", "length", "substr", "index"] - words = mapMaybe getLiteralString - - checkOp side = - case getLiteralString side of - Just "match" -> msg "'expr match' has unspecified results. Prefer 'expr str : regex'." - Just "length" -> msg "'expr length' has unspecified results. Prefer ${#var}." - Just "substr" -> msg "'expr substr' has unspecified results. Prefer 'cut' or ${var#???}." - Just "index" -> msg "'expr index' has unspecified results. Prefer x=${var%%[chars]*}; $((${#x}+1))." - _ -> return () - where - msg = info (getId side) 2308 - - -prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3" -prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3" -prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file" -prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3" -prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *" -prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3" -prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file" -prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg" -prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file" -prop_checkGrepRe10 = verifyNot checkGrepRe "grep '^aa*' file" -prop_checkGrepRe11 = verifyNot checkGrepRe "grep --include=*.png foo" -prop_checkGrepRe12 = verifyNot checkGrepRe "grep -F 'Foo*' file" -prop_checkGrepRe13 = verifyNot checkGrepRe "grep -- -foo bar*" -prop_checkGrepRe14 = verifyNot checkGrepRe "grep -e -foo bar*" -prop_checkGrepRe15 = verifyNot checkGrepRe "grep --regex -foo bar*" -prop_checkGrepRe16 = verifyNot checkGrepRe "grep --include 'Foo*' file" -prop_checkGrepRe17 = verifyNot checkGrepRe "grep --exclude 'Foo*' file" -prop_checkGrepRe18 = verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file" -prop_checkGrepRe19 = verify checkGrepRe "grep -- 'Foo*' file" -prop_checkGrepRe20 = verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file" -prop_checkGrepRe21 = verifyNot checkGrepRe "grep -o 'x*' file" -prop_checkGrepRe22 = verifyNot checkGrepRe "grep --only-matching 'x*' file" -prop_checkGrepRe23 = verifyNot checkGrepRe "grep '.*' file" - -checkGrepRe = CommandCheck (Basename "grep") check where - check cmd = f cmd (arguments cmd) - -- --regex=*(extglob) doesn't work. Fixme? - skippable s = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s - f _ [] = return () - f cmd (x:r) = - let str = getLiteralStringDef "_" x - in - if str `elem` ["--", "-e", "--regex"] - then checkRE cmd r -- Regex is *after* this - else - if skippable str - then f cmd r -- Regex is elsewhere - else checkRE cmd (x:r) -- Regex is this - - checkRE _ [] = return () - checkRE cmd (re:_) = do - when (isGlob re) $ - warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it." - - unless (any (`elem` flags) grepGlobFlags) $ do - let string = concat $ oversimplify re - if isConfusedGlobRegex string then - warn (getId re) 2063 "Grep uses regex, but this looks like a glob." - else sequence_ $ do - char <- getSuspiciousRegexWildcard string - return $ info (getId re) 2022 $ - "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'." - where - flags = map snd $ getAllFlags cmd - grepGlobFlags = ["fixed-strings", "F", "include", "exclude", "exclude-dir", "o", "only-matching"] - - wordStartingWith c = - headOrDefault (c:"test") . filter ([c] `isPrefixOf`) $ candidates - where - candidates = - sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords - - getSuspiciousRegexWildcard str = case matchRegex suspicious str of - Just [[c]] | not (str `matches` contra) -> Just c - _ -> fail "looks good" - suspicious = mkRegex "([A-Za-z1-9])\\*" - contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]" - - -prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT" -prop_checkTrapQuotes1a = verify checkTrapQuotes "trap \"echo `ls`\" INT" -prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT" -prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG" -checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where - f (x:_) = checkTrap x - f _ = return () - checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs - checkTrap _ = return () - warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled." - checkExpansions (T_DollarExpansion id _) = warning id - checkExpansions (T_Backticked id _) = warning id - checkExpansions (T_DollarBraced id _ _) = warning id - checkExpansions (T_DollarArithmetic id _) = warning id - checkExpansions _ = return () - - -prop_checkReturn1 = verifyNot checkReturn "return" -prop_checkReturn2 = verifyNot checkReturn "return 1" -prop_checkReturn3 = verifyNot checkReturn "return $var" -prop_checkReturn4 = verifyNot checkReturn "return $((a|b))" -prop_checkReturn5 = verify checkReturn "return -1" -prop_checkReturn6 = verify checkReturn "return 1000" -prop_checkReturn7 = verify checkReturn "return 'hello world'" -checkReturn = CommandCheck (Exactly "return") (returnOrExit - (\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.") - (\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout.")) - -prop_checkExit1 = verifyNot checkExit "exit" -prop_checkExit2 = verifyNot checkExit "exit 1" -prop_checkExit3 = verifyNot checkExit "exit $var" -prop_checkExit4 = verifyNot checkExit "exit $((a|b))" -prop_checkExit5 = verify checkExit "exit -1" -prop_checkExit6 = verify checkExit "exit 1000" -prop_checkExit7 = verify checkExit "exit 'hello world'" -checkExit = CommandCheck (Exactly "exit") (returnOrExit - (\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.") - (\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr.")) - -returnOrExit multi invalid = (f . arguments) - where - f (first:second:_) = - multi (getId first) - f [value] = - when (isInvalid $ literal value) $ - invalid (getId value) - f _ = return () - - isInvalid s = null s || any (not . isDigit) s || length s > 5 - || let value = (read s :: Integer) in value > 255 - - literal token = runIdentity $ getLiteralStringExt lit token - lit (T_DollarBraced {}) = return "0" - lit (T_DollarArithmetic {}) = return "0" - lit (T_DollarExpansion {}) = return "0" - lit (T_Backticked {}) = return "0" - lit _ = return "WTF" - - -prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;" -prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +" -prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;" -checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments) - where - f = void . sequence . mapMaybe check . tails - check (exec:arg:term:_) = do - execS <- getLiteralString exec - termS <- getLiteralString term - let cmdS = getLiteralStringDef " " arg - - guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"] - guard $ cmdS `matches` commandRegex - return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ." - check _ = Nothing - commandRegex = mkRegex "[ |;]" - - -prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'" -prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'" -prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" -prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol" -prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'" -prop_checkUnusedEchoEscapes6 = verify checkUnusedEchoEscapes "echo '\\506'" -prop_checkUnusedEchoEscapes7 = verify checkUnusedEchoEscapes "echo '\\5a'" -prop_checkUnusedEchoEscapes8 = verifyNot checkUnusedEchoEscapes "echo '\\8a'" -prop_checkUnusedEchoEscapes9 = verifyNot checkUnusedEchoEscapes "echo '\\d5a'" -prop_checkUnusedEchoEscapes10 = verify checkUnusedEchoEscapes "echo '\\x4a'" -prop_checkUnusedEchoEscapes11 = verify checkUnusedEchoEscapes "echo '\\xat'" -prop_checkUnusedEchoEscapes12 = verifyNot checkUnusedEchoEscapes "echo '\\xth'" -checkUnusedEchoEscapes = CommandCheck (Basename "echo") f - where - hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})" - f cmd = - whenShell [Sh, Bash, Ksh] $ - unless (cmd `hasFlag` "e") $ - mapM_ examine $ arguments cmd - - examine token = do - let str = onlyLiteralString token - when (str `matches` hasEscapes) $ - info (getId token) 2028 "echo may not expand escape sequences. Use printf." - - -prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;" -prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'" -prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;" -checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments) - where - check args = do - let idStrings = map (\x -> (getId x, onlyLiteralString x)) args - match pattern idStrings - - match _ [] = return () - match [] (next:_) = action next - match (p:tests) ((id, arg):args) = do - when (p arg) $ match tests args - match (p:tests) args - - pattern = [ - (`elem` ["-exec", "-execdir"]), - (`elem` ["sh", "bash", "dash", "ksh"]), - (== "-c") - ] - action (id, arg) = - when ("{}" `isInfixOf` arg) $ - warn id 2156 "Injecting filenames is fragile and insecure. Use parameters." - - -prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +" -prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)" -prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'" -checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments) - where - pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction] - f list | length list < length pattern = return () - f list@(_:rest) = - if and (zipWith ($) pattern list) - then warnFor (list !! (length pattern - 1)) - else f rest - isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ] - isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0", "-fls", "-fprint", "-fprint0", "-fprintf", "-ls", "-ok", "-okdir", "-printf" ] - isParam strs t = fromMaybe False $ do - param <- getLiteralString t - return $ param `elem` strs - warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group." - - -prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b" -prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir" -prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b" -prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b" -prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b" -prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b" -prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b" -prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b" -prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b" -prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b" -prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b" -prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b" -prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b" -prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b" -prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel" -prop_checkMkdirDashPM15 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin" -prop_checkMkdirDashPM16 = verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden" -prop_checkMkdirDashPM17 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin" -prop_checkMkdirDashPM18 = verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden" -prop_checkMkdirDashPM19 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin" -prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin" -prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin" -checkMkdirDashPM = CommandCheck (Basename "mkdir") check - where - check t = sequence_ $ do - let flags = getAllFlags t - dashP <- find (\(_,f) -> f == "p" || f == "parents") flags - dashM <- find (\(_,f) -> f == "m" || f == "mode") flags - -- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not. - guard $ any couldHaveSubdirs (drop 1 $ arguments t) - return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory." - couldHaveSubdirs t = fromMaybe True $ do - name <- getLiteralString t - return $ '/' `elem` name && not (name `matches` re) - re = mkRegex "^(\\.\\.?\\/)+[^/]+$" - - -prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8" -prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0" -prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14" -prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL" -prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9" -prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop" -prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' int" -checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments) - where - f args = case args of - first:rest | not $ isFlag first -> mapM_ check rest - _ -> return () - - check param = sequence_ $ do - str <- getLiteralString param - let id = getId param - return $ sequence_ $ mapMaybe (\f -> f id str) [ - checkNumeric, - checkUntrappable - ] - - checkNumeric id str = do - guard $ not (null str) - guard $ all isDigit str - guard $ str /= "0" -- POSIX exit trap - guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI - return $ warn id 2172 - "Trapping signals by number is not well defined. Prefer signal names." - - checkUntrappable id str = do - guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"] - return $ err id 2173 - "SIGKILL/SIGSTOP can not be trapped." - - -prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER" -prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit" -prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo" -prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script" -checkInteractiveSu = CommandCheck (Basename "su") f - where - f cmd = when (length (arguments cmd) <= 1) $ do - path <- getPathM cmd - when (all undirected path) $ - info (getId cmd) 2117 - "To run commands as another user, use su -c or sudo." - - undirected (T_Pipeline _ _ (_:_:_)) = False - -- This should really just be modifications to stdin, but meh - undirected (T_Redirecting _ (_:_) _) = False - undirected _ = True - - --- This is hard to get right without properly parsing ssh args -prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\"" -prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\"" -prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\"" -prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\"" -checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments) - where - isOption x = "-" `isPrefixOf` (concat $ oversimplify x) - f args = - case partition isOption args of - ([], hostport:r@(_:_)) -> checkArg $ last r - _ -> return () - checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) = - forM_ (find (not . isConstant) parts) $ - \x -> info (getId x) 2029 - "Note that, unescaped, this expands on the client side." - checkArg _ = return () - - -prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\"" -prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'" -prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)" -prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var" -prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar" -prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz" -prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz" -prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"" -prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png" -prop_checkPrintfVar10 = verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz" -prop_checkPrintfVar11 = verifyNot checkPrintfVar "printf '%(%s%s)T' -1" -prop_checkPrintfVar12 = verify checkPrintfVar "printf '%s %s\\n' 1 2 3" -prop_checkPrintfVar13 = verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4" -prop_checkPrintfVar14 = verify checkPrintfVar "printf '%*s\\n' 1" -prop_checkPrintfVar15 = verifyNot checkPrintfVar "printf '%*s\\n' 1 2" -prop_checkPrintfVar16 = verifyNot checkPrintfVar "printf $'string'" -prop_checkPrintfVar17 = verify checkPrintfVar "printf '%-*s\\n' 1" -prop_checkPrintfVar18 = verifyNot checkPrintfVar "printf '%-*s\\n' 1 2" -prop_checkPrintfVar19 = verifyNot checkPrintfVar "printf '%(%s)T'" -prop_checkPrintfVar20 = verifyNot checkPrintfVar "printf '%d %(%s)T' 42" -prop_checkPrintfVar21 = verify checkPrintfVar "printf '%d %(%s)T'" -prop_checkPrintfVar22 = verify checkPrintfVar "printf '%s\n%s' foo" - -checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where - f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest - f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest - f (format:params) = check format params - f _ = return () - - check format more = do - sequence_ $ do - string <- getLiteralString format - let formats = getPrintfFormats string - let formatCount = length formats - let argCount = length more - let pluraliseIfMany word n = if n > 1 then word ++ "s" else word - - return $ if - | argCount == 0 && formatCount == 0 -> - return () -- This is fine - | formatCount == 0 && argCount > 0 -> - err (getId format) 2182 - "This printf format string has no variables. Other arguments are ignored." - | any mayBecomeMultipleArgs more -> - return () -- We don't know so trust the user - | argCount < formatCount && onlyTrailingTs formats argCount -> - return () -- Allow trailing %()Ts since they use the current time - | argCount > 0 && argCount `mod` formatCount == 0 -> - return () -- Great: a suitable number of arguments - | otherwise -> - warn (getId format) 2183 $ - "This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount ++ - ", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "." - - unless ('%' `elem` concat (oversimplify format) || isLiteral format) $ - info (getId format) 2059 - "Don't use variables in the printf format string. Use printf '..%s..' \"$foo\"." - where - onlyTrailingTs format argCount = - all (== 'T') $ drop argCount format - - -prop_checkGetPrintfFormats1 = getPrintfFormats "%s" == "s" -prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s" -prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T" -prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT" -prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb" -prop_checkGetPrintfFormats6 = getPrintfFormats "%s%s" == "ss" -prop_checkGetPrintfFormats7 = getPrintfFormats "%s\n%s" == "ss" -prop_checkGetPrintfFormats8 = getPrintfFormats "%ld" == "d" -prop_checkGetPrintfFormats9 = getPrintfFormats "%lld" == "d" -prop_checkGetPrintfFormats10 = getPrintfFormats "%Q" == "Q" -getPrintfFormats = getFormats - where - -- Get the arguments in the string as a string of type characters, - -- e.g. "Hello %s" -> "s" and "%(%s)T %0*d\n" -> "T*d" - getFormats :: String -> String - getFormats string = - case string of - '%':'%':rest -> getFormats rest - '%':'(':rest -> - case dropWhile (/= ')') rest of - ')':c:trailing -> c : getFormats trailing - _ -> "" - '%':rest -> regexBasedGetFormats rest - _:rest -> getFormats rest - [] -> "" - - regexBasedGetFormats rest = - case matchRegex re rest of - Just [width, precision, len, typ, rest, _] -> - (if width == "*" then "*" else "") ++ - (if precision == "*" then "*" else "") ++ - typ ++ getFormats rest - Nothing -> take 1 rest ++ getFormats rest - where - -- constructed based on specifications in "man printf" - re = mkRegex "^#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)(hh|h|l|ll|q|L|j|z|Z|t)?([diouxXfFeEgGaAcsbqQSC])((\n|.)*)" - -- \____ _____/\___ ____/ \____ ____/\__________ ___________/\___________ ___________/\___ ___/ - -- V V V V V V - -- flags field width precision length modifier format character rest - -- field width and precision can be specified with an '*' instead of a digit, - -- in which case printf will accept one more argument for each '*' used - - -prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" -prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`" -prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\"" -prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\"" -prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\"" -prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$( True - _ -> False - --- Return the single variable expansion that makes up this word, if any. --- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing -getSingleUnmodifiedBracedString :: Token -> Maybe String -getSingleUnmodifiedBracedString word = - case getWordParts word of - [T_DollarBraced _ _ l] -> - let contents = concat $ oversimplify l - name = getBracedReference contents - in guard (contents == name) >> return contents - _ -> Nothing - -prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'" -prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'" -prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\"" -checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments) - where - re = mkRegex "\\$\\{?[0-9*@]" - f = mapM_ checkArg - checkArg arg = - let string = getLiteralStringDef "_" arg in - when ('=' `elem` string && string `matches` re) $ - err (getId arg) 2142 - "Aliases can't use positional parameters. Use a function." - - -prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\"" -prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p" -prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'" -checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments) - where - f = mapM_ checkArg - checkArg arg | '=' `elem` concat (oversimplify arg) = - forM_ (find (not . isLiteral) $ getWordParts arg) $ - \x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping." - checkArg _ = return () - - -prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]" -prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo" -prop_checkUnsetGlobs3 = verify checkUnsetGlobs "unset foo[$i]" -prop_checkUnsetGlobs4 = verify checkUnsetGlobs "unset foo[x${i}y]" -prop_checkUnsetGlobs5 = verifyNot checkUnsetGlobs "unset foo][" -checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments) - where - check arg = - when (isGlob arg) $ - warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded." - - -prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f" -prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find" -prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f" -prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print" -prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ." -prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ." -prop_checkFindWithoutPath7 = verifyNot checkFindWithoutPath "find --help" -prop_checkFindWithoutPath8 = verifyNot checkFindWithoutPath "find -Hx . -print" -checkFindWithoutPath = CommandCheck (Basename "find") f - where - f t@(T_SimpleCommand _ _ (cmd:args)) = - unless (t `hasFlag` "help" || hasPath args) $ - info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly." - - -- This is a bit of a kludge. find supports flag arguments both before and - -- after the path, as well as multiple non-flag arguments that are not the - -- path. We assume that all the pre-path flags are single characters from a - -- list of GNU and macOS flags. - hasPath (first:rest) = - let flag = getLiteralStringDef "___" first in - not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest - hasPath [] = False - isLeadingFlag flag = length flag <= 2 || all (`elem` leadingFlagChars) flag - leadingFlagChars="-EHLPXdfsxO0123456789" - - -prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10" -prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10" -prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo" -prop_checkTimeParameters4 = verifyNot checkTimeParameters "command time -f lol sleep 10" -checkTimeParameters = CommandCheck (Exactly "time") f - where - f (T_SimpleCommand _ _ (cmd:args:_)) = - whenShell [Bash, Sh] $ - let s = concat $ oversimplify args in - when ("-" `isPrefixOf` s && s /= "-p") $ - info (getId cmd) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one." - - f _ = return () - -prop_checkTimedCommand1 = verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar" -prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )" -prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1" -checkTimedCommand = CommandCheck (Exactly "time") f where - f (T_SimpleCommand _ _ (c:args@(_:_))) = - whenShell [Sh, Dash, BusyboxSh] $ do - let cmd = last args -- "time" is parsed with a command as argument - when (isPiped cmd) $ - warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead." - when (isSimple cmd == Just False) $ - warn (getId cmd) 2177 "'time' is undefined for compound commands, time sh -c instead." - f _ = return () - isPiped cmd = - case cmd of - T_Pipeline _ _ (_:_:_) -> True - _ -> False - getCommand cmd = - case cmd of - T_Pipeline _ _ (T_Redirecting _ _ a : _) -> return a - _ -> fail "" - isSimple cmd = do - innerCommand <- getCommand cmd - case innerCommand of - T_SimpleCommand {} -> return True - _ -> return False - -prop_checkLocalScope1 = verify checkLocalScope "local foo=3" -prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }" -checkLocalScope = CommandCheck (Exactly "local") $ \t -> - whenShell [Bash, Dash, BusyboxSh] $ do -- Ksh allows it, Sh doesn't support local - path <- getPathM t - unless (any isFunctionLike path) $ - err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions." - -prop_checkMultipleDeclaring1 = verify (checkMultipleDeclaring "local") "q() { local readonly var=1; }" -prop_checkMultipleDeclaring2 = verifyNot (checkMultipleDeclaring "local") "q() { local var=1; }" -prop_checkMultipleDeclaring3 = verify (checkMultipleDeclaring "readonly") "readonly local foo=5" -prop_checkMultipleDeclaring4 = verify (checkMultipleDeclaring "export") "export readonly foo=5" -prop_checkMultipleDeclaring5 = verifyNot (checkMultipleDeclaring "local") "f() { local -r foo=5; }" -prop_checkMultipleDeclaring6 = verifyNot (checkMultipleDeclaring "declare") "declare -rx foo=5" -prop_checkMultipleDeclaring7 = verifyNot (checkMultipleDeclaring "readonly") "readonly 'local' foo=5" -checkMultipleDeclaring cmd = CommandCheck (Exactly cmd) (mapM_ check . arguments) - where - check t = sequence_ $ do - lit <- getUnquotedLiteral t - guard $ lit `elem` declaringCommands - return $ err (getId $ getCommandTokenOrThis t) 2316 $ - "This applies " ++ cmd ++ " to the variable named " ++ lit ++ - ", which is probably not what you want. Use a separate command or the appropriate `declare` options instead." - -prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)" -prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)" -checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $ - \t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead." - -prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'" -checkDeprecatedEgrep = CommandCheck (Basename "egrep") $ - \t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead." - -prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files" -checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $ - \t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead." - -prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done" -prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done" -prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done" -prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done" -prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done" -prop_checkWhileGetoptsCase6 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $y in a) foo;; esac; done" -prop_checkWhileGetoptsCase7 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case x$x in xa) foo;; xb) foo;; esac; done" -prop_checkWhileGetoptsCase8 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do x=a; case $x in a) foo;; esac; done" -checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f - where - f :: Token -> Analysis - f t@(T_SimpleCommand _ _ (cmd:arg1:name:_)) = do - path <- getPathM t - params <- ask - sequence_ $ do - options <- getLiteralString arg1 - getoptsVar <- getLiteralString name - (T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path) - T_CaseExpression id var list <- mapMaybe findCase body !!! 0 - - -- Make sure getopts name and case variable matches - [T_DollarBraced _ _ bracedWord] <- return $ getWordParts var - [T_Literal _ caseVar] <- return $ getWordParts bracedWord - guard $ caseVar == getoptsVar - - -- Make sure the variable isn't modified - guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar - - return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list - f _ = return () - - check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis - check optId opts id list = do - unless (Nothing `M.member` handledMap) $ do - mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled - - unless (any (`M.member` handledMap) [Just "*",Just "?"]) $ - warn id 2220 "Invalid flags are not handled. Add a *) case." - - mapM_ warnRedundant $ M.toList notRequested - - where - handledMap = M.fromList (concatMap getHandledStrings list) - requestedMap = M.fromList $ map (\x -> (Just x, ())) opts - - notHandled = M.difference requestedMap handledMap - notRequested = M.difference handledMap requestedMap - - warnUnhandled optId caseId str = - warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'." - - warnRedundant (Just str, expr) - | str `notElem` ["*", ":", "?"] = - warn (getId expr) 2214 "This case is not specified by getopts." - warnRedundant _ = return () - - getHandledStrings (_, globs, _) = - map (\x -> (literal x, x)) globs - - literal :: Token -> Maybe String - literal t = do - getLiteralString t <> fromGlob t - - fromGlob t = - case t of - T_Glob _ ['[', c, ']'] -> return [c] - T_Glob _ "*" -> return "*" - T_Glob _ "?" -> return "?" - _ -> Nothing - - whileLoop t = - case t of - T_WhileExpression {} -> return True - T_Script {} -> return False - _ -> Nothing - - findCase t = - case t of - T_Annotation _ _ x -> findCase x - T_Pipeline _ _ [x] -> findCase x - T_Redirecting _ _ x@(T_CaseExpression {}) -> return x - _ -> Nothing - -prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2" -prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo" -prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*" -prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*" -prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*" -prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*" -prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home" -prop_checkCatastrophicRm10 = verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}" -prop_checkCatastrophicRm11 = verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec" -prop_checkCatastrophicRm12 = verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec" -prop_checkCatastrophicRm13 = verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec" -prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg" -prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*" -checkCatastrophicRm = CommandCheck (Basename "rm") $ \t -> - when (isRecursive t) $ - mapM_ (mapM_ checkWord . braceExpand) $ arguments t - where - isRecursive = any ((`elem` ["r", "R", "recursive"]) . snd) . getAllFlags - - checkWord token = - case getLiteralString token of - Just str -> - when (fixPath str `elem` importantPaths) $ - warn (getId token) 2114 "Warning: deletes a system directory." - Nothing -> - checkWord' token - - checkWord' token = sequence_ $ do - filename <- getPotentialPath token - let path = fixPath filename - return . when (path `elem` importantPaths) $ - warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ." - - fixPath filename = - let normalized = skipRepeating '/' . skipRepeating '*' $ filename in - if normalized == "/" then normalized else stripTrailing '/' normalized - - getPotentialPath = getLiteralStringExt f - where - f (T_Glob _ str) = return str - f (T_DollarBraced _ _ word) = - let var = onlyLiteralString word in - -- This shouldn't handle non-colon cases. - if any (`isInfixOf` var) [":?", ":-", ":="] - then Nothing - else return "" - f _ = return "" - - stripTrailing c = reverse . dropWhile (== c) . reverse - skipRepeating c = foldr go [] - where - go a r = a : case r of b:rest | b == c && a == b -> rest; _ -> r - - paths = [ - "", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local", - "/var", "/lib", "/dev", "/media", "/boot", "/lib64", "/usr/bin" - ] - importantPaths = filter (not . null) $ - ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths) - - -prop_checkLetUsage1 = verify checkLetUsage "let a=1" -prop_checkLetUsage2 = verifyNot checkLetUsage "(( a=1 ))" -checkLetUsage = CommandCheck (Exactly "let") f - where - f t = whenShell [Bash,Ksh] $ do - style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ." - - -missingDestination handler token = do - case params of - [single] -> do - unless (hasTarget || mayBecomeMultipleArgs single) $ - handler token - _ -> return () - where - args = getAllFlags token - params = [x | (x,"") <- args] - hasTarget = - any (\(_,x) -> x /= "" && x `isPrefixOf` "target-directory") args - -prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'" -prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar" -prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}" -prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\"" -prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar" -prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar" -prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar" -prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version" -prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\"" -checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f - where - f t = err (getId t) 2224 "This mv has no destination. Check the arguments." - -checkCpArguments = CommandCheck (Basename "cp") $ missingDestination f - where - f t = err (getId t) 2225 "This cp has no destination. Check the arguments." - -checkLnArguments = CommandCheck (Basename "ln") $ missingDestination f - where - f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly." - - -prop_checkFindRedirections1 = verify checkFindRedirections "find . -exec echo {} > file \\;" -prop_checkFindRedirections2 = verifyNot checkFindRedirections "find . -exec echo {} \\; > file" -prop_checkFindRedirections3 = verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;" -checkFindRedirections = CommandCheck (Basename "find") f - where - f t = do - redirecting <- getClosestCommandM t - case redirecting of - Just (T_Redirecting _ redirs@(_:_) (T_SimpleCommand _ _ args@(_:_:_))) -> do - -- This assumes IDs are sequential, which is mostly but not always true. - let minRedir = minimum $ map getId redirs - let maxArg = maximum $ map getId args - when (minRedir < maxArg) $ - warn minRedir 2227 - "Redirection applies to the find command itself. Rewrite to work per action (or move to end)." - _ -> return () - -prop_checkWhich = verify checkWhich "which '.+'" -checkWhich = CommandCheck (Basename "which") $ - \t -> info (getId $ getCommandTokenOrThis t) 2230 "'which' is non-standard. Use builtin 'command -v' instead." - -prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" -prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input" -prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file" -prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file" -prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1" -prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log" -prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1" -checkSudoRedirect = CommandCheck (Basename "sudo") f - where - f t = do - t_redir <- getClosestCommandM t - case t_redir of - Just (T_Redirecting _ redirs _) -> - mapM_ warnAbout redirs - warnAbout (T_FdRedirect _ s (T_IoFile id op file)) - | (null s || s == "&") && not (special file) = - case op of - T_Less _ -> - info (getId op) 2024 - "sudo doesn't affect redirects. Use sudo cat file | .." - T_Greater _ -> - warn (getId op) 2024 - "sudo doesn't affect redirects. Use ..| sudo tee file" - T_DGREAT _ -> - warn (getId op) 2024 - "sudo doesn't affect redirects. Use .. | sudo tee -a file" - _ -> return () - warnAbout _ = return () - special file = concat (oversimplify file) == "/dev/null" - -prop_checkSudoArgs1 = verify checkSudoArgs "sudo cd /root" -prop_checkSudoArgs2 = verify checkSudoArgs "sudo export x=3" -prop_checkSudoArgs3 = verifyNot checkSudoArgs "sudo ls /usr/local/protected" -prop_checkSudoArgs4 = verifyNot checkSudoArgs "sudo ls && export x=3" -prop_checkSudoArgs5 = verifyNot checkSudoArgs "sudo echo ls" -prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls" -prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo" -checkSudoArgs = CommandCheck (Basename "sudo") f - where - f t = sequence_ $ do - opts <- parseOpts $ arguments t - (_,(commandArg, _)) <- find (null . fst) opts - command <- getLiteralString commandArg - guard $ command `elem` builtins - return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?" - builtins = [ "cd", "command", "declare", "eval", "exec", "exit", "export", "hash", "history", "local", "popd", "pushd", "read", "readonly", "return", "set", "source", "trap", "type", "typeset", "ulimit", "umask", "unset", "wait" ] - -- This mess is why ShellCheck prefers not to know. - parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:" - -prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg" -prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script" -prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg" -checkSourceArgs = CommandCheck (Exactly ".") f - where - f t = whenShell [Sh, Dash] $ - case arguments t of - (file:arg1:_) -> warn (getId arg1) 2240 $ - "The dot command does not support arguments in sh/dash. Set them as variables." - _ -> return () - -prop_checkChmodDashr1 = verify checkChmodDashr "chmod -r 0755 dir" -prop_checkChmodDashr2 = verifyNot checkChmodDashr "chmod -R 0755 dir" -prop_checkChmodDashr3 = verifyNot checkChmodDashr "chmod a-r dir" -checkChmodDashr = CommandCheck (Basename "chmod") f - where - f t = mapM_ check $ arguments t - check t = sequence_ $ do - flag <- getLiteralString t - guard $ flag == "-r" - return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions." - -prop_checkXargsDashi1 = verify checkXargsDashi "xargs -i{} echo {}" -prop_checkXargsDashi2 = verifyNot checkXargsDashi "xargs -I{} echo {}" -prop_checkXargsDashi3 = verifyNot checkXargsDashi "xargs sed -i -e foo" -prop_checkXargsDashi4 = verify checkXargsDashi "xargs -e sed -i foo" -prop_checkXargsDashi5 = verifyNot checkXargsDashi "xargs -x sed -i foo" -checkXargsDashi = CommandCheck (Basename "xargs") f - where - f t = sequence_ $ do - opts <- parseOpts $ arguments t - (option, value) <- lookup "i" opts - return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}" - parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:" - - -prop_checkArgComparison1 = verify (checkArgComparison "declare") "declare a = b" -prop_checkArgComparison2 = verify (checkArgComparison "declare") "declare a =b" -prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b" -prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b" -prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo" -prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0" -prop_checkArgComparison9 = verify (checkArgComparison "alias") "alias x =0" --- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export -checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual - where - wordsWithEqual t = mapM_ check $ arguments t - check arg = do - sequence_ $ do - str <- getLeadingUnquotedString arg - case str of - '=':_ -> - return $ err (headId arg) 2290 $ - "Remove spaces around = to assign." - '+':'=':_ -> - return $ err (headId arg) 2290 $ - "Remove spaces around += to append." - _ -> Nothing - - -- 'let' is parsed as a sequence of arithmetic expansions, - -- so we want the additional warning for "x=" - when (cmd == "let") $ sequence_ $ do - token <- getTrailingUnquotedLiteral arg - str <- getLiteralString token - guard $ "=" `isSuffixOf` str - return $ err (getId token) 2290 $ - "Remove spaces around = to assign." - - headId t = - case t of - T_NormalWord _ (x:_) -> getId x - _ -> getId t - - -prop_checkMaskedReturns1 = verify (checkMaskedReturns "local") "f() { local a=$(false); }" -prop_checkMaskedReturns2 = verify (checkMaskedReturns "declare") "declare a=$(false)" -prop_checkMaskedReturns3 = verify (checkMaskedReturns "declare") "declare a=\"`false`\"" -prop_checkMaskedReturns4 = verify (checkMaskedReturns "readonly") "readonly a=$(false)" -prop_checkMaskedReturns5 = verify (checkMaskedReturns "readonly") "readonly a=\"`false`\"" -prop_checkMaskedReturns6 = verifyNot (checkMaskedReturns "declare") "declare a; a=$(false)" -prop_checkMaskedReturns7 = verifyNot (checkMaskedReturns "local") "f() { local -r a=$(false); }" -prop_checkMaskedReturns8 = verifyNot (checkMaskedReturns "readonly") "a=$(false); readonly a" -prop_checkMaskedReturns9 = verify (checkMaskedReturns "typeset") "#!/bin/ksh\n f() { typeset -r x=$(false); }" -prop_checkMaskedReturns10 = verifyNot (checkMaskedReturns "typeset") "#!/bin/ksh\n function f { typeset -r x=$(false); }" -prop_checkMaskedReturns11 = verifyNot (checkMaskedReturns "typeset") "#!/bin/bash\n f() { typeset -r x=$(false); }" -prop_checkMaskedReturns12 = verify (checkMaskedReturns "typeset") "typeset -r x=$(false);" -prop_checkMaskedReturns13 = verify (checkMaskedReturns "typeset") "f() { typeset -g x=$(false); }" -prop_checkMaskedReturns14 = verify (checkMaskedReturns "declare") "declare x=${ false; }" -prop_checkMaskedReturns15 = verify (checkMaskedReturns "declare") "f() { declare x=$(false); }" -checkMaskedReturns str = CommandCheck (Exactly str) checkCmd - where - checkCmd t = do - path <- getPathM t - shell <- asks shellType - sequence_ $ do - name <- getCommandName t - - let flags = map snd (getAllFlags t) - let hasDashR = "r" `elem` flags - let hasDashG = "g" `elem` flags - let isInScopedFunction = any (isScopedFunction shell) path - - let isLocal = not hasDashG && isLocalInFunction name && isInScopedFunction - let isReadOnly = name == "readonly" || hasDashR - - -- Don't warn about local variables that are declared readonly, - -- because the workaround `local x; x=$(false); local -r x;` is annoying - guard . not $ isLocal && isReadOnly - - return $ mapM_ checkArgs $ arguments t - - checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word = - warn id 2155 "Declare and assign separately to avoid masking return values." - checkArgs _ = return () - - isLocalInFunction = (`elem` ["local", "declare", "typeset"]) - isScopedFunction shell t = - case t of - T_BatsTest {} -> True - -- In ksh, only functions declared with 'function' have their own scope - T_Function _ (FunctionKeyword hasFunction) _ _ _ -> shell /= Ksh || hasFunction - _ -> False - - hasReturn t = case t of - T_Backticked {} -> True - T_DollarExpansion {} -> True - T_DollarBraceCommandExpansion {} -> True - _ -> False - - -prop_checkUnquotedEchoSpaces1 = verify checkUnquotedEchoSpaces "echo foo bar" -prop_checkUnquotedEchoSpaces2 = verifyNot checkUnquotedEchoSpaces "echo foo" -prop_checkUnquotedEchoSpaces3 = verifyNot checkUnquotedEchoSpaces "echo foo bar" -prop_checkUnquotedEchoSpaces4 = verifyNot checkUnquotedEchoSpaces "echo 'foo bar'" -prop_checkUnquotedEchoSpaces5 = verifyNot checkUnquotedEchoSpaces "echo a > myfile.txt b" -prop_checkUnquotedEchoSpaces6 = verifyNot checkUnquotedEchoSpaces " echo foo\\\n bar" -checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check - where - check t = do - let args = arguments t - m <- asks tokenPositions - redir <- getClosestCommandM t - sequence_ $ do - let positions = mapMaybe (\c -> M.lookup (getId c) m) args - let pairs = zip positions (drop 1 positions) - (T_Redirecting _ redirTokens _) <- redir - let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens - guard $ any (hasSpacesBetween redirPositions) pairs - return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one." - - hasSpacesBetween redirs ((a,b), (c,d)) = - posLine a == posLine d - && ((posColumn c) - (posColumn b)) >= 4 - && not (any (\x -> b < x && x < c) redirs) - - -prop_checkEvalArray1 = verify checkEvalArray "eval $@" -prop_checkEvalArray2 = verify checkEvalArray "eval \"${args[@]}\"" -prop_checkEvalArray3 = verify checkEvalArray "eval \"${args[@]@Q}\"" -prop_checkEvalArray4 = verifyNot checkEvalArray "eval \"${args[*]@Q}\"" -prop_checkEvalArray5 = verifyNot checkEvalArray "eval \"$*\"" -checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordParts . arguments) - where - check t = - when (isArrayExpansion t) $ - if isEscaped t - then style (getId t) 2293 "When eval'ing @Q-quoted words, use * rather than @ as the index." - else warn (getId t) 2294 "eval negates the benefit of arrays. Drop eval to preserve whitespace/symbols (or eval as string)." - - isEscaped q = - case q of - -- Match ${arr[@]@Q} and ${@@Q} and such - T_DollarBraced _ _ l -> 'Q' `elem` getBracedModifier (concat $ oversimplify l) - _ -> False - - -prop_checkBackreferencingDeclaration1 = verify (checkBackreferencingDeclaration "declare") "declare x=1 y=foo$x" -prop_checkBackreferencingDeclaration2 = verify (checkBackreferencingDeclaration "readonly") "readonly x=1 y=$((1+x))" -prop_checkBackreferencingDeclaration3 = verify (checkBackreferencingDeclaration "local") "local x=1 y=$(echo $x)" -prop_checkBackreferencingDeclaration4 = verify (checkBackreferencingDeclaration "local") "local x=1 y[$x]=z" -prop_checkBackreferencingDeclaration5 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1" -prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1" -prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x" -checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check - where - check t = do - maybeCfga <- asks cfgAnalysis - mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga - - perArg cfga leftArgs t = - case t of - T_Assignment id _ name idx t -> do - warnIfBackreferencing cfga leftArgs $ t:idx - return $ M.insert name id leftArgs - t -> do - warnIfBackreferencing cfga leftArgs [t] - return leftArgs - - warnIfBackreferencing cfga backrefs l = do - references <- findReferences cfga l - let reused = M.intersection backrefs references - mapM msg $ M.toList reused - - msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s." - - findReferences cfga list = do - let graph = CF.graph cfga - let nodesMap = CF.tokenToNodes cfga - let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list - let labels = mapMaybe (G.lab graph) $ S.toList nodes - let references = M.fromList $ concatMap refFromLabel labels - return references - - refFromLabel lab = - case lab of - CFApplyEffects effects -> mapMaybe refFromEffect effects - _ -> [] - refFromEffect e = - case e of - IdTagged id (CFReadVariable name) -> return (name, id) - _ -> Nothing - - -return [] -runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/Checks/ControlFlow.hs b/src/ShellCheck/Checks/ControlFlow.hs deleted file mode 100644 index 9f63141..0000000 --- a/src/ShellCheck/Checks/ControlFlow.hs +++ /dev/null @@ -1,101 +0,0 @@ -{- - Copyright 2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} - --- Checks that run on the Control Flow Graph (as opposed to the AST) --- This is scaffolding for a work in progress. - -module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where - -import ShellCheck.AST -import ShellCheck.ASTLib -import ShellCheck.CFG hiding (cfgAnalysis) -import ShellCheck.CFGAnalysis -import ShellCheck.AnalyzerLib -import ShellCheck.Data -import ShellCheck.Interface - -import Control.Monad -import Control.Monad.Reader -import Data.Graph.Inductive.Graph -import qualified Data.Map as M -import qualified Data.Set as S -import Data.List -import Data.Maybe - -import Test.QuickCheck.All (forAllProperties) -import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) - - -optionalChecks :: [CheckDescription] -optionalChecks = [] - --- A check that runs on the entire graph -type ControlFlowCheck = Analysis --- A check invoked once per node, with its (pre,post) data -type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis --- A check invoked once per effect, with its node's (pre,post) data -type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis - - -checker :: AnalysisSpec -> Parameters -> Checker -checker spec params = Checker { - perScript = const $ sequence_ controlFlowChecks, - perToken = const $ return () -} - -controlFlowChecks :: [ControlFlowCheck] -controlFlowChecks = [ - runNodeChecks controlFlowNodeChecks - ] - -controlFlowNodeChecks :: [ControlFlowNodeCheck] -controlFlowNodeChecks = [ - runEffectChecks controlFlowEffectChecks - ] - -controlFlowEffectChecks :: [ControlFlowEffectCheck] -controlFlowEffectChecks = [ - ] - -runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck -runNodeChecks perNode = do - cfg <- asks cfgAnalysis - mapM_ runOnAll cfg - where - getData datas n@(node, label) = do - (pre, post) <- M.lookup node datas - return (n, (pre, post)) - - runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis - runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode - runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg) - -runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck -runEffectChecks list = checkNode - where - checkNode (node, label) prepost = - case label of - CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects - _ -> return () - - -return [] -runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/Checks/Custom.hs b/src/ShellCheck/Checks/Custom.hs deleted file mode 100644 index 17e9c9e..0000000 --- a/src/ShellCheck/Checks/Custom.hs +++ /dev/null @@ -1,21 +0,0 @@ -{- - This empty file is provided for ease of patching in site specific checks. - However, there are no guarantees regarding compatibility between versions. --} - -{-# LANGUAGE TemplateHaskell #-} -module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where - -import ShellCheck.AnalyzerLib -import Test.QuickCheck - -checker :: Parameters -> Checker -checker params = Checker { - perScript = const $ return (), - perToken = const $ return () - } - -prop_CustomTestsWork = True - -return [] -runTests = $quickCheckAll diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs deleted file mode 100644 index b664879..0000000 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ /dev/null @@ -1,701 +0,0 @@ -{- - Copyright 2012-2020 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE ViewPatterns #-} -module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where - -import ShellCheck.AST -import ShellCheck.ASTLib -import ShellCheck.AnalyzerLib -import ShellCheck.Interface -import ShellCheck.Prelude -import ShellCheck.Regex - -import Control.Monad -import Control.Monad.RWS -import Data.Char -import Data.Functor.Identity -import Data.List -import Data.Maybe -import qualified Data.Map as Map -import qualified Data.Set as Set -import Test.QuickCheck.All (forAllProperties) -import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) - -data ForShell = ForShell [Shell] (Token -> Analysis) - -getChecker params list = Checker { - perScript = nullCheck, - perToken = foldl composeAnalyzers nullCheck $ mapMaybe include list - } - where - shell = shellType params - include (ForShell list a) = do - guard $ shell `elem` list - return a - -checker params = getChecker params checks - -checks = [ - checkForDecimals - ,checkBashisms - ,checkEchoSed - ,checkBraceExpansionVars - ,checkMultiDimensionalArrays - ,checkPS1Assignments - ,checkMultipleBangs - ,checkBangAfterPipe - ,checkNegatedUnaryOps - ] - -testChecker (ForShell _ t) = - Checker { - perScript = nullCheck, - perToken = t - } -verify c s = producesComments (testChecker c) s == Just True -verifyNot c s = producesComments (testChecker c) s == Just False - -prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" -prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" -prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" -checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f - where - f t@(TA_Expansion id _) = sequence_ $ do - first:rest <- getLiteralString t - guard $ isDigit first && '.' `elem` rest - return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk." - f _ = return () - - -prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" -prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]" -prop_checkBashisms3 = verify checkBashisms "echo $((i++))" -prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" -prop_checkBashisms5 = verify checkBashisms "source file" -prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]" -prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42" -prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]" -prop_checkBashisms6d = verify checkBashisms "test foo =~ bar" -prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" -prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" -prop_checkBashisms9 = verify checkBashisms "echo ${!var*}" -prop_checkBashisms10 = verify checkBashisms "echo ${var:4:12}" -prop_checkBashisms11 = verifyNot checkBashisms "echo ${var:-4}" -prop_checkBashisms12 = verify checkBashisms "echo ${var//foo/bar}" -prop_checkBashisms13 = verify checkBashisms "exec -c env" -prop_checkBashisms14 = verify checkBashisms "echo -n \"Foo: \"" -prop_checkBashisms15 = verify checkBashisms "let n++" -prop_checkBashisms16 = verify checkBashisms "echo $RANDOM" -prop_checkBashisms17 = verify checkBashisms "echo $((RANDOM%6+1))" -prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null" -prop_checkBashisms19 = verify checkBashisms "foo > file*.txt" -prop_checkBashisms20 = verify checkBashisms "read -ra foo" -prop_checkBashisms21 = verify checkBashisms "[ -a foo ]" -prop_checkBashisms21b = verify checkBashisms "test -a foo" -prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]" -prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT" -prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM" -prop_checkBashisms25 = verify checkBashisms "cat < /dev/tcp/host/123" -prop_checkBashisms26 = verify checkBashisms "trap mything ERR SIGTERM" -prop_checkBashisms27 = verify checkBashisms "echo *[^0-9]*" -prop_checkBashisms28 = verify checkBashisms "exec {n}>&2" -prop_checkBashisms29 = verify checkBashisms "echo ${!var}" -prop_checkBashisms30 = verify checkBashisms "printf -v '%s' \"$1\"" -prop_checkBashisms31 = verify checkBashisms "printf '%q' \"$1\"" -prop_checkBashisms32 = verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]" -prop_checkBashisms33 = verify checkBashisms "#!/bin/sh\necho -n foo" -prop_checkBashisms34 = verifyNot checkBashisms "#!/bin/dash\necho -n foo" -prop_checkBashisms35 = verifyNot checkBashisms "#!/bin/dash\nlocal foo" -prop_checkBashisms36 = verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar" -prop_checkBashisms37 = verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME" -prop_checkBashisms38 = verify checkBashisms "RANDOM=9; echo $RANDOM" -prop_checkBashisms39 = verify checkBashisms "foo-bar() { true; }" -prop_checkBashisms40 = verify checkBashisms "echo $(/dev/null" -prop_checkBashisms48 = verifyNot checkBashisms "#!/bin/sh\necho $LINENO" -prop_checkBashisms49 = verify checkBashisms "#!/bin/dash\necho $MACHTYPE" -prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file" -prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1" -prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2" -prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var" -prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var" -prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n" -prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar" -prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}" -prop_checkBashisms56 = verifyNot checkBashisms "#!/bin/sh\necho ${##}" -prop_checkBashisms57 = verifyNot checkBashisms "#!/bin/dash\nulimit -c 0" -prop_checkBashisms58 = verify checkBashisms "#!/bin/sh\nulimit -c 0" -prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s" -prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p" -prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp" -prop_checkBashisms62 = verify checkBashisms "#!/bin/sh\nexport -f foo" -prop_checkBashisms63 = verifyNot checkBashisms "#!/bin/sh\nexport -p" -prop_checkBashisms64 = verify checkBashisms "#!/bin/sh\nreadonly -a" -prop_checkBashisms65 = verifyNot checkBashisms "#!/bin/sh\nreadonly -p" -prop_checkBashisms66 = verifyNot checkBashisms "#!/bin/sh\ncd -P ." -prop_checkBashisms67 = verify checkBashisms "#!/bin/sh\ncd -P -e ." -prop_checkBashisms68 = verify checkBashisms "#!/bin/sh\numask -p" -prop_checkBashisms69 = verifyNot checkBashisms "#!/bin/sh\numask -S" -prop_checkBashisms70 = verify checkBashisms "#!/bin/sh\ntrap -l" -prop_checkBashisms71 = verify checkBashisms "#!/bin/sh\ntype -a ls" -prop_checkBashisms72 = verifyNot checkBashisms "#!/bin/sh\ntype ls" -prop_checkBashisms73 = verify checkBashisms "#!/bin/sh\nunset -n namevar" -prop_checkBashisms74 = verifyNot checkBashisms "#!/bin/sh\nunset -f namevar" -prop_checkBashisms75 = verifyNot checkBashisms "#!/bin/sh\necho \"-n foo\"" -prop_checkBashisms76 = verifyNot checkBashisms "#!/bin/sh\necho \"-ne foo\"" -prop_checkBashisms77 = verifyNot checkBashisms "#!/bin/sh\necho -Q foo" -prop_checkBashisms78 = verify checkBashisms "#!/bin/sh\necho -ne foo" -prop_checkBashisms79 = verify checkBashisms "#!/bin/sh\nhash -l" -prop_checkBashisms80 = verifyNot checkBashisms "#!/bin/sh\nhash -r" -prop_checkBashisms81 = verifyNot checkBashisms "#!/bin/dash\nhash -v" -prop_checkBashisms82 = verifyNot checkBashisms "#!/bin/sh\nset -v +o allexport -o errexit -C" -prop_checkBashisms83 = verifyNot checkBashisms "#!/bin/sh\nset --" -prop_checkBashisms84 = verify checkBashisms "#!/bin/sh\nset -o pipefail" -prop_checkBashisms85 = verify checkBashisms "#!/bin/sh\nset -B" -prop_checkBashisms86 = verifyNot checkBashisms "#!/bin/dash\nset -o emacs" -prop_checkBashisms87 = verify checkBashisms "#!/bin/sh\nset -o emacs" -prop_checkBashisms88 = verifyNot checkBashisms "#!/bin/sh\nset -- wget -o foo 'https://some.url'" -prop_checkBashisms89 = verifyNot checkBashisms "#!/bin/sh\nopts=$-\nset -\"$opts\"" -prop_checkBashisms90 = verifyNot checkBashisms "#!/bin/sh\nset -o \"$opt\"" -prop_checkBashisms91 = verify checkBashisms "#!/bin/sh\nwait -n" -prop_checkBashisms92 = verify checkBashisms "#!/bin/sh\necho $((16#FF))" -prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m) ))" -prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]" -prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_" -prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_" -prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}" -prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}" -prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo" -prop_checkBashisms100 = verify checkBashisms "read -r" -prop_checkBashisms101 = verify checkBashisms "read" -prop_checkBashisms102 = verifyNot checkBashisms "read -r foo" -prop_checkBashisms103 = verifyNot checkBashisms "read foo" -prop_checkBashisms104 = verifyNot checkBashisms "read ''" -prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail" -prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]" -prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]" -prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null" -prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM" -prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null" -prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057 -prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057 -prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060 -prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060 -prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053 -prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054 -prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055 -prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056 -prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059 -prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]" -prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]" -prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'" -prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'" -prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test" -prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test" -prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar" -prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo" -prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test" -prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]" -prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp" -prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]" -checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do - params <- ask - kludge params t - where - -- This code was copy-pasted from Analytics where params was a variable - kludge params = bashism - where - isBusyboxSh = shellType params == BusyboxSh - isDash = shellType params == Dash || isBusyboxSh - warnMsg id code s = - if isDash - then err id code $ "In dash, " ++ s ++ " not supported." - else warn id code $ "In POSIX sh, " ++ s ++ " undefined." - asStr = getLiteralString - - bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is" - bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is" - bashism (T_DollarSingleQuoted id _) = - unless isBusyboxSh $ warnMsg id 3003 "$'..' is" - bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is" - bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are" - bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is" - bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is" - bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are" - bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is" - bashism (T_Condition id DoubleBracket _) = - unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is" - bashism (T_HereString id _) = warnMsg id 3011 "here-strings are" - - bashism (TC_Binary id _ op _ _) = - checkTestOp bashismBinaryTestFlags op id - bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) = - checkTestOp bashismBinaryTestFlags op id - bashism (TC_Unary id _ op _) = - checkTestOp bashismUnaryTestFlags op id - bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) = - checkTestOp bashismUnaryTestFlags op id - - bashism (TA_Unary id op _) - | op `elem` [ "|++", "|--", "++|", "--|"] = - warnMsg id 3018 $ filter (/= '|') op ++ " is" - bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are" - bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = - unless isBusyboxSh $ warnMsg id 3020 "&> is" - bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) = - unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is" - bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are" - bashism (T_FdRedirect id num _) - | all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are" - bashism (T_Assignment id Append _ _ _) = - warnMsg id 3024 "+= is" - bashism (T_IoFile id _ word) | isNetworked = - warnMsg id 3025 "/dev/{tcp,udp} is" - where - file = onlyLiteralString word - isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"] - bashism (T_Glob id str) | "[^" `isInfixOf` str = - warnMsg id 3026 "^ in place of ! in glob bracket expressions is" - - bashism t@(TA_Variable id str _) | isBashVariable str = - warnMsg id 3028 $ str ++ " is" - - bashism t@(T_DollarBraced id _ token) = do - unless isBusyboxSh $ mapM_ check simpleExpansions - mapM_ check advancedExpansions - when (isBashVariable var) $ - warnMsg id 3028 $ var ++ " is" - where - str = concat $ oversimplify token - var = getBracedReference str - check (regex, code, feature) = - when (isJust $ matchRegex regex str) $ warnMsg id code feature - - bashism t@(T_Pipe id "|&") = - warnMsg id 3029 "|& in place of 2>&1 | is" - bashism (T_Array id _) = - warnMsg id 3030 "arrays are" - bashism (T_IoFile id _ t) | isGlob t = - warnMsg id 3031 "redirecting to/from globs is" - bashism (T_CoProc id _ _) = - warnMsg id 3032 "coproc is" - - bashism (T_Function id _ _ str _) | not (isVariableName str) = - warnMsg id 3033 "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is" - - bashism (T_DollarExpansion id [x]) | isOnlyRedirection x = - warnMsg id 3034 "$( [(Id, String)] - getLiteralArgs = foldr go [] - where - go first rest = case getLiteralString first of - Just str -> (getId first, str) : rest - Nothing -> [] - - -- Check a flag-option pair (such as -o errexit) - checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest) - | flag' `matches` oFlagRegex = do - when (opt' `notElem` longOptions) $ - warnMsg oid 3040 $ "set option " <> opt' <> " is" - checkFlags (flag:rest) - | otherwise = checkFlags (flag:opt:rest) - checkOptions (flag:rest) = checkFlags (flag:rest) - checkOptions _ = return () - - -- Check that each option in a sequence of flags - -- (such as -aveo) is valid - checkFlags (flag@(fid, flag'):rest) - | startsOption flag' = do - unless (flag' `matches` validFlagsRegex) $ - forM_ (tail flag') $ \letter -> - when (letter `notElem` optionsSet) $ - warnMsg fid 3041 $ "set flag " <> ('-':letter:" is") - checkOptions rest - | beginsWithDoubleDash flag' = do - warnMsg fid 3042 $ "set flag " <> flag' <> " is" - checkOptions rest - -- Either a word that doesn't start with a dash, or simply '--', - -- so stop checking. - | otherwise = return () - checkFlags [] = return () - - options = "abCefhmnuvxo" - optionsSet = Set.fromList options - startsOption = (`matches` mkRegex "^(\\+|-[^-])") - oFlagRegex = mkRegex $ "^[-+][" <> options <> "]*o$" - validFlagsRegex = mkRegex $ "^[-+]([" <> options <> "]+o?|o)$" - beginsWithDoubleDash = (`matches` mkRegex "^--.+$") - longOptions = Set.fromList - [ "allexport", "errexit", "ignoreeof", "monitor", "noclobber" - , "noexec", "noglob", "nolog", "notify" , "nounset", "verbose" - , "vi", "xtrace" ] - - bashism t@(T_SimpleCommand id _ (cmd:rest)) = - let name = fromMaybe "" $ getCommandName t - flags = getLeadingFlags t - in do - when (name == "local" && not isDash) $ - -- This is so commonly accepted that we'll make it a special case - warnMsg id 3043 $ "'local' is" - when (name `elem` unsupportedCommands) $ - warnMsg id 3044 $ "'" ++ name ++ "' is" - sequence_ $ do - allowed' <- Map.lookup name allowedFlags - allowed <- allowed' - (word, flag) <- find - (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags - return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is" - - when (name == "source" && not isBusyboxSh) $ - warnMsg id 3046 "'source' in place of '.' is" - when (name == "trap") $ - let - check token = sequence_ $ do - str <- getLiteralString token - let upper = map toUpper str - return $ do - when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $ - warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is" - when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $ - warnMsg (getId token) 3048 - "prefixing signal names with 'SIG' is" - when (not isDash && upper /= str) $ - warnMsg (getId token) 3049 - "using lower/mixed case for signal names is" - in - mapM_ check (drop 1 rest) - - when (name == "printf") $ sequence_ $ do - format <- rest !!! 0 -- flags are covered by allowedFlags - let literal = onlyLiteralString format - guard $ "%q" `isInfixOf` literal - return $ warnMsg (getId format) 3050 "printf %q is" - - when (name == "read" && all isFlag rest) $ - warnMsg (getId cmd) 3061 "read without a variable is" - where - unsupportedCommands = [ - "let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown", - "enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", - "typeset" - ] - allowedFlags = Map.fromList [ - ("cd", Just ["L", "P"]), - ("exec", Just []), - ("export", Just ["p"]), - ("hash", Just $ if isDash then ["r", "v"] else ["r"]), - ("jobs", Just ["l", "p"]), - ("printf", Just []), - ("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]), - ("readonly", Just ["p"]), - ("trap", Just []), - ("type", Just $ if isBusyboxSh then ["p"] else []), - ("ulimit", if isDash then Nothing else Just ["f"]), - ("umask", Just ["S"]), - ("unset", Just ["f", "v"]), - ("wait", Just []) - ] - bashism t@(T_SourceCommand id src _) - | getCommandName src == Just "source" = - unless isBusyboxSh $ - warnMsg id 3051 "'source' in place of '.' is" - bashism (TA_Expansion _ (T_Literal id str : _)) - | str `matches` radix = warnMsg id 3052 "arithmetic base conversion is" - where - radix = mkRegex "^[0-9]+#" - bashism _ = return () - - varChars="_0-9a-zA-Z" - advancedExpansions = let re = mkRegex in [ - (re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"), - (re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"), - (re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"), - (re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"), - (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is") - ] - simpleExpansions = let re = mkRegex in [ - (re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"), - (re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"), - (re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is") - ] - bashVars = [ - -- This list deliberately excludes $BASH_VERSION as it's often used - -- for shell identification. - "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME", - "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS", - "_", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC", - "BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND", - "BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", - "BASH_SUBSHELL", "BASH_VERSINFO", "EPOCHREALTIME", "EPOCHSECONDS", - "FUNCNAME", "GROUPS", "MACHTYPE", "MAPFILE" - ] - bashDynamicVars = [ "RANDOM", "SECONDS" ] - dashVars = [ "_" ] - isBashVariable var = - (var `elem` bashDynamicVars - || var `elem` bashVars && not (isAssigned var)) - && not (isDash && var `elem` dashVars) - isAssigned var = any f (variableFlow params) - where - f x = case x of - Assignment (_, _, name, _) -> name == var - _ -> False - - checkTestOp table op id = sequence_ $ do - (code, shells, msg) <- Map.lookup op table - guard . not $ shellType params `elem` shells - return $ warnMsg id code (msg op) - - -buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list -bashismBinaryTestFlags = buildTestFlagMap [ - -- ([list of applicable flags], - -- (error code, exempt shells, message builder :: String -> String)), - -- - -- Distinct error codes allow the wiki to give more helpful, targeted - -- information. - (["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="], - (3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")), - (["=="], - (3014, [BusyboxSh], \op -> op ++ " in place of = is")), - (["=~"], - (3015, [], \op -> op ++ " regex matching is")), - - ([], (0,[],const "")) - ] -bashismUnaryTestFlags = buildTestFlagMap [ - (["-v"], - (3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")), - (["-a"], - (3017, [], \op -> "unary " ++ op ++ " in place of -e is")), - (["-o"], - (3062, [], \op -> "test " ++ op ++ " to check options is")), - (["-R"], - (3063, [], \op -> "test " ++ op ++ " and namerefs in general are")), - (["-N"], - (3064, [], \op -> "test " ++ op ++ " is")), - (["-k"], - (3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), - (["-G"], - (3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), - (["-O"], - (3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), - - ([], (0,[],const "")) - ] - - -prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" -prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")" -prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')" -prop_checkEchoSed2b = verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)" -checkEchoSed = ForShell [Bash, Ksh] f - where - f (T_Redirecting id lefts r) = - when (any redirectHereString lefts) $ - checkSed id rcmd - where - redirectHereString :: Token -> Bool - redirectHereString t = case t of - (T_FdRedirect _ _ T_HereString{}) -> True - _ -> False - rcmd = oversimplify r - - f (T_Pipeline id _ [a, b]) = - when (acmd == ["echo", "${VAR}"]) $ - checkSed id bcmd - where - acmd = oversimplify a - bcmd = oversimplify b - - f _ = return () - - checkSed id ["sed", v] = checkIn id v - checkSed id ["sed", "-e", v] = checkIn id v - checkSed _ _ = return () - - -- This should have used backreferences, but TDFA doesn't support them - sedRe = mkRegex "^s(.)([^\n]*)g?$" - isSimpleSed s = isJust $ do - [h:_,rest] <- matchRegex sedRe s - let delimiters = filter (== h) rest - guard $ length delimiters == 2 - checkIn id s = - when (isSimpleSed s) $ - style id 2001 "See if you can use ${variable//search/replace} instead." - - -prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}" -prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}" -prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg" -prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}" -checkBraceExpansionVars = ForShell [Bash] f - where - f t@(T_BraceExpansion id list) = mapM_ check list - where - check element = - when (any (`isInfixOf` toString element) ["$..", "..$"]) $ do - c <- isEvaled element - if c - then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval." - else warn id 2051 "Bash doesn't support variables in brace range expansions." - f _ = return () - - literalExt t = - case t of - T_DollarBraced {} -> return "$" - T_DollarExpansion {} -> return "$" - T_DollarArithmetic {} -> return "$" - _ -> return "-" - toString t = runIdentity $ getLiteralStringExt literalExt t - isEvaled t = do - cmd <- getClosestCommandM t - return $ maybe False (`isUnqualifiedCommand` "eval") cmd - - -prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3" -prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3" -prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )" -prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )" -prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}" -prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}" -checkMultiDimensionalArrays = ForShell [Bash] f - where - f token = - case token of - T_Assignment _ _ name (first:second:_) _ -> about second - T_IndexedElement _ (first:second:_) _ -> about second - T_DollarBraced _ _ l -> - when (isMultiDim l) $ about token - _ -> return () - about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays." - - re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well - isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re - -prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '" -prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" -prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '" -prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '" -prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '" -prop_checkPS14a = verify checkPS1Assignments "export PS1=$'\\e[3m; '" -prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '" -prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '" -prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'" -prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'" -checkPS1Assignments = ForShell [Bash] f - where - f token = case token of - (T_Assignment _ _ "PS1" _ word) -> warnFor word - _ -> return () - - warnFor word = - let contents = concat $ oversimplify word in - when (containsUnescaped contents) $ - info (getId word) 2025 "Make sure all escape sequences are enclosed in \\[..\\] to prevent line wrapping issues" - containsUnescaped s = - let unenclosed = subRegex enclosedRegex s "" in - isJust $ matchRegex escapeRegex unenclosed - enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager - escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" - - -prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true" -prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true" -checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f - where - f token = case token of - T_Banged id (T_Banged _ _) -> - err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1." - _ -> return () - - -prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true" -prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )" -prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true" -checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f - where - f token = case token of - T_Pipeline _ _ cmds -> mapM_ check cmds - _ -> return () - - check token = case token of - T_Banged id _ -> - err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary." - _ -> return () - - -prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]" -prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]" -prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]" -prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]" -prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]" -checkNegatedUnaryOps = ForShell [Bash] f - where - f token = case token of - TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] -> - err id 2332 $ msg op - _ -> return () - - msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]." - msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead." - msg _ = pleaseReport "unhandled negated unary message" - -return [] -runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs deleted file mode 100644 index 688d0d7..0000000 --- a/src/ShellCheck/Data.hs +++ /dev/null @@ -1,177 +0,0 @@ -module ShellCheck.Data where - -import ShellCheck.Interface -import Data.Version (showVersion) - - -{- -If you are here because you saw an error about Paths_ShellCheck in this file, -simply comment out the import below and define the version as a constant string. - -Instead of: - - import Paths_ShellCheck (version) - shellcheckVersion = showVersion version - -Use: - - -- import Paths_ShellCheck (version) - shellcheckVersion = "kludge" - --} - -import Paths_ShellCheck (version) -shellcheckVersion = showVersion version -- VERSIONSTRING - - -internalVariables = [ - -- Generic - "", "_", "rest", "REST", - - -- Bash - "BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC", - "BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND", - "BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH", - "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO", - "BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT", - "COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK", - "EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD", - "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD", - "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM", - "READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT", - "REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT", - "BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS", - "COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE", - "FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE", - "HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS", - "IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE", - "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", - "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", - "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1", - "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR", - "BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT", - "auto_resume", "histchars", - - -- Other - "USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY", - "HOSTNAME", "KRB5CCNAME", "XAUTHORITY" - - -- Ksh - , ".sh.version" - - -- shflags - , "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", - "FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", - "flags_error", "flags_return" - - -- Bats - ,"stderr", "stderr_lines" - ] - -specialIntegerVariables = [ - "$", "?", "!", "#" - ] - -specialVariablesWithoutSpaces = "-" : specialIntegerVariables - -variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [ - "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", - "EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM", - "READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS", - "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE", - "HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG" - - -- shflags - , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE" - ] - -specialVariables = specialVariablesWithoutSpaces ++ ["@", "*"] - -unbracedVariables = specialVariables ++ [ - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" - ] - -arrayVariables = [ - "BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO", - "BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC", - "DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY" - ] - -commonCommands = [ - "admin", "alias", "ar", "asa", "at", "awk", "basename", "batch", - "bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp", - "chmod", "chown", "cksum", "cmp", "colon", "comm", "command", - "compress", "continue", "cp", "crontab", "csplit", "ctags", "cut", - "cxref", "date", "dd", "delta", "df", "diff", "dirname", "dot", - "du", "echo", "ed", "env", "eval", "ex", "exec", "exit", "expand", - "export", "expr", "fc", "fg", "file", "find", "fold", "fort77", - "fuser", "gencat", "get", "getconf", "getopts", "grep", "hash", - "head", "iconv", "ipcrm", "ipcs", "jobs", "join", "kill", "lex", - "link", "ln", "locale", "localedef", "logger", "logname", "lp", - "ls", "m4", "mailx", "make", "man", "mesg", "mkdir", "mkfifo", - "more", "mv", "newgrp", "nice", "nl", "nm", "nohup", "od", "paste", - "patch", "pathchk", "pax", "pr", "printf", "prs", "ps", "pwd", - "qalter", "qdel", "qhold", "qmove", "qmsg", "qrerun", "qrls", - "qselect", "qsig", "qstat", "qsub", "read", "readonly", "renice", - "return", "rm", "rmdel", "rmdir", "sact", "sccs", "sed", "set", - "sh", "shift", "sleep", "sort", "split", "strings", "strip", "stty", - "tabs", "tail", "talk", "tee", "test", "time", "times", "touch", - "tput", "tr", "trap", "tsort", "tty", "type", "ulimit", "umask", - "unalias", "uname", "uncompress", "unexpand", "unget", "uniq", - "unlink", "unset", "uucp", "uudecode", "uuencode", "uustat", "uux", - "val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc", - "zcat" - ] - -nonReadingCommands = [ - "alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown", - "cp", "du", "echo", "export", "fg", "fuser", "getconf", - "getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", - "locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir", - "set", "sleep", "touch", "trap", "ulimit", "unalias", "uname" - ] - -sampleWords = [ - "alpha", "bravo", "charlie", "delta", "echo", "foxtrot", - "golf", "hotel", "india", "juliett", "kilo", "lima", "mike", - "november", "oscar", "papa", "quebec", "romeo", "sierra", - "tango", "uniform", "victor", "whiskey", "xray", "yankee", - "zulu" - ] - -binaryTestOps = [ - "-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", - "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>=" - ] - -arithmeticBinaryTestOps = [ - "-eq", "-ne", "-lt", "-le", "-gt", "-ge" - ] - -unaryTestOps = [ - "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", - "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", - "-o", "-v", "-R" - ] - -shellForExecutable :: String -> Maybe Shell -shellForExecutable name = - case name of - "sh" -> return Sh - "bash" -> return Bash - "bats" -> return Bash - "busybox" -> return BusyboxSh -- Used for directives and --shell=busybox - "busybox sh" -> return BusyboxSh - "busybox ash" -> return BusyboxSh - "dash" -> return Dash - "ash" -> return Dash -- There's also a warning for this. - "ksh" -> return Ksh - "ksh88" -> return Ksh - "ksh93" -> return Ksh - "oksh" -> return Ksh - _ -> Nothing - -flagsForRead = "sreu:n:N:i:p:a:t:" -flagsForMapfile = "d:n:O:s:u:C:c:t" - -declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"] diff --git a/src/ShellCheck/Debug.hs b/src/ShellCheck/Debug.hs deleted file mode 100644 index b6015e5..0000000 --- a/src/ShellCheck/Debug.hs +++ /dev/null @@ -1,313 +0,0 @@ -{- - -This file contains useful functions for debugging and developing ShellCheck. - -To invoke them interactively, run: - - cabal repl - -At the ghci prompt, enter: - - :load ShellCheck.Debug - -You can now invoke the functions. Here are some examples: - - shellcheckString "echo $1" - stringToAst "(( x+1 ))" - stringToCfg "if foo; then bar; else baz; fi" - writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done" - -The latter file can be rendered to png with GraphViz: - - dot -Tpng /tmp/test.dot > /tmp/test.png - -To run all unit tests in a module: - - ShellCheck.Parser.runTests - ShellCheck.Analytics.runTests - -To run a specific test: - - :load ShellCheck.Analytics - prop_checkUuoc3 - -If you make code changes, reload in seconds at any time with: - - :r - -=========================================================================== - -Crash course in printf debugging in Haskell: - - import Debug.Trace - - greet 0 = return () - -- Print when a function is invoked - greet n | trace ("calling greet " ++ show n) False = undefined - greet n = do - putStrLn "Enter name" - name <- getLine - -- Print at some point in any monadic function - traceM $ "user entered " ++ name - putStrLn $ "Hello " ++ name - -- Print a value before passing it on - greet $ traceShowId (n - 1) - - -=========================================================================== - -If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to -debug all of ShellCheck including I/O, you may see an error like this: - - src/ShellCheck/Data.hs:5:1: error: - Could not load module ‘Paths_ShellCheck’ - it is a hidden module in the package ‘ShellCheck-0.8.0’ - -This can easily be circumvented by running `./setgitversion` or manually -editing src/ShellCheck/Data.hs to replace the auto-deduced version number -with a constant string as indicated. - -Afterwards, you can run the ShellCheck tool, as if from the shell, with: - - $ ghci shellcheck.hs - ghci> runMain ["-x", "file.sh"] - --} - -module ShellCheck.Debug () where - -import ShellCheck.Analyzer -import ShellCheck.AST -import ShellCheck.CFG -import ShellCheck.Checker -import ShellCheck.CFGAnalysis as CF -import ShellCheck.Interface -import ShellCheck.Parser -import ShellCheck.Prelude - -import Control.Monad -import Control.Monad.Identity -import Control.Monad.RWS -import Control.Monad.Writer -import Data.Graph.Inductive.Graph as G -import Data.List -import Data.Maybe -import qualified Data.Map as M -import qualified Data.Set as S - - --- Run all of ShellCheck (minus output formatters) -shellcheckString :: String -> CheckResult -shellcheckString scriptString = - runIdentity $ checkScript dummySystemInterface checkSpec - where - checkSpec :: CheckSpec - checkSpec = emptyCheckSpec { - csScript = scriptString - } - -dummySystemInterface :: SystemInterface Identity -dummySystemInterface = mockedSystemInterface [ - -- A tiny, fake filesystem for sourced files - ("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"), - ("lib/mylib2.sh", "bar=42") - ] - --- Parameters used when generating Control Flow Graphs -cfgParams :: CFGParameters -cfgParams = CFGParameters { - cfLastpipe = False, - cfPipefail = False -} - --- An example script to play with -exampleScript :: String -exampleScript = unlines [ - "#!/bin/sh", - "count=0", - "for file in *", - "do", - " (( count++ ))", - "done", - "echo $count" - ] - --- Parse the script string into ShellCheck's ParseResult -parseScriptString :: String -> ParseResult -parseScriptString scriptString = - runIdentity $ parseScript dummySystemInterface parseSpec - where - parseSpec :: ParseSpec - parseSpec = newParseSpec { - psFilename = "myscript", - psScript = scriptString - } - - --- Parse the script string into an Abstract Syntax Tree -stringToAst :: String -> Token -stringToAst scriptString = - case maybeRoot of - Just root -> root - Nothing -> error $ "Script failed to parse: " ++ show parserWarnings - where - parseResult :: ParseResult - parseResult = parseScriptString scriptString - - maybeRoot :: Maybe Token - maybeRoot = prRoot parseResult - - parserWarnings :: [PositionedComment] - parserWarnings = prComments parseResult - - -astToCfgResult :: Token -> CFGResult -astToCfgResult = buildGraph cfgParams - -astToDfa :: Token -> CFGAnalysis -astToDfa = analyzeControlFlow cfgParams - -astToCfg :: Token -> CFGraph -astToCfg = cfGraph . astToCfgResult - -stringToCfg :: String -> CFGraph -stringToCfg = astToCfg . stringToAst - -stringToDfa :: String -> CFGAnalysis -stringToDfa = astToDfa . stringToAst - -cfgToGraphViz :: CFGraph -> String -cfgToGraphViz = cfgToGraphVizWith show - -stringToCfgViz :: String -> String -stringToCfgViz = cfgToGraphViz . stringToCfg - -stringToDfaViz :: String -> String -stringToDfaViz = dfaToGraphViz . stringToDfa - --- Dump a Control Flow Graph as GraphViz with extended information -stringToDetailedCfgViz :: String -> String -stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph - where - ast :: Token - ast = stringToAst scriptString - - cfgResult :: CFGResult - cfgResult = astToCfgResult ast - - graph :: CFGraph - graph = cfGraph cfgResult - - idToToken :: M.Map Id Token - idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast - - idToNode :: M.Map Id (Node, Node) - idToNode = cfIdToRange cfgResult - - nodeToStartIds :: M.Map Node (S.Set Id) - nodeToStartIds = - M.fromListWith S.union $ - map (\(id, (start, _)) -> (start, S.singleton id)) $ - M.toList idToNode - - nodeToEndIds :: M.Map Node (S.Set Id) - nodeToEndIds = - M.fromListWith S.union $ - map (\(id, (_, end)) -> (end, S.singleton id)) $ - M.toList idToNode - - formatId :: Id -> String - formatId id = fromMaybe ("Unknown " ++ show id) $ do - (OuterToken _ token) <- M.lookup id idToToken - firstWord <- words (show token) !!! 0 - -- Strip off "Inner_" - (_ : tokenName) <- return $ dropWhile (/= '_') firstWord - return $ tokenName ++ " " ++ show id - - formatGroup :: S.Set Id -> String - formatGroup set = intercalate ", " $ map formatId $ S.toList set - - nodeLabel (node, label) = unlines [ - show node ++ ". " ++ show label, - "Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds), - "End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds) - ] - - --- Dump a Control Flow Graph with Data Flow Analysis as GraphViz -dfaToGraphViz :: CF.CFGAnalysis -> String -dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis - where - label (node, label) = - let - desc = show node ++ ". " ++ show label - in - fromMaybe ("No DFA available\n\n" ++ desc) $ do - (pre, post) <- M.lookup node $ CF.nodeToData analysis - return $ unlines [ - "Precondition: " ++ show pre, - "", - desc, - "", - "Postcondition: " ++ show post - ] - - --- Dump an Control Flow Graph to GraphViz with a given node formatter -cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String -cfgToGraphVizWith nodeLabel graph = concat [ - "digraph {\n", - concatMap dumpNode (labNodes graph), - concatMap dumpLink (labEdges graph), - tagVizEntries graph, - "}\n" - ] - where - dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n" - dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ) ++ "]\n" - edgeStyle CFEFlow = "solid" - edgeStyle CFEExit = "bold" - edgeStyle CFEFalseFlow = "dotted" - -quoteViz str = "\"" ++ escapeViz str ++ "\"" -escapeViz [] = [] -escapeViz (c:rest) = - case c of - '\"' -> '\\' : '\"' : escapeViz rest - '\n' -> '\\' : 'l' : escapeViz rest - '\\' -> '\\' : '\\' : escapeViz rest - _ -> c : escapeViz rest - - --- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format -astToGraphViz :: Token -> String -astToGraphViz token = concat [ - "digraph {\n", - formatTree token, - "}\n" - ] - where - formatTree :: Token -> String - formatTree t = snd $ execRWS (doStackAnalysis push pop t) () [] - - push :: Token -> RWS () String [Int] () - push (OuterToken (Id n) inner) = do - stack <- get - put (n : stack) - case stack of - [] -> return () - (top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n" - tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n" - - pop :: Token -> RWS () String [Int] () - pop _ = modify tail - - --- For each entry point, set the rank so that they'll align in the graph -tagVizEntries :: CFGraph -> String -tagVizEntries graph = "{ rank=same " ++ rank ++ " }" - where - entries = mapMaybe find $ labNodes graph - find (node, CFEntryPoint name) = return (node, name) - find _ = Nothing - rank = unwords $ map (\(c, _) -> show c) entries diff --git a/src/ShellCheck/Fixer.hs b/src/ShellCheck/Fixer.hs deleted file mode 100644 index 0d3c8f4..0000000 --- a/src/ShellCheck/Fixer.hs +++ /dev/null @@ -1,412 +0,0 @@ -{- - Copyright 2018-2019 Vidar Holen, Ng Zhi An - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} - -{-# LANGUAGE TemplateHaskell #-} -module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where - -import ShellCheck.Interface -import ShellCheck.Prelude -import Control.Monad -import Control.Monad.State -import Data.Array -import Data.List -import Data.Semigroup -import GHC.Exts (sortWith) -import Test.QuickCheck - --- The Ranged class is used for types that has a start and end position. -class Ranged a where - start :: a -> Position - end :: a -> Position - overlap :: a -> a -> Bool - overlap x y = - xEnd > yStart && yEnd > xStart - where - yStart = start y - yEnd = end y - xStart = start x - xEnd = end x - -- Set a new start and end position on a Ranged - setRange :: (Position, Position) -> a -> a - --- Tests auto-verify that overlap commutes -assertOverlap x y = overlap x y && overlap y x -assertNoOverlap x y = not (overlap x y) && not (overlap y x) - -prop_overlap_contiguous = assertNoOverlap - (tFromStart 10 12 "foo" 1) - (tFromStart 12 14 "bar" 2) - -prop_overlap_adjacent_zerowidth = assertNoOverlap - (tFromStart 3 3 "foo" 1) - (tFromStart 3 3 "bar" 2) - -prop_overlap_enclosed = assertOverlap - (tFromStart 3 5 "foo" 1) - (tFromStart 1 10 "bar" 2) - -prop_overlap_partial = assertOverlap - (tFromStart 1 5 "foo" 1) - (tFromStart 3 7 "bar" 2) - - -instance Ranged PositionedComment where - start = pcStartPos - end = pcEndPos - setRange (s, e) pc = pc { - pcStartPos = s, - pcEndPos = e - } - -instance Ranged Replacement where - start = repStartPos - end = repEndPos - setRange (s, e) r = r { - repStartPos = s, - repEndPos = e - } - --- The Monoid instance for Fix merges fixes that do not conflict. --- TODO: Make an efficient 'mconcat' -instance Monoid Fix where - mempty = newFix - mappend = (<>) - mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap - -instance Semigroup Fix where - f1 <> f2 = - -- FIXME: This might need to also discard adjacent zero-width ranges for - -- when two fixes change the same AST node, e.g. `foo` -> "$(foo)" - if or [ r2 `overlap` r1 | r1 <- fixReplacements f1, r2 <- fixReplacements f2 ] - then f1 - else newFix { - fixReplacements = fixReplacements f1 ++ fixReplacements f2 - } - --- Conveniently apply a transformation to positions in a Fix -mapPositions :: (Position -> Position) -> Fix -> Fix -mapPositions f = adjustFix - where - adjustReplacement rep = - rep { - repStartPos = f $ repStartPos rep, - repEndPos = f $ repEndPos rep - } - adjustFix fix = - fix { - fixReplacements = map adjustReplacement $ fixReplacements fix - } - --- Rewrite a Ranged from a tabstop of 8 to 1 -removeTabStops :: Ranged a => a -> Array Int String -> a -removeTabStops range ls = - let startColumn = realignColumn lineNo colNo range - endColumn = realignColumn endLineNo endColNo range - startPosition = (start range) { posColumn = startColumn } - endPosition = (end range) { posColumn = endColumn } in - setRange (startPosition, endPosition) range - where - realignColumn lineNo colNo c = - if lineNo c > 0 && lineNo c <= fromIntegral (length ls) - then real (ls ! fromIntegral (lineNo c)) 0 0 (colNo c) - else colNo c - real _ r v target | target <= v = r - -- hit this case at the end of line, and if we don't hit the target - -- return real + (target - v) - real [] r v target = r + (target - v) - real ('\t':rest) r v target = real rest (r+1) (v + 8 - (v `mod` 8)) target - real (_:rest) r v target = real rest (r+1) (v+1) target - lineNo = posLine . start - endLineNo = posLine . end - colNo = posColumn . start - endColNo = posColumn . end - - --- A replacement that spans multiple line is applied by: --- 1. merging the affected lines into a single string using `unlines` --- 2. apply the replacement as if it only spanned a single line --- The tricky part is adjusting the end column of the replacement --- (the end line doesn't matter because there is only one line) --- --- aaS <--- start of replacement (row 1 column 3) --- bbbb --- cEc --- \------- end of replacement (row 3 column 2) --- --- a flattened string will look like: --- --- "aaS\nbbbb\ncEc\n" --- --- The column of E has to be adjusted by: --- 1. lengths of lines to be replaced, except the end row itself --- 2. end column of the replacement --- 3. number of '\n' by `unlines` -multiToSingleLine :: [Fix] -> Array Int String -> ([Fix], String) -multiToSingleLine fixes lines = - (map (mapPositions adjust) fixes, unlines $ elems lines) - where - -- A prefix sum tree from line number to column shift. - -- FIXME: The tree will be totally unbalanced. - shiftTree :: PSTree Int - shiftTree = - foldl (\t (n,s) -> addPSValue (n+1) (length s + 1) t) newPSTree $ - assocs lines - singleString = unlines $ elems lines - adjust pos = - pos { - posLine = 1, - posColumn = (posColumn pos) + - (fromIntegral $ getPrefixSum (fromIntegral $ posLine pos) shiftTree) - } - --- Apply a fix and return resulting lines. --- The number of lines can increase or decrease with no obvious mapping back, so --- the function does not return an array. -applyFix :: Fix -> Array Int String -> [String] -applyFix fix fileLines = - let - untabbed = fix { - fixReplacements = - map (\c -> removeTabStops c fileLines) $ - fixReplacements fix - } - (adjustedFixes, singleLine) = multiToSingleLine [untabbed] fileLines - in - lines . runFixer $ applyFixes2 adjustedFixes singleLine - - --- start and end comes from pos, which is 1 based -prop_doReplace1 = doReplace 0 0 "1234" "A" == "A1234" -- technically not valid -prop_doReplace2 = doReplace 1 1 "1234" "A" == "A1234" -prop_doReplace3 = doReplace 1 2 "1234" "A" == "A234" -prop_doReplace4 = doReplace 3 3 "1234" "A" == "12A34" -prop_doReplace5 = doReplace 4 4 "1234" "A" == "123A4" -prop_doReplace6 = doReplace 5 5 "1234" "A" == "1234A" -doReplace start end o r = - let si = fromIntegral (start-1) - ei = fromIntegral (end-1) - (x, xs) = splitAt si o - z = drop (ei - si) xs - in - x ++ r ++ z - --- Fail if the 'expected' string is not result when applying 'fixes' to 'original'. -testFixes :: String -> String -> [Fix] -> Bool -testFixes expected original fixes = - actual == expected - where - actual = runFixer (applyFixes2 fixes original) - - --- A Fixer allows doing repeated modifications of a string where each --- replacement automatically accounts for shifts from previous ones. -type Fixer a = State (PSTree Int) a - --- Apply a single replacement using its indices into the original string. --- It does not handle multiple lines, all line indices must be 1. -applyReplacement2 :: Replacement -> String -> Fixer String -applyReplacement2 rep string = do - tree <- get - let transform pos = pos + getPrefixSum pos tree - let originalPos = (repStartPos rep, repEndPos rep) - (oldStart, oldEnd) = tmap (fromInteger . posColumn) originalPos - (newStart, newEnd) = tmap transform (oldStart, oldEnd) - - let (l1, l2) = tmap posLine originalPos in - when (l1 /= 1 || l2 /= 1) $ - error $ pleaseReport "bad cross-line fix" - - let replacer = repString rep - let shift = (length replacer) - (oldEnd - oldStart) - let insertionPoint = - case repInsertionPoint rep of - InsertBefore -> oldStart - InsertAfter -> oldEnd+1 - put $ addPSValue insertionPoint shift tree - - return $ doReplace newStart newEnd string replacer - where - tmap f (a,b) = (f a, f b) - --- Apply a list of Replacements in the correct order -applyReplacements2 :: [Replacement] -> String -> Fixer String -applyReplacements2 reps str = - foldM (flip applyReplacement2) str $ - reverse $ sortWith repPrecedence reps - --- Apply all fixes with replacements in the correct order -applyFixes2 :: [Fix] -> String -> Fixer String -applyFixes2 fixes = applyReplacements2 (concatMap fixReplacements fixes) - --- Get the final value of a Fixer. -runFixer :: Fixer a -> a -runFixer f = evalState f newPSTree - - - --- A Prefix Sum Tree that lets you look up the sum of values at and below an index. --- It's implemented essentially as a Fenwick tree without the bit-based balancing. --- The last Num is the sum of the left branch plus current element. -data PSTree n = PSBranch n (PSTree n) (PSTree n) n | PSLeaf - deriving (Show) - -newPSTree :: Num n => PSTree n -newPSTree = PSLeaf - --- Get the sum of values whose keys are <= 'target' -getPrefixSum :: (Ord n, Num n) => n -> PSTree n -> n -getPrefixSum = f 0 - where - f sum _ PSLeaf = sum - f sum target (PSBranch pivot left right cumulative) = - case target `compare` pivot of - LT -> f sum target left - GT -> f (sum+cumulative) target right - EQ -> sum+cumulative - --- Add a value to the Prefix Sum tree at the given index. --- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5 -addPSValue :: (Ord n, Num n) => n -> n -> PSTree n -> PSTree n -addPSValue key value tree = if value == 0 then tree else f tree - where - f PSLeaf = PSBranch key PSLeaf PSLeaf value - f (PSBranch pivot left right sum) = - case key `compare` pivot of - LT -> PSBranch pivot (f left) right (sum + value) - GT -> PSBranch pivot left (f right) sum - EQ -> PSBranch pivot left right (sum + value) - -prop_pstreeSumsCorrectly kvs targets = - let - -- Trivial O(n * m) implementation - dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int] - dumbPrefixSums kvs targets = - let prefixSum target = sum [v | (k,v) <- kvs, k <= target] - in map prefixSum targets - -- PSTree O(n * log m) implementation - smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int] - smartPrefixSums kvs targets = - let tree = foldl (\tree (pos, shift) -> addPSValue pos shift tree) PSLeaf kvs - in map (\x -> getPrefixSum x tree) targets - in smartPrefixSums kvs targets == dumbPrefixSums kvs targets - - --- Semi-convenient functions for constructing tests. -testFix :: [Replacement] -> Fix -testFix list = newFix { - fixReplacements = list - } - -tFromStart :: Int -> Int -> String -> Int -> Replacement -tFromStart start end repl order = - newReplacement { - repStartPos = newPosition { - posLine = 1, - posColumn = fromIntegral start - }, - repEndPos = newPosition { - posLine = 1, - posColumn = fromIntegral end - }, - repString = repl, - repPrecedence = order, - repInsertionPoint = InsertAfter - } - -tFromEnd start end repl order = - (tFromStart start end repl order) { - repInsertionPoint = InsertBefore - } - -prop_simpleFix1 = testFixes "hello world" "hell world" [ - testFix [ - tFromEnd 5 5 "o" 1 - ]] - -prop_anchorsLeft = testFixes "-->foobar<--" "--><--" [ - testFix [ - tFromStart 4 4 "foo" 1, - tFromStart 4 4 "bar" 2 - ]] - -prop_anchorsRight = testFixes "-->foobar<--" "--><--" [ - testFix [ - tFromEnd 4 4 "bar" 1, - tFromEnd 4 4 "foo" 2 - ]] - -prop_anchorsBoth1 = testFixes "-->foobar<--" "--><--" [ - testFix [ - tFromStart 4 4 "bar" 2, - tFromEnd 4 4 "foo" 1 - ]] - -prop_anchorsBoth2 = testFixes "-->foobar<--" "--><--" [ - testFix [ - tFromEnd 4 4 "foo" 2, - tFromStart 4 4 "bar" 1 - ]] - -prop_composeFixes1 = testFixes "cd \"$1\" || exit" "cd $1" [ - testFix [ - tFromStart 4 4 "\"" 10, - tFromEnd 6 6 "\"" 10 - ], - testFix [ - tFromEnd 6 6 " || exit" 5 - ]] - -prop_composeFixes2 = testFixes "$(\"$1\")" "`$1`" [ - testFix [ - tFromStart 1 2 "$(" 5, - tFromEnd 4 5 ")" 5 - ], - testFix [ - tFromStart 2 2 "\"" 10, - tFromEnd 4 4 "\"" 10 - ]] - -prop_composeFixes3 = testFixes "(x)[x]" "xx" [ - testFix [ - tFromStart 1 1 "(" 4, - tFromEnd 2 2 ")" 3, - tFromStart 2 2 "[" 2, - tFromEnd 3 3 "]" 1 - ]] - -prop_composeFixes4 = testFixes "(x)[x]" "xx" [ - testFix [ - tFromStart 1 1 "(" 4, - tFromStart 2 2 "[" 3, - tFromEnd 2 2 ")" 2, - tFromEnd 3 3 "]" 1 - ]] - -prop_composeFixes5 = testFixes "\"$(x)\"" "`x`" [ - testFix [ - tFromStart 1 2 "$(" 2, - tFromEnd 3 4 ")" 2, - tFromStart 1 1 "\"" 1, - tFromEnd 4 4 "\"" 1 - ]] - - -return [] -runTests = $quickCheckAll diff --git a/src/ShellCheck/Formatter/CheckStyle.hs b/src/ShellCheck/Formatter/CheckStyle.hs deleted file mode 100644 index 3f898c3..0000000 --- a/src/ShellCheck/Formatter/CheckStyle.hs +++ /dev/null @@ -1,95 +0,0 @@ -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -module ShellCheck.Formatter.CheckStyle (format) where - -import ShellCheck.Interface -import ShellCheck.Formatter.Format - -import Data.Char -import Data.List -import System.IO -import qualified Data.List.NonEmpty as NE - -format :: IO Formatter -format = return Formatter { - header = do - putStrLn "" - putStrLn "", - - onFailure = outputError, - onResult = outputResults, - - footer = putStrLn "" -} - -outputResults cr sys = - if null comments - then outputFile (crFilename cr) "" [] - else mapM_ outputGroup fileGroups - where - comments = crComments cr - fileGroups = NE.groupWith sourceFile comments - outputGroup group = do - let filename = sourceFile (NE.head group) - result <- siReadFile sys (Just True) filename - let contents = either (const "") id result - outputFile filename contents (NE.toList group) - -outputFile filename contents warnings = do - let comments = makeNonVirtual warnings contents - putStrLn . formatFile filename $ comments - -formatFile name comments = concat [ - "\n", - concatMap formatComment comments, - "" - ] - -formatComment c = concat [ - "\n" - ] - -outputError file error = putStrLn $ concat [ - "\n", - "\n", - "" - ] - - -attr s v = concat [ s, "='", escape v, "' " ] -escape = concatMap escape' -escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";" -isOk x = any ($ x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")] - -severity "error" = "error" -severity "warning" = "warning" -severity _ = "info" diff --git a/src/ShellCheck/Formatter/Diff.hs b/src/ShellCheck/Formatter/Diff.hs deleted file mode 100644 index c00da1a..0000000 --- a/src/ShellCheck/Formatter/Diff.hs +++ /dev/null @@ -1,260 +0,0 @@ -{- - Copyright 2019 Vidar 'koala_man' Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -module ShellCheck.Formatter.Diff (format, ShellCheck.Formatter.Diff.runTests) where - -import ShellCheck.Interface -import ShellCheck.Fixer -import ShellCheck.Formatter.Format - -import Control.Monad -import Data.Algorithm.Diff -import Data.Array -import Data.IORef -import Data.List -import qualified Data.Monoid as Monoid -import Data.Maybe -import qualified Data.Map as M -import GHC.Exts (sortWith) -import System.IO -import System.FilePath - -import Test.QuickCheck - -format :: FormatterOptions -> IO Formatter -format options = do - foundIssues <- newIORef False - reportedIssues <- newIORef False - shouldColor <- shouldOutputColor (foColorOption options) - let color = if shouldColor then colorize else nocolor - return Formatter { - header = return (), - footer = checkFooter foundIssues reportedIssues color, - onFailure = reportFailure color, - onResult = reportResult foundIssues reportedIssues color - } - - -contextSize = 3 -red = 31 -green = 32 -yellow = 33 -cyan = 36 -bold = 1 - -nocolor n = id -colorize n s = (ansi n) ++ s ++ (ansi 0) -ansi n = "\x1B[" ++ show n ++ "m" - -printErr :: ColorFunc -> String -> IO () -printErr color = hPutStrLn stderr . color bold . color red -reportFailure color file msg = printErr color $ file ++ ": " ++ msg - -checkFooter foundIssues reportedIssues color = do - found <- readIORef foundIssues - output <- readIORef reportedIssues - when (found && not output) $ - printErr color "Issues were detected, but none were auto-fixable. Use another format to see them." - -type ColorFunc = (Int -> String -> String) -data LFStatus = LinefeedMissing | LinefeedOk -data DiffDoc a = DiffDoc String LFStatus [DiffRegion a] -data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a] - -reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO () -reportResult foundIssues reportedIssues color result sys = do - let comments = crComments result - unless (null comments) $ writeIORef foundIssues True - let suggestedFixes = mapMaybe pcFix comments - let fixmap = buildFixMap suggestedFixes - mapM_ output $ M.toList fixmap - where - output (name, fix) = do - file <- siReadFile sys (Just True) name - case file of - Right contents -> do - putStrLn $ formatDoc color $ makeDiff name contents fix - writeIORef reportedIssues True - Left msg -> reportFailure color name msg - -hasTrailingLinefeed str = - case str of - [] -> True - _ -> last str == '\n' - -coversLastLine regions = - case regions of - [] -> False - _ -> (fst $ last regions) - --- TODO: Factor this out into a unified diff library because we're doing a lot --- of the heavy lifting anyways. -makeDiff :: String -> String -> Fix -> DiffDoc String -makeDiff name contents fix = do - let hunks = groupDiff $ computeDiff contents fix - let lf = if coversLastLine hunks && not (hasTrailingLinefeed contents) - then LinefeedMissing - else LinefeedOk - DiffDoc name lf $ findRegions hunks - -computeDiff :: String -> Fix -> [Diff String] -computeDiff contents fix = - let old = lines contents - array = listArray (1, fromIntegral $ (length old)) old - new = applyFix fix array - in getDiff old new - --- Group changes into hunks -groupDiff :: [Diff a] -> [(Bool, [Diff a])] -groupDiff = filter (\(_, l) -> not (null l)) . hunt [] - where - -- Churn through 'Both's until we find a difference - hunt current [] = [(False, reverse current)] - hunt current (x@Both {}:rest) = hunt (x:current) rest - hunt current list = - let (context, previous) = splitAt contextSize current - in (False, reverse previous) : gather context 0 list - - -- Pick out differences until we find a run of Both's - gather current n [] = - let (extras, patch) = splitAt (max 0 $ n - contextSize) current - in [(True, reverse patch), (False, reverse extras)] - - gather current n list@(Both {}:_) | n == contextSize*2 = - let (context, previous) = splitAt contextSize current - in (True, reverse previous) : hunt context list - - gather current n (x@Both {}:rest) = gather (x:current) (n+1) rest - gather current n (x:rest) = gather (x:current) 0 rest - --- Get line numbers for hunks -findRegions :: [(Bool, [Diff String])] -> [DiffRegion String] -findRegions = find' 1 1 - where - find' _ _ [] = [] - find' left right ((output, run):rest) = - let (dl, dr) = countDelta run - remainder = find' (left+dl) (right+dr) rest - in - if output - then DiffRegion (left, dl) (right, dr) run : remainder - else remainder - --- Get left/right line counts for a hunk -countDelta :: [Diff a] -> (Int, Int) -countDelta = count' 0 0 - where - count' left right [] = (left, right) - count' left right (x:rest) = - case x of - Both {} -> count' (left+1) (right+1) rest - First {} -> count' (left+1) right rest - Second {} -> count' left (right+1) rest - -formatRegion :: ColorFunc -> LFStatus -> DiffRegion String -> String -formatRegion color lf (DiffRegion left right diffs) = - let header = color cyan ("@@ -" ++ (tup left) ++ " +" ++ (tup right) ++" @@") - in - unlines $ header : reverse (getStrings lf (reverse diffs)) - where - noLF = "\\ No newline at end of file" - - getStrings LinefeedOk list = map format list - getStrings LinefeedMissing list@((Both _ _):_) = noLF : map format list - getStrings LinefeedMissing list@((First _):_) = noLF : map format list - getStrings LinefeedMissing (last:rest) = format last : getStrings LinefeedMissing rest - - tup (a,b) = (show a) ++ "," ++ (show b) - format (Both x _) = ' ':x - format (First x) = color red $ '-':x - format (Second x) = color green $ '+':x - -splitLast [] = ([], []) -splitLast x = - let (last, rest) = splitAt 1 $ reverse x - in (reverse rest, last) - --- git patch does not like `\` on Windows -normalizePath path = - case path of - c:rest -> (if c == pathSeparator then '/' else c) : normalizePath rest - [] -> [] - -formatDoc color (DiffDoc name lf regions) = - let (most, last) = splitLast regions - in - (color bold $ "--- " ++ (normalizePath $ "a" name)) ++ "\n" ++ - (color bold $ "+++ " ++ (normalizePath $ "b" name)) ++ "\n" ++ - concatMap (formatRegion color LinefeedOk) most ++ - concatMap (formatRegion color lf) last - --- Create a Map from filename to Fix -buildFixMap :: [Fix] -> M.Map String Fix -buildFixMap fixes = perFile - where - splitFixes = splitFixByFile $ mconcat fixes - perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes - -splitFixByFile :: Fix -> [Fix] -splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix) - where - sameFile rep1 rep2 = (posFile $ repStartPos rep1) == (posFile $ repStartPos rep2) - makeFix reps = newFix { fixReplacements = reps } - -groupByMap :: (Ord k, Monoid v) => (v -> k) -> [v] -> M.Map k v -groupByMap f = M.fromListWith Monoid.mappend . map (\x -> (f x, x)) - --- For building unit tests -b n = Both n n -l = First -r = Second - -prop_identifiesProperContext = groupDiff [b 1, b 2, b 3, b 4, l 5, b 6, b 7, b 8, b 9] == - [(False, [b 1]), -- Omitted - (True, [b 2, b 3, b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context - (False, [b 9])] -- Omitted - -prop_includesContextFromStartIfNecessary = groupDiff [b 4, l 5, b 6, b 7, b 8, b 9] == - [ -- Nothing omitted - (True, [b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context - (False, [b 9])] -- Omitted - -prop_includesContextUntilEndIfNecessary = groupDiff [b 4, l 5] == - [ -- Nothing omitted - (True, [b 4, l 5]) - ] -- Nothing Omitted - -prop_splitsIntoMultipleHunks = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, b 7, r 8] == - [ -- Nothing omitted - (True, [l 1, b 1, b 2, b 3]), - (False, [b 4]), - (True, [b 5, b 6, b 7, r 8]) - ] -- Nothing Omitted - -prop_splitsIntoMultipleHunksUnlessTouching = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7] == - [ - (True, [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7]) - ] - -prop_countDeltasWorks = countDelta [b 1, l 2, r 3, r 4, b 5] == (3,4) -prop_countDeltasWorks2 = countDelta [] == (0,0) - -return [] -runTests = $quickCheckAll diff --git a/src/ShellCheck/Formatter/Format.hs b/src/ShellCheck/Formatter/Format.hs deleted file mode 100644 index 53b59a4..0000000 --- a/src/ShellCheck/Formatter/Format.hs +++ /dev/null @@ -1,82 +0,0 @@ -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -module ShellCheck.Formatter.Format where - -import ShellCheck.Data -import ShellCheck.Interface -import ShellCheck.Fixer - -import Control.Monad -import Data.Array -import Data.List -import System.IO -import System.Info -import System.Environment - --- A formatter that carries along an arbitrary piece of data -data Formatter = Formatter { - header :: IO (), - onResult :: CheckResult -> SystemInterface IO -> IO (), - onFailure :: FilePath -> ErrorMessage -> IO (), - footer :: IO () -} - -sourceFile = posFile . pcStartPos -lineNo = posLine . pcStartPos -endLineNo = posLine . pcEndPos -colNo = posColumn . pcStartPos -endColNo = posColumn . pcEndPos -codeNo = cCode . pcComment -messageText = cMessage . pcComment - -severityText :: PositionedComment -> String -severityText pc = - case cSeverity (pcComment pc) of - ErrorC -> "error" - WarningC -> "warning" - InfoC -> "info" - StyleC -> "style" - --- Realign comments from a tabstop of 8 to 1 -makeNonVirtual comments contents = - map fix comments - where - list = lines contents - arr = listArray (1, length list) list - untabbedFix f = newFix { - fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f) - } - fix c = (removeTabStops c arr) { - pcFix = fmap untabbedFix (pcFix c) - } - - -shouldOutputColor :: ColorOption -> IO Bool -shouldOutputColor colorOption = - case colorOption of - ColorAlways -> return True - ColorNever -> return False - ColorAuto -> do - isTerminal <- hIsTerminalDevice stdout - term <- lookupEnv "TERM" - let windows = "mingw" `isPrefixOf` os - let dumbTerm = term `elem` [Just "dumb", Just "", Nothing] - let isUsableTty = isTerminal && not windows && not dumbTerm - return isUsableTty diff --git a/src/ShellCheck/Formatter/GCC.hs b/src/ShellCheck/Formatter/GCC.hs deleted file mode 100644 index b921753..0000000 --- a/src/ShellCheck/Formatter/GCC.hs +++ /dev/null @@ -1,65 +0,0 @@ -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -module ShellCheck.Formatter.GCC (format) where - -import ShellCheck.Interface -import ShellCheck.Formatter.Format - -import Data.List -import System.IO -import qualified Data.List.NonEmpty as NE - -format :: IO Formatter -format = return Formatter { - header = return (), - footer = return (), - onFailure = outputError, - onResult = outputAll -} - -outputError file error = hPutStrLn stderr $ file ++ ": " ++ error - -outputAll cr sys = mapM_ f groups - where - comments = crComments cr - groups = NE.groupWith sourceFile comments - f :: NE.NonEmpty PositionedComment -> IO () - f group = do - let filename = sourceFile (NE.head group) - result <- siReadFile sys (Just True) filename - let contents = either (const "") id result - outputResult filename contents (NE.toList group) - -outputResult filename contents warnings = do - let comments = makeNonVirtual warnings contents - mapM_ (putStrLn . formatComment filename) comments - -formatComment filename c = concat [ - filename, ":", - show $ lineNo c, ":", - show $ colNo c, ": ", - case severityText c of - "error" -> "error" - "warning" -> "warning" - _ -> "note", - ": ", - concat . lines $ messageText c, - " [SC", show $ codeNo c, "]" - ] diff --git a/src/ShellCheck/Formatter/JSON.hs b/src/ShellCheck/Formatter/JSON.hs deleted file mode 100644 index 6b38532..0000000 --- a/src/ShellCheck/Formatter/JSON.hs +++ /dev/null @@ -1,111 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -module ShellCheck.Formatter.JSON (format) where - -import ShellCheck.Interface -import ShellCheck.Formatter.Format - -import Control.DeepSeq -import Data.Aeson -import Data.IORef -import Data.Monoid -import GHC.Exts -import System.IO -import qualified Data.ByteString.Lazy.Char8 as BL - -format :: IO Formatter -format = do - ref <- newIORef [] - return Formatter { - header = return (), - onResult = collectResult ref, - onFailure = outputError, - footer = finish ref - } - -instance ToJSON Replacement where - toJSON replacement = - let start = repStartPos replacement - end = repEndPos replacement - str = repString replacement in - object [ - "precedence" .= repPrecedence replacement, - "insertionPoint" .= - case repInsertionPoint replacement of - InsertBefore -> "beforeStart" :: String - InsertAfter -> "afterEnd", - "line" .= posLine start, - "column" .= posColumn start, - "endLine" .= posLine end, - "endColumn" .= posColumn end, - "replacement" .= str - ] - -instance ToJSON PositionedComment where - toJSON comment = - let start = pcStartPos comment - end = pcEndPos comment - c = pcComment comment in - object [ - "file" .= posFile start, - "line" .= posLine start, - "endLine" .= posLine end, - "column" .= posColumn start, - "endColumn" .= posColumn end, - "level" .= severityText comment, - "code" .= cCode c, - "message" .= cMessage c, - "fix" .= pcFix comment - ] - - toEncoding comment = - let start = pcStartPos comment - end = pcEndPos comment - c = pcComment comment in - pairs ( - "file" .= posFile start - <> "line" .= posLine start - <> "endLine" .= posLine end - <> "column" .= posColumn start - <> "endColumn" .= posColumn end - <> "level" .= severityText comment - <> "code" .= cCode c - <> "message" .= cMessage c - <> "fix" .= pcFix comment - ) - -instance ToJSON Fix where - toJSON fix = object [ - "replacements" .= fixReplacements fix - ] - -outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg - -collectResult ref cr sys = mapM_ f groups - where - comments = crComments cr - groups = groupWith sourceFile comments - f :: [PositionedComment] -> IO () - f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x) - -finish ref = do - list <- readIORef ref - BL.putStrLn $ encode list diff --git a/src/ShellCheck/Formatter/JSON1.hs b/src/ShellCheck/Formatter/JSON1.hs deleted file mode 100644 index b4dbe35..0000000 --- a/src/ShellCheck/Formatter/JSON1.hs +++ /dev/null @@ -1,128 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -module ShellCheck.Formatter.JSON1 (format) where - -import ShellCheck.Interface -import ShellCheck.Formatter.Format - -import Control.DeepSeq -import Data.Aeson -import Data.IORef -import Data.Monoid -import System.IO -import qualified Data.ByteString.Lazy.Char8 as BL -import qualified Data.List.NonEmpty as NE - -format :: IO Formatter -format = do - ref <- newIORef [] - return Formatter { - header = return (), - onResult = collectResult ref, - onFailure = outputError, - footer = finish ref - } - -data Json1Output = Json1Output { - comments :: [PositionedComment] - } - -instance ToJSON Json1Output where - toJSON result = object [ - "comments" .= comments result - ] - toEncoding result = pairs ( - "comments" .= comments result - ) - -instance ToJSON Replacement where - toJSON replacement = - let start = repStartPos replacement - end = repEndPos replacement - str = repString replacement in - object [ - "precedence" .= repPrecedence replacement, - "insertionPoint" .= - case repInsertionPoint replacement of - InsertBefore -> "beforeStart" :: String - InsertAfter -> "afterEnd", - "line" .= posLine start, - "column" .= posColumn start, - "endLine" .= posLine end, - "endColumn" .= posColumn end, - "replacement" .= str - ] - -instance ToJSON PositionedComment where - toJSON comment = - let start = pcStartPos comment - end = pcEndPos comment - c = pcComment comment in - object [ - "file" .= posFile start, - "line" .= posLine start, - "endLine" .= posLine end, - "column" .= posColumn start, - "endColumn" .= posColumn end, - "level" .= severityText comment, - "code" .= cCode c, - "message" .= cMessage c, - "fix" .= pcFix comment - ] - - toEncoding comment = - let start = pcStartPos comment - end = pcEndPos comment - c = pcComment comment in - pairs ( - "file" .= posFile start - <> "line" .= posLine start - <> "endLine" .= posLine end - <> "column" .= posColumn start - <> "endColumn" .= posColumn end - <> "level" .= severityText comment - <> "code" .= cCode c - <> "message" .= cMessage c - <> "fix" .= pcFix comment - ) - -instance ToJSON Fix where - toJSON fix = object [ - "replacements" .= fixReplacements fix - ] - -outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg - -collectResult ref cr sys = mapM_ f groups - where - comments = crComments cr - groups = NE.groupWith sourceFile comments - f :: NE.NonEmpty PositionedComment -> IO () - f group = do - let filename = sourceFile (NE.head group) - result <- siReadFile sys (Just True) filename - let contents = either (const "") id result - let comments' = makeNonVirtual comments contents - deepseq comments' $ modifyIORef ref (\x -> comments' ++ x) - -finish ref = do - list <- readIORef ref - BL.putStrLn $ encode $ Json1Output { comments = list } diff --git a/src/ShellCheck/Formatter/Quiet.hs b/src/ShellCheck/Formatter/Quiet.hs deleted file mode 100644 index b7e0ee9..0000000 --- a/src/ShellCheck/Formatter/Quiet.hs +++ /dev/null @@ -1,36 +0,0 @@ -{- - Copyright 2019 Austin Voecks - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -module ShellCheck.Formatter.Quiet (format) where - -import ShellCheck.Interface -import ShellCheck.Formatter.Format - -import Control.Monad -import Data.IORef -import System.Exit - -format :: FormatterOptions -> IO Formatter -format options = - return Formatter { - header = return (), - footer = return (), - onFailure = \ _ _ -> exitFailure, - onResult = \ result _ -> unless (null $ crComments result) exitFailure - } diff --git a/src/ShellCheck/Formatter/TTY.hs b/src/ShellCheck/Formatter/TTY.hs deleted file mode 100644 index 117da6e..0000000 --- a/src/ShellCheck/Formatter/TTY.hs +++ /dev/null @@ -1,197 +0,0 @@ -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -module ShellCheck.Formatter.TTY (format) where - -import ShellCheck.Fixer -import ShellCheck.Interface -import ShellCheck.Formatter.Format - -import Control.DeepSeq -import Control.Monad -import Data.Array -import Data.Foldable -import Data.Ord -import Data.IORef -import Data.List -import Data.Maybe -import System.IO -import System.Info -import qualified Data.List.NonEmpty as NE - -wikiLink = "https://www.shellcheck.net/wiki/" - --- An arbitrary Ord thing to order warnings -type Ranking = (Char, Severity, Integer) --- Ansi coloring function -type ColorFunc = (String -> String -> String) - -format :: FormatterOptions -> IO Formatter -format options = do - topErrorRef <- newIORef [] - return Formatter { - header = return (), - footer = outputWiki topErrorRef, - onFailure = outputError options, - onResult = outputResult options topErrorRef - } - -colorForLevel level = - case level of - "error" -> 31 -- red - "warning" -> 33 -- yellow - "info" -> 32 -- green - "style" -> 32 -- green - "verbose" -> 32 -- green - "message" -> 1 -- bold - "source" -> 0 -- none - _ -> 0 -- none - -rankError :: PositionedComment -> Ranking -rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err) - where - ranking = - if cCode (pcComment err) `elem` uninteresting - then 'Z' - else 'A' - - -- A list of the most generic, least directly helpful - -- error codes to downrank. - uninteresting = [ - 1009, -- Mentioned parser error was.. - 1019, -- Expected this to be an argument - 1036, -- ( is invalid here - 1047, -- Expected 'fi' - 1062, -- Expected 'done' - 1070, -- Parsing stopped here (generic) - 1072, -- Missing/unexpected .. - 1073, -- Couldn't parse this .. - 1088, -- Parsing stopped here (paren) - 1089 -- Parsing stopped here (keyword) - ] - -appendComments errRef comments max = do - previous <- readIORef errRef - let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments - writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current - where - fst3 (x,_,_) = x - equal x y = fst3 x == fst3 y - -outputWiki :: IORef [(Ranking, Integer, String)] -> IO () -outputWiki errRef = do - issues <- readIORef errRef - unless (null issues) $ do - putStrLn "For more information:" - mapM_ showErr issues - where - showErr (_, code, msg) = - putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg - limit = 36 - shorten msg = - if length msg < limit - then msg - else (take (limit-3) msg) ++ "..." - -outputError options file error = do - color <- getColorFunc $ foColorOption options - hPutStrLn stderr $ color "error" $ file ++ ": " ++ error - -outputResult options ref result sys = do - color <- getColorFunc $ foColorOption options - let comments = crComments result - appendComments ref comments (fromIntegral $ foWikiLinkCount options) - let fileGroups = NE.groupWith sourceFile comments - mapM_ (outputForFile color sys) fileGroups - -outputForFile color sys comments = do - let fileName = sourceFile (NE.head comments) - result <- siReadFile sys (Just True) fileName - let contents = either (const "") id result - let fileLinesList = lines contents - let lineCount = length fileLinesList - let fileLines = listArray (1, lineCount) fileLinesList - let groups = NE.groupWith lineNo comments - forM_ groups $ \commentsForLine -> do - let lineNum = fromIntegral $ lineNo (NE.head commentsForLine) - let line = if lineNum < 1 || lineNum > lineCount - then "" - else fileLines ! fromIntegral lineNum - putStrLn "" - putStrLn $ color "message" $ - "In " ++ fileName ++" line " ++ show lineNum ++ ":" - putStrLn (color "source" line) - forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c - putStrLn "" - showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines - --- Pick out only the lines necessary to show a fix in action -sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) -sliceFile fix lines = - (mapPositions adjust fix, sliceLines lines) - where - (minLine, maxLine) = - foldl (\(mm, mx) pos -> ((min mm $ fromIntegral $ posLine pos), (max mx $ fromIntegral $ posLine pos))) - (maxBound, minBound) $ - concatMap (\x -> [repStartPos x, repEndPos x]) $ fixReplacements fix - sliceLines :: Array Int String -> Array Int String - sliceLines = ixmap (1, maxLine - minLine + 1) (\x -> x + minLine - 1) - adjust pos = - pos { - posLine = posLine pos - (fromIntegral minLine) + 1 - } - -showFixedString :: ColorFunc -> [PositionedComment] -> Int -> Array Int String -> IO () -showFixedString color comments lineNum fileLines = - let line = fileLines ! fromIntegral lineNum in - case mapMaybe pcFix comments of - [] -> return () - fixes -> do - -- Folding automatically removes overlap - let mergedFix = fold fixes - -- We show the complete, associated fixes, whether or not it includes this - -- and/or other unrelated lines. - let (excerptFix, excerpt) = sliceFile mergedFix fileLines - -- in the spirit of error prone - putStrLn $ color "message" "Did you mean:" - putStrLn $ unlines $ applyFix excerptFix excerpt - -cuteIndent :: PositionedComment -> String -cuteIndent comment = - replicate (fromIntegral $ colNo comment - 1) ' ' ++ - makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment - where - arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^" - makeArrow = - let sameLine = lineNo comment == endLineNo comment - delta = endColNo comment - colNo comment - in - if sameLine && delta > 2 && delta < 32 then arrow delta else "^--" - -code num = "SC" ++ show num - -getColorFunc :: ColorOption -> IO ColorFunc -getColorFunc colorOption = do - useColor <- shouldOutputColor colorOption - return $ if useColor then colorComment else const id - where - colorComment level comment = - ansi (colorForLevel level) ++ comment ++ clear - clear = ansi 0 - ansi n = "\x1B[" ++ show n ++ "m" diff --git a/src/ShellCheck/Interface.hs b/src/ShellCheck/Interface.hs deleted file mode 100644 index 16a7e36..0000000 --- a/src/ShellCheck/Interface.hs +++ /dev/null @@ -1,341 +0,0 @@ -{- - Copyright 2012-2024 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-} -module ShellCheck.Interface - ( - SystemInterface(..) - , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks) - , CheckResult(crFilename, crComments) - , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) - , ParseResult(prComments, prTokenPositions, prRoot) - , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks) - , AnalysisResult(arComments) - , FormatterOptions(foColorOption, foWikiLinkCount) - , Shell(Ksh, Sh, Bash, Dash, BusyboxSh) - , ExecutionMode(Executed, Sourced) - , ErrorMessage - , Code - , Severity(ErrorC, WarningC, InfoC, StyleC) - , Position(posFile, posLine, posColumn) - , Comment(cSeverity, cCode, cMessage) - , PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix) - , ColorOption(ColorAuto, ColorAlways, ColorNever) - , TokenComment(tcId, tcComment, tcFix) - , emptyCheckResult - , newAnalysisResult - , newAnalysisSpec - , newFormatterOptions - , newParseResult - , newPosition - , newSystemInterface - , newTokenComment - , mockedSystemInterface - , mockRcFile - , newParseSpec - , emptyCheckSpec - , newPositionedComment - , newComment - , Fix(fixReplacements) - , newFix - , InsertionPoint(InsertBefore, InsertAfter) - , Replacement(repStartPos, repEndPos, repString, repPrecedence, repInsertionPoint) - , newReplacement - , CheckDescription(cdName, cdDescription, cdPositive, cdNegative) - , newCheckDescription - ) where - -import ShellCheck.AST - -import Control.DeepSeq -import Control.Monad.Identity -import Data.List -import Data.Monoid -import Data.Ord -import Data.Semigroup -import GHC.Generics (Generic) -import qualified Data.Map as Map - - -data SystemInterface m = SystemInterface { - -- | Given: - -- What annotations say about including external files (if anything) - -- A resolved filename from siFindSource - -- Read the file or return an error - siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String), - -- | Given: - -- the current script, - -- what annotations say about including external files (if anything) - -- a list of source-path annotations in effect, - -- and a sourced file, - -- find the sourced file - siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath, - -- | Get the configuration file (name, contents) for a filename - siGetConfig :: String -> m (Maybe (FilePath, String)) -} - --- ShellCheck input and output -data CheckSpec = CheckSpec { - csFilename :: String, - csScript :: String, - csCheckSourced :: Bool, - csIgnoreRC :: Bool, - csExcludedWarnings :: [Integer], - csIncludedWarnings :: Maybe [Integer], - csShellTypeOverride :: Maybe Shell, - csMinSeverity :: Severity, - csExtendedAnalysis :: Maybe Bool, - csOptionalChecks :: [String] -} deriving (Show, Eq) - -data CheckResult = CheckResult { - crFilename :: String, - crComments :: [PositionedComment] -} deriving (Show, Eq) - -emptyCheckResult :: CheckResult -emptyCheckResult = CheckResult { - crFilename = "", - crComments = [] -} - -emptyCheckSpec :: CheckSpec -emptyCheckSpec = CheckSpec { - csFilename = "", - csScript = "", - csCheckSourced = False, - csIgnoreRC = False, - csExcludedWarnings = [], - csIncludedWarnings = Nothing, - csShellTypeOverride = Nothing, - csMinSeverity = StyleC, - csExtendedAnalysis = Nothing, - csOptionalChecks = [] -} - -newParseSpec :: ParseSpec -newParseSpec = ParseSpec { - psFilename = "", - psScript = "", - psCheckSourced = False, - psIgnoreRC = False, - psShellTypeOverride = Nothing -} - -newSystemInterface :: Monad m => SystemInterface m -newSystemInterface = - SystemInterface { - siReadFile = \_ _ -> return $ Left "Not implemented", - siFindSource = \_ _ _ name -> return name, - siGetConfig = \_ -> return Nothing - } - --- Parser input and output -data ParseSpec = ParseSpec { - psFilename :: String, - psScript :: String, - psCheckSourced :: Bool, - psIgnoreRC :: Bool, - psShellTypeOverride :: Maybe Shell -} deriving (Show, Eq) - -data ParseResult = ParseResult { - prComments :: [PositionedComment], - prTokenPositions :: Map.Map Id (Position, Position), - prRoot :: Maybe Token -} deriving (Show, Eq) - -newParseResult :: ParseResult -newParseResult = ParseResult { - prComments = [], - prTokenPositions = Map.empty, - prRoot = Nothing -} - --- Analyzer input and output -data AnalysisSpec = AnalysisSpec { - asScript :: Token, - asShellType :: Maybe Shell, - asFallbackShell :: Maybe Shell, - asExecutionMode :: ExecutionMode, - asCheckSourced :: Bool, - asOptionalChecks :: [String], - asExtendedAnalysis :: Maybe Bool, - asTokenPositions :: Map.Map Id (Position, Position) -} - -newAnalysisSpec token = AnalysisSpec { - asScript = token, - asShellType = Nothing, - asFallbackShell = Nothing, - asExecutionMode = Executed, - asCheckSourced = False, - asOptionalChecks = [], - asExtendedAnalysis = Nothing, - asTokenPositions = Map.empty -} - -newtype AnalysisResult = AnalysisResult { - arComments :: [TokenComment] -} - -newAnalysisResult = AnalysisResult { - arComments = [] -} - --- Formatter options -data FormatterOptions = FormatterOptions { - foColorOption :: ColorOption, - foWikiLinkCount :: Integer -} - -newFormatterOptions = FormatterOptions { - foColorOption = ColorAuto, - foWikiLinkCount = 3 -} - -data CheckDescription = CheckDescription { - cdName :: String, - cdDescription :: String, - cdPositive :: String, - cdNegative :: String - } - -newCheckDescription = CheckDescription { - cdName = "", - cdDescription = "", - cdPositive = "", - cdNegative = "" - } - --- Supporting data types -data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq) -data ExecutionMode = Executed | Sourced deriving (Show, Eq) - -type ErrorMessage = String -type Code = Integer - -data Severity = ErrorC | WarningC | InfoC | StyleC - deriving (Show, Eq, Ord, Generic, NFData) -data Position = Position { - posFile :: String, -- Filename - posLine :: Integer, -- 1 based source line - posColumn :: Integer -- 1 based source column, where tabs are 8 -} deriving (Show, Eq, Generic, NFData, Ord) - -newPosition :: Position -newPosition = Position { - posFile = "", - posLine = 1, - posColumn = 1 -} - -data Comment = Comment { - cSeverity :: Severity, - cCode :: Code, - cMessage :: String -} deriving (Show, Eq, Generic, NFData) - -newComment :: Comment -newComment = Comment { - cSeverity = StyleC, - cCode = 0, - cMessage = "" -} - --- only support single line for now -data Replacement = Replacement { - repStartPos :: Position, - repEndPos :: Position, - repString :: String, - -- Order in which the replacements should happen: highest precedence first. - repPrecedence :: Int, - -- Whether to insert immediately before or immediately after the specified region. - repInsertionPoint :: InsertionPoint -} deriving (Show, Eq, Generic, NFData) - -data InsertionPoint = InsertBefore | InsertAfter - deriving (Show, Eq, Generic, NFData) - -newReplacement = Replacement { - repStartPos = newPosition, - repEndPos = newPosition, - repString = "", - repPrecedence = 1, - repInsertionPoint = InsertAfter -} - -data Fix = Fix { - fixReplacements :: [Replacement] -} deriving (Show, Eq, Generic, NFData) - -newFix = Fix { - fixReplacements = [] -} - -data PositionedComment = PositionedComment { - pcStartPos :: Position, - pcEndPos :: Position, - pcComment :: Comment, - pcFix :: Maybe Fix -} deriving (Show, Eq, Generic, NFData) - -newPositionedComment :: PositionedComment -newPositionedComment = PositionedComment { - pcStartPos = newPosition, - pcEndPos = newPosition, - pcComment = newComment, - pcFix = Nothing -} - -data TokenComment = TokenComment { - tcId :: Id, - tcComment :: Comment, - tcFix :: Maybe Fix -} deriving (Show, Eq, Generic, NFData) - -newTokenComment = TokenComment { - tcId = Id 0, - tcComment = newComment, - tcFix = Nothing -} - -data ColorOption = - ColorAuto - | ColorAlways - | ColorNever - deriving (Ord, Eq, Show) - --- For testing -mockedSystemInterface :: [(String, String)] -> SystemInterface Identity -mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) { - siReadFile = rf, - siFindSource = fs, - siGetConfig = const $ return Nothing -} - where - rf _ file = return $ - case find ((== file) . fst) files of - Nothing -> Left "File not included in mock." - Just (_, contents) -> Right contents - fs _ _ _ file = return file - -mockRcFile rcfile mock = mock { - siGetConfig = const . return $ Just (".shellcheckrc", rcfile) -} diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs deleted file mode 100644 index f8a94bc..0000000 --- a/src/ShellCheck/Parser.hs +++ /dev/null @@ -1,3668 +0,0 @@ -{- - Copyright 2012-2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE NoMonomorphismRestriction #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE MultiWayIf #-} -module ShellCheck.Parser (parseScript, runTests) where - -import ShellCheck.AST -import ShellCheck.ASTLib hiding (runTests) -import ShellCheck.Data -import ShellCheck.Interface -import ShellCheck.Prelude - -import Control.Applicative ((<*), (*>)) -import Control.Monad -import Control.Monad.Identity -import Control.Monad.Trans -import Data.Char -import Data.Functor -import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find) -import Data.Maybe -import Data.Monoid -import GHC.Exts (sortWith) -import Prelude hiding (readList) -import System.IO -import Text.Parsec hiding (runParser, ()) -import Text.Parsec.Error -import Text.Parsec.Pos -import qualified Control.Monad.Reader as Mr -import qualified Control.Monad.State as Ms -import qualified Data.List.NonEmpty as NE -import qualified Data.Map.Strict as Map -import Debug.Trace - -import Test.QuickCheck.All (quickCheckAll) - -type SCBase m = Mr.ReaderT (Environment m) (Ms.StateT SystemState m) -type SCParser m v = ParsecT String UserState (SCBase m) v - -backslash :: Monad m => SCParser m Char -backslash = char '\\' -linefeed :: Monad m => SCParser m Char -linefeed = do - optional carriageReturn - c <- char '\n' - readPendingHereDocs - return c -singleQuote = char '\'' -doubleQuote = char '"' -variableStart = upper <|> lower <|> oneOf "_" -variableChars = upper <|> lower <|> digit <|> oneOf "_" --- Chars to allow function names to start with -functionStartChars = variableChars <|> oneOf ":+?-./^@," --- Chars to allow inside function names -functionChars = variableChars <|> oneOf "#:+?-./^@," --- Chars to allow function names to start with, using the 'function' keyword -extendedFunctionStartChars = functionStartChars <|> oneOf "[]*=!" --- Chars to allow in functions using the 'function' keyword -extendedFunctionChars = extendedFunctionStartChars <|> oneOf "[]*=!" -specialVariable = oneOf (concat specialVariables) -paramSubSpecialChars = oneOf "/:+-=%" -quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars -quotable = almostSpace <|> oneOf quotableChars -bracedQuotable = oneOf "}\"$`'" -doubleQuotableChars = "\\\"$`" -doubleQuotable = oneOf doubleQuotableChars -whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed -linewhitespace = oneOf " \t" <|> almostSpace - -suspectCharAfterQuotes = variableChars <|> char '%' - -extglobStartChars = "?*@!+" -extglobStart = oneOf extglobStartChars - -unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036" -unicodeSingleQuotes = "\x2018\x2019" - -prop_spacing1 = isOk spacing " \\\n # Comment" -prop_spacing2 = isOk spacing "# We can continue lines with \\" -prop_spacing3 = isWarning spacing " \\\n # --verbose=true \\" -spacing = do - x <- many (many1 linewhitespace <|> continuation) - optional readComment - return $ concat x - where - continuation = do - try (string "\\\n") - -- The line was continued. Warn if this next line is a comment with a trailing \ - whitespace <- many linewhitespace - optional $ do - x <- readComment - when ("\\" `isSuffixOf` x) $ - parseProblem ErrorC 1143 "This backslash is part of a comment and does not continue the line." - return whitespace - -spacing1 = do - spacing <- spacing - when (null spacing) $ fail "Expected whitespace" - return spacing - -prop_allspacing = isOk allspacing "#foo" -prop_allspacing2 = isOk allspacing " #foo\n # bar\n#baz\n" -prop_allspacing3 = isOk allspacing "#foo\n#bar\n#baz\n" -allspacing = do - s <- spacing - more <- option False (linefeed >> return True) - if more then do - rest <- allspacing - return $ s ++ "\n" ++ rest - else - return s - -allspacingOrFail = do - s <- allspacing - when (null s) $ fail "Expected whitespace" - return s - -readUnicodeQuote = do - start <- startSpan - c <- oneOf (unicodeSingleQuotes ++ unicodeDoubleQuotes) - id <- endSpan start - parseProblemAtId id WarningC 1110 "This is a unicode quote. Delete and retype it (or quote to make literal)." - return $ T_Literal id [c] - -carriageReturn = do - pos <- getPosition - char '\r' - parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ." - return '\r' - -almostSpace = do - parseNote ErrorC 1018 $ "This is a unicode space. Delete and retype it." - oneOf "\xA0\x2002\x2003\x2004\x2005\x2006\x2007\x2008\x2009\x200B\x202F" - return ' ' - ---------- Message/position annotation on top of user state -data ParseNote = ParseNote SourcePos SourcePos Severity Code String deriving (Show, Eq) -data Context = - ContextName SourcePos String - | ContextAnnotation [Annotation] - | ContextSource String - deriving (Show) - -data HereDocContext = - HereDocPending Id Dashed Quoted String [Context] -- on linefeed, read this T_HereDoc - deriving (Show) - -data UserState = UserState { - lastId :: Id, - positionMap :: Map.Map Id (SourcePos, SourcePos), - parseNotes :: [ParseNote], - hereDocMap :: Map.Map Id [Token], - pendingHereDocs :: [HereDocContext] -} -initialUserState = UserState { - lastId = Id $ -1, - positionMap = Map.empty, - parseNotes = [], - hereDocMap = Map.empty, - pendingHereDocs = [] -} - -codeForParseNote (ParseNote _ _ _ code _) = code - -getLastId = lastId <$> getState - -getNextIdBetween startPos endPos = do - state <- getState - let newId = incId (lastId state) - let newMap = Map.insert newId (startPos, endPos) (positionMap state) - putState $ state { - lastId = newId, - positionMap = newMap - } - return newId - where incId (Id n) = Id $ n+1 - -getNextIdSpanningTokens startTok endTok = do - (start, _) <- getSpanForId (getId startTok) - (_, end) <- getSpanForId (getId endTok) - getNextIdBetween start end - --- Get an ID starting from the first token of the list, and ending after the last -getNextIdSpanningTokenList list = - case list of - [] -> do - pos <- getPosition - getNextIdBetween pos pos - (h:_) -> - getNextIdSpanningTokens h (last list) - --- Get the span covered by an id -getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos) -getSpanForId id = - Map.findWithDefault (error $ pleaseReport "no parser span for id") id <$> - getMap - --- Create a new id with the same span as an existing one -getNewIdFor :: Monad m => Id -> SCParser m Id -getNewIdFor id = getSpanForId id >>= uncurry getNextIdBetween - -data IncompleteInterval = IncompleteInterval SourcePos - -startSpan = IncompleteInterval <$> getPosition - -endSpan (IncompleteInterval start) = do - endPos <- getPosition - getNextIdBetween start endPos - -getSpanPositionsFor m = do - start <- getPosition - m - end <- getPosition - return (start, end) - -addToHereDocMap id list = do - state <- getState - let map = hereDocMap state - putState $ state { - hereDocMap = Map.insert id list map - } - -addPendingHereDoc id d q str = do - state <- getState - context <- getCurrentContexts - let docs = pendingHereDocs state - putState $ state { - pendingHereDocs = HereDocPending id d q str context : docs - } - -popPendingHereDocs = do - state <- getState - let pending = pendingHereDocs state - putState $ state { - pendingHereDocs = [] - } - return . reverse $ pendingHereDocs state - -getMap = positionMap <$> getState -getParseNotes = parseNotes <$> getState - -addParseNote n = do - irrelevant <- shouldIgnoreCode (codeForParseNote n) - unless irrelevant $ do - state <- getState - putState $ state { - parseNotes = n : parseNotes state - } - -ignoreProblemsOf p = do - systemState <- lift . lift $ Ms.get - p <* (lift . lift . Ms.put $ systemState) - -shouldIgnoreCode code = do - context <- getCurrentContexts - checkSourced <- Mr.asks checkSourced - return $ any (contextItemDisablesCode checkSourced code) context - --- Does this item on the context stack disable warnings for 'code'? -contextItemDisablesCode :: Bool -> Integer -> Context -> Bool -contextItemDisablesCode alsoCheckSourced code = disabling alsoCheckSourced - where - disabling checkSourced item = - case item of - ContextAnnotation list -> any disabling' list - ContextSource _ -> not $ checkSourced - _ -> False - disabling' (DisableComment n m) = code >= n && code < m - disabling' _ = False - - - -getCurrentAnnotations includeSource = - concatMap get . takeWhile (not . isBoundary) <$> getCurrentContexts - where - get (ContextAnnotation list) = list - get _ = [] - isBoundary (ContextSource _) = not includeSource - isBoundary _ = False - - -shouldFollow file = do - context <- getCurrentContexts - if any isThisFile context - then return False - else - if length (filter isSource context) >= 100 - then do - parseProblem ErrorC 1092 "Stopping at 100 'source' frames :O" - return False - else - return True - where - isSource (ContextSource _) = True - isSource _ = False - isThisFile (ContextSource name) | name == file = True - isThisFile _= False - -getSourceOverride = do - context <- getCurrentContexts - return . msum . map findFile $ takeWhile isSameFile context - where - isSameFile (ContextSource _) = False - isSameFile _ = True - - findFile (ContextAnnotation list) = msum $ map getFile list - findFile _ = Nothing - getFile (SourceOverride str) = Just str - getFile _ = Nothing - --- Store potential parse problems outside of parsec - -data SystemState = SystemState { - contextStack :: [Context], - parseProblems :: [ParseNote] -} -initialSystemState = SystemState { - contextStack = [], - parseProblems = [] -} - -data Environment m = Environment { - systemInterface :: SystemInterface m, - checkSourced :: Bool, - ignoreRC :: Bool, - currentFilename :: String, - shellTypeOverride :: Maybe Shell -} - -parseProblem level code msg = do - pos <- getPosition - parseProblemAt pos level code msg - -setCurrentContexts c = Ms.modify (\state -> state { contextStack = c }) -getCurrentContexts = Ms.gets contextStack - -popContext = do - v <- getCurrentContexts - case v of - (a:r) -> do - setCurrentContexts r - return $ Just a - [] -> - return Nothing - -pushContext c = do - v <- getCurrentContexts - setCurrentContexts (c:v) - -parseProblemAtWithEnd start end level code msg = do - irrelevant <- shouldIgnoreCode code - unless irrelevant $ - addParseProblem note - where - note = ParseNote start end level code msg - -addParseProblem note = - Ms.modify (\state -> state { - parseProblems = note:parseProblems state - }) - -parseProblemAt pos = parseProblemAtWithEnd pos pos - -parseProblemAtId :: Monad m => Id -> Severity -> Integer -> String -> SCParser m () -parseProblemAtId id level code msg = do - (start, end) <- getSpanForId id - parseProblemAtWithEnd start end level code msg - --- Store non-parse problems inside - -parseNote c l a = do - pos <- getPosition - parseNoteAt pos c l a - -parseNoteAt pos c l a = addParseNote $ ParseNote pos pos c l a -parseNoteAtId id c l a = do - (start, end) <- getSpanForId id - addParseNote $ ParseNote start end c l a - -parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a - ---------- Convenient combinators -thenSkip main follow = main <* optional follow - -unexpecting s p = try $ - (try p >> fail ("Unexpected " ++ s)) <|> return () - -notFollowedBy2 = unexpecting "" - -isFollowedBy p = (lookAhead . try $ p $> True) <|> return False - -reluctantlyTill p end = - (lookAhead (void (try end) <|> eof) >> return []) <|> do - x <- p - more <- reluctantlyTill p end - return $ x:more - <|> return [] - -reluctantlyTill1 p end = do - notFollowedBy2 end - x <- p - more <- reluctantlyTill p end - return $ x:more - -attempting rest branch = - (try branch >> rest) <|> rest - -orFail parser errorAction = - try parser <|> (errorAction >>= fail) - --- Construct a node with a parser, e.g. T_Literal `withParser` (readGenericLiteral ",") -withParser node parser = do - start <- startSpan - contents <- parser - id <- endSpan start - return $ node id contents - -wasIncluded p = option False (p >> return True) - -acceptButWarn parser level code note = - optional $ try (do - pos <- getPosition - parser - parseProblemAt pos level code note - ) - -parsecBracket before after op = do - val <- before - op val `thenSkip` after val <|> (after val *> fail "") - -swapContext contexts p = - parsecBracket (getCurrentContexts <* setCurrentContexts contexts) - setCurrentContexts - (const p) - -withContext entry p = parsecBracket (pushContext entry) (const popContext) (const p) - -called s p = do - pos <- getPosition - withContext (ContextName pos s) p - -withAnnotations anns p = - if null anns then p else withContext (ContextAnnotation anns) p - -readConditionContents single = - readCondContents `attempting` lookAhead (do - pos <- getPosition - s <- readVariableName - spacing1 - when (s `elem` commonCommands) $ - parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.") - - where - spacingOrLf = condSpacing True - condSpacing required = do - pos <- getPosition - space <- allspacing - when (required && null space) $ - parseProblemAt pos ErrorC 1035 "You are missing a required space here." - when (single && '\n' `elem` space) $ - parseProblemAt pos ErrorC 1080 "When breaking lines in [ ], you need \\ before the linefeed." - return space - - typ = if single then SingleBracket else DoubleBracket - readCondBinaryOp = try $ do - optional guardArithmetic - op <- getOp - spacingOrLf - return op - where - flaglessOps = [ "==", "!=", "<=", ">=", "=~", ">", "<", "=" ] - - getOp = do - start <- startSpan - op <- readRegularOrEscaped anyOp - id <- endSpan start - return $ TC_Binary id typ op - - anyOp = flagOp <|> flaglessOp <|> fail - "Expected comparison operator (don't wrap commands in []/[[]])" - flagOp = try $ do - s <- readOp - when (s == "-a" || s == "-o") $ fail "Unexpected operator" - return s - flaglessOp = - choice $ map (try . string) flaglessOps - - -- hacks to read quoted operators without having to read a shell word - readEscaped p = try $ withEscape <|> withQuotes - where - withEscape = do - char '\\' - escaped <$> p - withQuotes = do - c <- oneOf "'\"" - s <- p - char c - return $ escaped s - escaped s = if any (`elem` s) "<>()" then '\\':s else s - - readRegularOrEscaped p = readEscaped p <|> p - - - guardArithmetic = do - try . lookAhead $ void (oneOf "+*/%") <|> void (string "- ") - parseProblem ErrorC 1076 $ - if single - then "Trying to do math? Use e.g. [ $((i/2+7)) -ge 18 ]." - else "Trying to do math? Use e.g. [[ $((i/2+7)) -ge 18 ]]." - - readCondUnaryExp = do - op <- readCondUnaryOp - pos <- getPosition - liftM op readCondWord `orFail` do - parseProblemAt pos ErrorC 1019 "Expected this to be an argument to the unary condition." - return "Expected an argument for the unary operator" - - readCondUnaryOp = try $ do - start <- startSpan - s <- readOp - id <- endSpan start - spacingOrLf - return $ TC_Unary id typ s - - readOp = try $ do - char '-' <|> weirdDash - s <- many1 letter <|> fail "Expected a test operator" - return ('-':s) - - weirdDash = do - pos <- getPosition - oneOf "\x058A\x05BE\x2010\x2011\x2012\x2013\x2014\x2015\xFE63\xFF0D" - parseProblemAt pos ErrorC 1100 - "This is a unicode dash. Delete and retype as ASCII minus." - return '-' - - readCondWord = do - notFollowedBy2 (try (spacing >> string "]")) - x <- readNormalWord - pos <- getPosition - when (notArrayIndex x && endedWith "]" x && not (x `containsLiteral` "[")) $ do - parseProblemAt pos ErrorC 1020 $ - "You need a space before the " ++ (if single then "]" else "]]") ++ "." - fail "Missing space before ]" - when (single && endedWith ")" x) $ do - parseProblemAt pos ErrorC 1021 - "You need a space before the \\)" - fail "Missing space before )" - void spacing - return x - where endedWith str (T_NormalWord id s@(_:_)) = - case last s of T_Literal id s -> str `isSuffixOf` s - _ -> False - endedWith _ _ = False - notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "[" - notArrayIndex _ = True - containsLiteral x s = s `isInfixOf` onlyLiteralString x - - readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True - - readCondOrOp = do - optional guardArithmetic - readAndOrOp TC_Or "||" False <|> readAndOrOp TC_Or "-o" True - - readAndOrOp node op requiresSpacing = do - optional $ lookAhead weirdDash - start <- startSpan - x <- try $ string op - id <- endSpan start - condSpacing requiresSpacing - return $ node id typ x - - readCondNullaryOrBinary = do - start <- startSpan - x <- readCondWord `attempting` (do - pos <- getPosition - lookAhead (char '[') - parseProblemAt pos ErrorC 1026 $ if single - then "If grouping expressions inside [..], use \\( ..\\)." - else "If grouping expressions inside [[..]], use ( .. )." - ) - id <- endSpan start - (do - pos <- getPosition - isRegex <- regexOperatorAhead - op <- readCondBinaryOp - y <- if isRegex - then readRegex - else readCondWord <|> (parseProblemAt pos ErrorC 1027 "Expected another argument for this operator." >> mzero) - return (x `op` y) - ) <|> ( do - checkTrailingOp x - return $ TC_Nullary id typ x - ) - - checkTrailingOp x = sequence_ $ do - (T_Literal id str) <- getTrailingUnquotedLiteral x - trailingOp <- find (`isSuffixOf` str) binaryTestOps - return $ parseProblemAtId id ErrorC 1108 $ - "You need a space before and after the " ++ trailingOp ++ " ." - - readCondGroup = do - start <- startSpan - pos <- getPosition - lparen <- try $ readRegularOrEscaped (string "(") - when (single && lparen == "(") $ - singleWarning pos - when (not single && lparen == "\\(") $ - doubleWarning pos - condSpacing single - x <- readCondContents - cpos <- getPosition - rparen <- readRegularOrEscaped (string ")") - id <- endSpan start - condSpacing single - when (single && rparen == ")") $ - singleWarning cpos - when (not single && rparen == "\\)") $ - doubleWarning cpos - return $ TC_Group id typ x - - where - singleWarning pos = - parseProblemAt pos ErrorC 1028 "In [..] you have to escape \\( \\) or preferably combine [..] expressions." - doubleWarning pos = - parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ( or )." - - - -- Currently a bit of a hack since parsing rules are obscure - regexOperatorAhead = lookAhead (do - try (string "=~") <|> try (string "~=") - return True) - <|> return False - readRegex = called "regex" $ do - start <- startSpan - parts <- many1 readPart - id <- endSpan start - void spacing - return $ T_NormalWord id parts - where - readPart = choice [ - readGroup, - readSingleQuoted, - readDoubleQuoted, - readDollarExpression, - readLiteralForParser $ readNormalLiteral "( ", - readLiteralString "|", - readGlobLiteral - ] - readGlobLiteral = do - start <- startSpan - s <- extglobStart <|> oneOf "{}[]$" - id <- endSpan start - return $ T_Literal id [s] - readGroup = called "regex grouping" $ do - start <- startSpan - p1 <- readLiteralString "(" - parts <- many (readPart <|> readRegexLiteral) - p2 <- readLiteralString ")" - id <- endSpan start - return $ T_NormalWord id (p1:(parts ++ [p2])) - readRegexLiteral = do - start <- startSpan - str <- readGenericLiteral1 (singleQuote <|> doubleQuotable <|> oneOf "()") - id <- endSpan start - return $ T_Literal id str - readLiteralString s = do - start <- startSpan - str <- string s - id <- endSpan start - return $ T_Literal id str - - readCondTerm = do - term <- readCondNot <|> readCondExpr - condSpacing False - return term - - readCondNot = do - start <- startSpan - char '!' - id <- endSpan start - spacingOrLf - expr <- readCondExpr - return $ TC_Unary id typ "!" expr - - readCondExpr = - readCondGroup <|> readCondUnaryExp <|> readCondNullaryOrBinary - - readCondOr = chainl1 readCondAnd readCondAndOp - readCondAnd = chainl1 readCondTerm readCondOrOp - readCondContents = readCondOr - - -prop_a1 = isOk readArithmeticContents " n++ + ++c" -prop_a2 = isOk readArithmeticContents "$N*4-(3,2)" -prop_a3 = isOk readArithmeticContents "n|=2<<1" -prop_a4 = isOk readArithmeticContents "n &= 2 **3" -prop_a5 = isOk readArithmeticContents "1 |= 4 && n >>= 4" -prop_a6 = isOk readArithmeticContents " 1 | 2 ||3|4" -prop_a7 = isOk readArithmeticContents "3*2**10" -prop_a8 = isOk readArithmeticContents "3" -prop_a9 = isOk readArithmeticContents "a^!-b" -prop_a10 = isOk readArithmeticContents "! $?" -prop_a11 = isOk readArithmeticContents "10#08 * 16#f" -prop_a12 = isOk readArithmeticContents "\"$((3+2))\" + '37'" -prop_a13 = isOk readArithmeticContents "foo[9*y+x]++" -prop_a14 = isOk readArithmeticContents "1+`echo 2`" -prop_a15 = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4" -prop_a16 = isOk readArithmeticContents "$foo$bar" -prop_a17 = isOk readArithmeticContents "i<(0+(1+1))" -prop_a18 = isOk readArithmeticContents "a?b:c" -prop_a19 = isOk readArithmeticContents "\\\n3 +\\\n 2" -prop_a20 = isOk readArithmeticContents "a ? b ? c : d : e" -prop_a21 = isOk readArithmeticContents "a ? b : c ? d : e" -prop_a22 = isOk readArithmeticContents "!!a" -prop_a23 = isOk readArithmeticContents "~0" -readArithmeticContents :: Monad m => SCParser m Token -readArithmeticContents = - readSequence - where - spacing = - let lf = try (string "\\\n") >> return '\n' - in many (whitespace <|> lf) - - splitBy x ops = chainl1 x (readBinary ops) - readBinary ops = readComboOp ops TA_Binary - readComboOp op token = do - start <- startSpan - op <- choice (map (\x -> try $ do - s <- string x - failIfIncompleteOp - return s - ) op) - id <- endSpan start - spacing - return $ token id op - - failIfIncompleteOp = notFollowedBy2 $ oneOf "&|<>=" - - -- Read binary minus, but also check for -lt, -gt and friends: - readMinusOp = do - start <- startSpan - pos <- getPosition - try $ do - char '-' - failIfIncompleteOp - optional $ do - (str, alt) <- lookAhead . choice $ map tryOp [ - ("lt", "<"), - ("gt", ">"), - ("le", "<="), - ("ge", ">="), - ("eq", "=="), - ("ne", "!=") - ] - parseProblemAt pos ErrorC 1106 $ "In arithmetic contexts, use " ++ alt ++ " instead of -" ++ str - id <- endSpan start - spacing - return $ TA_Binary id "-" - where - tryOp (str, alt) = try $ do - string str - spacing1 - return (str, alt) - - readArrayIndex = do - start <- startSpan - char '[' - pos <- getPosition - middle <- readStringForParser readArithmeticContents - char ']' - id <- endSpan start - return $ T_UnparsedIndex id pos middle - - literal s = do - start <- startSpan - string s - id <- endSpan start - return $ T_Literal id s - - readVariable = do - start <- startSpan - name <- readVariableName - indices <- many readArrayIndex - id <- endSpan start - spacing - return $ TA_Variable id name indices - - readExpansion = do - start <- startSpan - pieces <- many1 $ choice [ - readSingleQuoted, - readDoubleQuoted, - readNormalDollar, - readBraced, - readUnquotedBackTicked, - literal "#", - readNormalLiteral "+-*/=%^,]?:" - ] - id <- endSpan start - spacing - return $ TA_Expansion id pieces - - readGroup = do - start <- startSpan - char '(' - s <- readSequence - char ')' - id <- endSpan start - spacing - return $ TA_Parenthesis id s - - readArithTerm = readGroup <|> readVariable <|> readExpansion - - readSequence = do - spacing - start <- startSpan - l <- readAssignment `sepBy` (char ',' >> spacing) - id <- endSpan start - return $ TA_Sequence id l - - readAssignment = chainr1 readTrinary readAssignmentOp - readAssignmentOp = readComboOp ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] TA_Assignment - - readTrinary = do - x <- readLogicalOr - do - start <- startSpan - string "?" - spacing - y <- readTrinary - string ":" - spacing - z <- readTrinary - id <- endSpan start - return $ TA_Trinary id x y z - <|> - return x - - readLogicalOr = readLogicalAnd `splitBy` ["||"] - readLogicalAnd = readBitOr `splitBy` ["&&"] - readBitOr = readBitXor `splitBy` ["|"] - readBitXor = readBitAnd `splitBy` ["^"] - readBitAnd = readEquated `splitBy` ["&"] - readEquated = readCompared `splitBy` ["==", "!="] - readCompared = readShift `splitBy` ["<=", ">=", "<", ">"] - readShift = readAddition `splitBy` ["<<", ">>"] - readAddition = chainl1 readMultiplication (readBinary ["+"] <|> readMinusOp) - readMultiplication = readExponential `splitBy` ["*", "/", "%"] - readExponential = readAnyNegated `splitBy` ["**"] - - readAnyNegated = readNegated <|> readAnySigned - readNegated = do - start <- startSpan - op <- oneOf "!~" - id <- endSpan start - spacing - x <- readAnyNegated - return $ TA_Unary id [op] x - - readAnySigned = readSigned <|> readAnycremented - readSigned = do - start <- startSpan - op <- choice (map readSignOp "+-") - id <- endSpan start - spacing - x <- readAnycremented - return $ TA_Unary id [op] x - where - readSignOp c = try $ do - char c - notFollowedBy2 $ char c - spacing - return c - - readAnycremented = readNormalOrPostfixIncremented <|> readPrefixIncremented - readPrefixIncremented = do - start <- startSpan - op <- try $ string "++" <|> string "--" - id <- endSpan start - spacing - x <- readArithTerm - return $ TA_Unary id (op ++ "|") x - - readNormalOrPostfixIncremented = do - x <- readArithTerm - spacing - do - start <- startSpan - op <- try $ string "++" <|> string "--" - id <- endSpan start - spacing - return $ TA_Unary id ('|':op) x - <|> - return x - - - -prop_readCondition = isOk readCondition "[ \\( a = b \\) -a \\( c = d \\) ]" -prop_readCondition2 = isOk readCondition "[[ (a = b) || (c = d) ]]" -prop_readCondition3 = isOk readCondition "[[ $c = [[:alpha:].~-] ]]" -prop_readCondition4 = isOk readCondition "[[ $c =~ *foo* ]]" -prop_readCondition5 = isOk readCondition "[[ $c =~ f( ]] )* ]]" -prop_readCondition5a = isOk readCondition "[[ $c =~ a(b) ]]" -prop_readCondition5b = isOk readCondition "[[ $c =~ f( ($var ]]) )* ]]" -prop_readCondition6 = isOk readCondition "[[ $c =~ ^[yY]$ ]]" -prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]" -prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]" -prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]" -prop_readCondition10 = isOk readCondition "[[\na == b\n||\nc == d ]]" -prop_readCondition10a = isOk readCondition "[[\na == b ||\nc == d ]]" -prop_readCondition10b = isOk readCondition "[[ a == b\n||\nc == d ]]" -prop_readCondition11 = isOk readCondition "[[ a == b ||\n c == d ]]" -prop_readCondition12 = isWarning readCondition "[ a == b \n -o c == d ]" -prop_readCondition13 = isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]" -prop_readCondition14 = isOk readCondition "[ foo '>' bar ]" -prop_readCondition15 = isOk readCondition "[ foo \">=\" bar ]" -prop_readCondition16 = isOk readCondition "[ foo \\< bar ]" -prop_readCondition17 = isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]" -prop_readCondition18 = isOk readCondition "[ ]" -prop_readCondition19 = isOk readCondition "[ '(' x \")\" ]" -prop_readCondition20 = isOk readCondition "[[ echo_rc -eq 0 ]]" -prop_readCondition21 = isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]" -prop_readCondition22 = isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]" -prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]" -prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]" -prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar" -prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo" -prop_readCondition28 = isOk readCondition "[[ x = [\"$1\"] ]]" -prop_readCondition29 = isOk readCondition "[[ x = [*] ]]" - -readCondition = called "test expression" $ do - opos <- getPosition - start <- startSpan - open <- try (string "[[") <|> string "[" - let single = open == "[" - let typ = if single then SingleBracket else DoubleBracket - - pos <- getPosition - space <- allspacing - when (null space) $ - parseProblemAtWithEnd opos pos ErrorC 1035 $ "You need a space after the " ++ - if single - then "[ and before the ]." - else "[[ and before the ]]." - when (single && '\n' `elem` space) $ - parseProblemAt pos ErrorC 1080 "You need \\ before line feeds to break lines in [ ]." - - condition <- readConditionContents single <|> do - guard . not . null $ space - lookAhead $ string "]" - id <- endSpan start - return $ TC_Empty id typ - - cpos <- getPosition - close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])" - id <- endSpan start - when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Test expression was opened with double [[ but closed with single ]. Make sure they match." - when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Test expression was opened with single [ but closed with double ]]. Make sure they match." - spacing - return $ T_Condition id typ condition - -readAnnotationPrefix = do - char '#' - many linewhitespace - string "shellcheck" - -prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n" -prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n" -prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n" -prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n" -prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n" -prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n" -prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n" -prop_readAnnotation8 = isOk readAnnotation "# shellcheck disable=all\n" -prop_readAnnotation9 = isOk readAnnotation "# shellcheck source='foo bar' source-path=\"baz etc\"\n" -prop_readAnnotation10 = isOk readAnnotation "# shellcheck disable='SC1234,SC2345' enable=\"foo\" shell='bash'\n" -prop_readAnnotation11 = isOk (readAnnotationWithoutPrefix False) "external-sources='true'" - -readAnnotation = called "shellcheck directive" $ do - try readAnnotationPrefix - many1 linewhitespace - readAnnotationWithoutPrefix True - -readAnnotationWithoutPrefix sandboxed = do - values <- many1 readKey - optional readAnyComment - void linefeed <|> eof <|> do - parseNote ErrorC 1125 "Invalid key=value pair? Ignoring the rest of this directive starting here." - many (noneOf "\n") - void linefeed <|> eof - many linewhitespace - return $ concat values - where - plainOrQuoted p = quoted p <|> p - quoted p = do - c <- oneOf "'\"" - start <- getPosition - str <- many1 $ noneOf (c:"\n") - char c <|> fail "Missing terminating quote for directive." - subParse start p str - readKey = do - keyPos <- getPosition - key <- many1 (letter <|> char '-') - char '=' <|> fail "Expected '=' after directive key" - annotations <- case key of - "disable" -> plainOrQuoted $ readElement `sepBy` char ',' - where - readElement = readRange <|> readAll - readAll = do - string "all" - return $ DisableComment 0 1000000 - readRange = do - from <- readCode - to <- choice [ char '-' *> readCode, return $ from+1 ] - return $ DisableComment from to - readCode = do - optional $ string "SC" - int <- many1 digit - return $ read int - - "enable" -> plainOrQuoted $ readName `sepBy` char ',' - where - readName = EnableComment <$> many1 (letter <|> char '-') - - "source" -> do - filename <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n") - return [SourceOverride filename] - - "source-path" -> do - dirname <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n") - return [SourcePath dirname] - - "shell" -> do - pos <- getPosition - shell <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n") - when (isNothing $ shellForExecutable shell) $ - parseNoteAt pos ErrorC 1103 - "This shell type is unknown. Use e.g. sh or bash." - return [ShellOverride shell] - - "extended-analysis" -> do - pos <- getPosition - value <- plainOrQuoted $ many1 letter - case value of - "true" -> return [ExtendedAnalysis True] - "false" -> return [ExtendedAnalysis False] - _ -> do - parseNoteAt pos ErrorC 1146 "Unknown extended-analysis value. Expected true/false." - return [] - - "external-sources" -> do - pos <- getPosition - value <- plainOrQuoted $ many1 letter - case value of - "true" -> - if sandboxed - then do - parseNoteAt pos ErrorC 1144 "external-sources can only be enabled in .shellcheckrc, not in individual files." - return [] - else return [ExternalSources True] - "false" -> return [ExternalSources False] - _ -> do - parseNoteAt pos ErrorC 1145 "Unknown external-sources value. Expected true/false." - return [] - - _ -> do - parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored." - anyChar `reluctantlyTill` whitespace - return [] - - many linewhitespace - return annotations - -readAnnotations = do - annotations <- many (readAnnotation `thenSkip` allspacing) - return $ concat annotations - -readComment = do - unexpecting "shellcheck annotation" readAnnotationPrefix - readAnyComment - -prop_readAnyComment = isOk readAnyComment "# Comment" -readAnyComment = do - char '#' - many $ noneOf "\r\n" - -prop_readNormalWord = isOk readNormalWord "'foo'\"bar\"{1..3}baz$(lol)" -prop_readNormalWord2 = isOk readNormalWord "foo**(foo)!!!(@@(bar))" -prop_readNormalWord3 = isOk readNormalWord "foo#" -prop_readNormalWord4 = isOk readNormalWord "$\"foo\"$'foo\nbar'" -prop_readNormalWord5 = isWarning readNormalWord "${foo}}" -prop_readNormalWord6 = isOk readNormalWord "foo/{}" -prop_readNormalWord7 = isOk readNormalWord "foo\\\nbar" -prop_readNormalWord8 = isWarning readSubshell "(foo\\ \nbar)" -prop_readNormalWord9 = isOk readSubshell "(foo\\ ;\nbar)" -prop_readNormalWord10 = isWarning readNormalWord "\x201Chello\x201D" -prop_readNormalWord11 = isWarning readNormalWord "\x2018hello\x2019" -prop_readNormalWord12 = isWarning readNormalWord "hello\x2018" -readNormalWord = readNormalishWord "" ["do", "done", "then", "fi", "esac"] - -readPatternWord = readNormalishWord "" ["esac"] - -readNormalishWord end terms = do - start <- startSpan - pos <- getPosition - x <- many1 (readNormalWordPart end) - id <- endSpan start - checkPossibleTermination pos x terms - return $ T_NormalWord id x - -readIndexSpan = do - start <- startSpan - x <- many (readNormalWordPart "]" <|> someSpace <|> otherLiteral) - id <- endSpan start - return $ T_NormalWord id x - where - someSpace = do - start <- startSpan - str <- spacing1 - id <- endSpan start - return $ T_Literal id str - otherLiteral = do - start <- startSpan - str <- many1 $ oneOf quotableChars - id <- endSpan start - return $ T_Literal id str - -checkPossibleTermination pos [T_Literal _ x] terminators = - when (x `elem` terminators) $ - parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)." -checkPossibleTermination _ _ _ = return () - -readNormalWordPart end = do - notFollowedBy2 $ oneOf end - checkForParenthesis - choice [ - readSingleQuoted, - readDoubleQuoted, - readGlob, - readNormalDollar, - readBraced, - readUnquotedBackTicked, - readProcSub, - readUnicodeQuote, - readNormalLiteral end, - readLiteralCurlyBraces - ] - where - checkForParenthesis = - return () `attempting` do - pos <- getPosition - lookAhead $ char '(' - parseProblemAt pos ErrorC 1036 "'(' is invalid here. Did you forget to escape it?" - - readLiteralCurlyBraces = do - start <- startSpan - str <- findParam <|> literalBraces - id <- endSpan start - return $ T_Literal id str - - findParam = try $ string "{}" - literalBraces = do - pos <- getPosition - c <- oneOf "{}" - parseProblemAt pos WarningC 1083 $ - "This " ++ [c] ++ " is literal. Check expression (missing ;/\\n?) or quote it." - return [c] - - -readSpacePart = do - start <- startSpan - x <- many1 whitespace - id <- endSpan start - return $ T_Literal id x - -readDollarBracedWord = do - start <- startSpan - list <- many readDollarBracedPart - id <- endSpan start - return $ T_NormalWord id list - -readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> - readParamSubSpecialChar <|> readExtglob <|> readNormalDollar <|> - readUnquotedBackTicked <|> readDollarBracedLiteral - -readDollarBracedLiteral = do - start <- startSpan - vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable - id <- endSpan start - return $ T_Literal id $ concat vars - -readParamSubSpecialChar = do - start <- startSpan - x <- many1 paramSubSpecialChars - id <- endSpan start - return $ T_ParamSubSpecialChar id x - -prop_readProcSub1 = isOk readProcSub "<(echo test | wc -l)" -prop_readProcSub2 = isOk readProcSub "<( if true; then true; fi )" -prop_readProcSub3 = isOk readProcSub "<( # nothing here \n)" -readProcSub = called "process substitution" $ do - start <- startSpan - dir <- try $ do - x <- oneOf "<>" - char '(' - return [x] - list <- readCompoundListOrEmpty - allspacing - char ')' - id <- endSpan start - return $ T_ProcSub id dir list - -prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'" -prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'" -prop_readSingleQuoted4 = isWarning readNormalWord "'it's" -prop_readSingleQuoted5 = isWarning readSimpleCommand "foo='bar\ncow 'arg" -prop_readSingleQuoted6 = isOk readSimpleCommand "foo='bar cow 'arg" -prop_readSingleQuoted7 = isOk readSingleQuoted "'foo\x201C\&bar'" -prop_readSingleQuoted8 = isWarning readSingleQuoted "'foo\x2018\&bar'" -readSingleQuoted = called "single quoted string" $ do - start <- startSpan - startPos <- getPosition - singleQuote - s <- many readSingleQuotedPart - let string = concat s - endPos <- getPosition - singleQuote <|> fail "Expected end of single quoted string" - - optional $ do - c <- try . lookAhead $ suspectCharAfterQuotes <|> oneOf "'" - if not (null string) && isAlpha c && isAlpha (last string) - then - parseProblemAt endPos WarningC 1011 - "This apostrophe terminated the single quoted string!" - else - when ('\n' `elem` string && not ("\n" `isPrefixOf` string)) $ - suggestForgotClosingQuote startPos endPos "single quoted string" - - id <- endSpan start - return (T_SingleQuoted id string) - -readSingleQuotedLiteral = do - singleQuote - strs <- many1 readSingleQuotedPart - singleQuote - return $ concat strs - -readSingleQuotedPart = - readSingleEscaped - <|> many1 (noneOf $ "'\\" ++ unicodeSingleQuotes) - <|> readUnicodeQuote - where - readUnicodeQuote = do - pos <- getPosition - x <- oneOf unicodeSingleQuotes - parseProblemAt pos WarningC 1112 - "This is a unicode quote. Delete and retype it (or ignore/doublequote for literal)." - return [x] - - -prop_readBackTicked = isOk (readBackTicked False) "`ls *.mp3`" -prop_readBackTicked2 = isOk (readBackTicked False) "`grep \"\\\"\"`" -prop_readBackTicked3 = isWarning (readBackTicked False) "´grep \"\\\"\"´" -prop_readBackTicked4 = isOk readSimpleCommand "`echo foo\necho bar`" -prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar" -prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar" -prop_readBackTicked7 = isOk readSimpleCommand "`#inline comment`" -prop_readBackTicked8 = isOk readSimpleCommand "echo `#comment` \\\nbar baz" -readQuotedBackTicked = readBackTicked True -readUnquotedBackTicked = readBackTicked False -readBackTicked quoted = called "backtick expansion" $ do - start <- startSpan - startPos <- getPosition - backtick - subStart <- getPosition - subString <- readGenericLiteral "`´" - endPos <- getPosition - backtick - id <- endSpan start - - optional $ do - c <- try . lookAhead $ suspectCharAfterQuotes - when ('\n' `elem` subString && not ("\n" `isPrefixOf` subString)) $ - suggestForgotClosingQuote startPos endPos "backtick expansion" - - -- Result positions may be off due to escapes - result <- subParse subStart (tryWithErrors subParser <|> return []) (unEscape subString) - return $ T_Backticked id result - where - unEscape [] = [] - unEscape ('\\':'"':rest) | quoted = '"' : unEscape rest - unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest - unEscape ('\\':'\n':rest) = unEscape rest - unEscape (c:rest) = c : unEscape rest - subParser = do - cmds <- readCompoundListOrEmpty - verifyEof - return cmds - backtick = - void (char '`') <|> do - pos <- getPosition - char '´' - parseProblemAt pos ErrorC 1077 - "For command expansion, the tick should slant left (` vs ´). Use $(..) instead." - --- Run a parser on a new input, such as for `..` or here documents. -subParse pos parser input = do - lastPosition <- getPosition - lastInput <- getInput - setPosition pos - setInput input - result <- parser - setInput lastInput - setPosition lastPosition - return result - --- Parse something, but forget all parseProblems -inSeparateContext = parseForgettingContext True --- Parse something, but forget all parseProblems on failure -forgetOnFailure = parseForgettingContext False - -parseForgettingContext alsoOnSuccess parser = do - context <- Ms.get - success context <|> failure context - where - success c = do - res <- try parser - when alsoOnSuccess $ Ms.put c - return res - failure c = do - Ms.put c - fail "" - -prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\"" -prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\"" -prop_readDoubleQuoted3 = isOk readDoubleQuoted "\"\x2018hello\x2019\"" -prop_readDoubleQuoted4 = isWarning readSimpleCommand "\"foo\nbar\"foo" -prop_readDoubleQuoted5 = isOk readSimpleCommand "lol \"foo\nbar\" etc" -prop_readDoubleQuoted6 = isOk readSimpleCommand "echo \"${ ls; }\"" -prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\"" -prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\"" -prop_readDoubleQuoted10 = isOk readDoubleQuoted "\"foo\\\\n\"" -readDoubleQuoted = called "double quoted string" $ do - start <- startSpan - startPos <- getPosition - doubleQuote - x <- many doubleQuotedPart - endPos <- getPosition - doubleQuote <|> fail "Expected end of double quoted string" - id <- endSpan start - optional $ do - try . lookAhead $ suspectCharAfterQuotes <|> oneOf "$\"" - when (any hasLineFeed x && not (startsWithLineFeed x)) $ - suggestForgotClosingQuote startPos endPos "double quoted string" - return $ T_DoubleQuoted id x - where - startsWithLineFeed (T_Literal _ ('\n':_):_) = True - startsWithLineFeed _ = False - hasLineFeed (T_Literal _ str) | '\n' `elem` str = True - hasLineFeed _ = False - -suggestForgotClosingQuote startPos endPos name = do - parseProblemAt startPos WarningC 1078 $ - "Did you forget to close this " ++ name ++ "?" - parseProblemAt endPos InfoC 1079 - "This is actually an end quote, but due to next char it looks suspect." - -doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBackTicked <|> readUnicodeQuote - where - readUnicodeQuote = do - pos <- getPosition - start <- startSpan - c <- oneOf unicodeDoubleQuotes - id <- endSpan start - parseProblemAt pos WarningC 1111 - "This is a unicode quote. Delete and retype it (or ignore/singlequote for literal)." - return $ T_Literal id [c] - -readDoubleLiteral = do - start <- startSpan - s <- many1 readDoubleLiteralPart - id <- endSpan start - return $ T_Literal id (concat s) - -readDoubleLiteralPart = do - x <- many1 (readDoubleEscaped <|> many1 (noneOf (doubleQuotableChars ++ unicodeDoubleQuotes))) - return $ concat x - -readNormalLiteral end = do - start <- startSpan - s <- many1 (readNormalLiteralPart end) - id <- endSpan start - return $ T_Literal id (concat s) - -prop_readGlob1 = isOk readGlob "*" -prop_readGlob2 = isOk readGlob "[^0-9]" -prop_readGlob3 = isOk readGlob "[a[:alpha:]]" -prop_readGlob4 = isOk readGlob "[[:alnum:]]" -prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]" -prop_readGlob6 = isOk readGlob "[\\|]" -prop_readGlob7 = isOk readGlob "[^[]" -prop_readGlob8 = isOk readGlob "[*?]" -prop_readGlob9 = isOk readGlob "[!]^]" -prop_readGlob10 = isOk readGlob "[]]" -readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral - where - readSimple = do - start <- startSpan - c <- oneOf "*?" - id <- endSpan start - return $ T_Glob id [c] - readClass = try $ do - start <- startSpan - char '[' - negation <- charToString (oneOf "!^") <|> return "" - leadingBracket <- charToString (oneOf "]") <|> return "" - s <- many (predefined <|> readNormalLiteralPart "]" <|> globchars) - guard $ not (null leadingBracket) || not (null s) - char ']' - id <- endSpan start - return $ T_Glob id $ "[" ++ concat (negation:leadingBracket:s) ++ "]" - where - globchars = charToString $ oneOf $ "![" ++ extglobStartChars - predefined = do - try $ string "[:" - s <- many1 letter - string ":]" - return $ "[:" ++ s ++ ":]" - - charToString = fmap return - readGlobbyLiteral = do - start <- startSpan - c <- extglobStart <|> char '[' - id <- endSpan start - return $ T_Literal id [c] - -readNormalLiteralPart customEnd = - readNormalEscaped <|> - many1 (noneOf (customEnd ++ standardEnd)) - where - standardEnd = "[{}" - ++ quotableChars - ++ extglobStartChars - ++ unicodeDoubleQuotes - ++ unicodeSingleQuotes - -readNormalEscaped = called "escaped char" $ do - pos <- getPosition - backslash - do - next <- quotable <|> oneOf "?*@!+[]{}.,~#" - when (next == ' ') $ checkTrailingSpaces pos <|> return () - -- Check if this line is followed by a commented line with a trailing backslash - when (next == '\n') $ try . lookAhead $ void spacing - return $ if next == '\n' then "" else [next] - <|> - do - next <- anyChar - case escapedChar next of - Just name -> parseNoteAt pos WarningC 1012 $ "\\" ++ [next] ++ " is just literal '" ++ [next] ++ "' here. For " ++ name ++ ", use " ++ alternative next ++ " instead." - Nothing -> parseNoteAt pos InfoC 1001 $ "This \\" ++ [next] ++ " will be a regular '" ++ [next] ++ "' in this context." - return [next] - where - alternative 'n' = "a quoted, literal line feed" - alternative t = "\"$(printf '\\" ++ [t] ++ "')\"" - escapedChar 'n' = Just "line feed" - escapedChar 't' = Just "tab" - escapedChar 'r' = Just "carriage return" - escapedChar _ = Nothing - - checkTrailingSpaces pos = lookAhead . try $ do - many linewhitespace - void linefeed <|> eof - parseProblemAt pos ErrorC 1101 "Delete trailing spaces after \\ to break line (or use quotes for literal space)." - - -prop_readExtglob1 = isOk readExtglob "!(*.mp3)" -prop_readExtglob2 = isOk readExtglob "!(*.mp3|*.wmv)" -prop_readExtglob4 = isOk readExtglob "+(foo \\) bar)" -prop_readExtglob5 = isOk readExtglob "+(!(foo *(bar)))" -prop_readExtglob6 = isOk readExtglob "*(((||))|())" -prop_readExtglob7 = isOk readExtglob "*(<>)" -prop_readExtglob8 = isOk readExtglob "@(|*())" -readExtglob = called "extglob" $ do - start <- startSpan - c <- try $ do - f <- extglobStart - char '(' - return f - contents <- readExtglobPart `sepBy` char '|' - char ')' - id <- endSpan start - return $ T_Extglob id [c] contents - -readExtglobPart = do - start <- startSpan - x <- many (readExtglobGroup <|> readNormalWordPart "" <|> readSpacePart <|> readExtglobLiteral) - id <- endSpan start - return $ T_NormalWord id x - where - readExtglobGroup = do - char '(' - start <- startSpan - contents <- readExtglobPart `sepBy` char '|' - id <- endSpan start - char ')' - return $ T_Extglob id "" contents - readExtglobLiteral = do - start <- startSpan - str <- many1 (oneOf "<>#;&") - id <- endSpan start - return $ T_Literal id str - - -readSingleEscaped = do - pos <- getPosition - s <- backslash - x <- lookAhead anyChar - - case x of - '\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'."; - _ -> return () - - return [s] - -readDoubleEscaped = do - pos <- getPosition - bs <- backslash - (linefeed >> return "") - <|> fmap return doubleQuotable - <|> do - c <- anyChar - -- This is an invalid escape sequence where the \ is literal. - -- Previously this caused a SC1117, which may be re-enabled as - -- as a pedantic warning. - return [bs, c] - -readBraceEscaped = do - bs <- backslash - (linefeed >> return "") - <|> fmap return bracedQuotable - <|> fmap (\ x -> [bs, x]) anyChar - - -readGenericLiteral endChars = do - strings <- many (readGenericEscaped <|> many1 (noneOf ('\\':endChars))) - return $ concat strings - -readGenericLiteral1 endExp = do - strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp - return $ concat strings - -readGenericEscaped = do - backslash - x <- anyChar - return $ if x == '\n' then [] else ['\\', x] - -prop_readBraced = isOk readBraced "{1..4}" -prop_readBraced2 = isOk readBraced "{foo,bar,\"baz lol\"}" -prop_readBraced3 = isOk readBraced "{1,\\},2}" -prop_readBraced4 = isOk readBraced "{1,{2,3}}" -prop_readBraced5 = isOk readBraced "{JP{,E}G,jp{,e}g}" -prop_readBraced6 = isOk readBraced "{foo,bar,$((${var}))}" -prop_readBraced7 = isNotOk readBraced "{}" -prop_readBraced8 = isNotOk readBraced "{foo}" -readBraced = try braceExpansion - where - braceExpansion = - T_BraceExpansion `withParser` do - char '{' - elements <- bracedElement `sepBy1` char ',' - guard $ - case elements of - (_:_:_) -> True - [t] -> ".." `isInfixOf` onlyLiteralString t - [] -> False - char '}' - return elements - bracedElement = - T_NormalWord `withParser` do - many $ choice [ - braceExpansion, - readDollarExpression, - readSingleQuoted, - readDoubleQuoted, - braceLiteral - ] - braceLiteral = - T_Literal `withParser` readGenericLiteral1 (oneOf "{}\"$'," <|> whitespace) - -ensureDollar = - -- The grammar should have been designed along the lines of readDollarExpr = char '$' >> stuff, but - -- instead, each subunit parses its own $. This results in ~7 1-3 char lookaheads instead of one 1-char. - -- Instead of optimizing the grammar, here's a green cut that decreases shellcheck runtime by 10%: - lookAhead $ char '$' - -readNormalDollar = do - ensureDollar - readDollarExp <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely False -readDoubleQuotedDollar = do - ensureDollar - readDollarExp <|> readDollarLonely True - - -prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))" -prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)" -prop_readDollarExpression3 = isWarning readDollarExpression "$((\"$@\" &); foo;)" -readDollarExpression :: Monad m => SCParser m Token -readDollarExpression = do - ensureDollar - readDollarExp - -readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable - where - arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos -> - parseNoteAt pos ErrorC 1102 "Shells disambiguate $(( differently or not at all. For $(command substitution), add space after $( . For $((arithmetics)), fix parsing errors.") - -prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'" -readDollarSingleQuote = called "$'..' expression" $ do - start <- startSpan - try $ string "$'" - str <- readGenericLiteral "'" - char '\'' - id <- endSpan start - return $ T_DollarSingleQuoted id str - -prop_readDollarDoubleQuote = isOk readDollarDoubleQuote "$\"hello\"" -readDollarDoubleQuote = do - lookAhead . try $ string "$\"" - start <- startSpan - char '$' - doubleQuote - x <- many doubleQuotedPart - doubleQuote <|> fail "Expected end of translated double quoted string" - id <- endSpan start - return $ T_DollarDoubleQuoted id x - -prop_readDollarArithmetic = isOk readDollarArithmetic "$(( 3 * 4 +5))" -prop_readDollarArithmetic2 = isOk readDollarArithmetic "$(((3*4)+(1*2+(3-1))))" -readDollarArithmetic = called "$((..)) expression" $ do - start <- startSpan - try (string "$((") - c <- readArithmeticContents - pos <- getPosition - char ')' - char ')' <|> fail "Expected a double )) to end the $((..))" - id <- endSpan start - return (T_DollarArithmetic id c) - -readDollarBracket = called "$[..] expression" $ do - start <- startSpan - try (string "$[") - c <- readArithmeticContents - string "]" - id <- endSpan start - return (T_DollarBracket id c) - -prop_readArithmeticExpression = isOk readArithmeticExpression "((a?b:c))" -readArithmeticExpression = called "((..)) command" $ do - start <- startSpan - try (string "((") - c <- readArithmeticContents - string "))" - id <- endSpan start - spacing - return (T_Arithmetic id c) - --- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used -readAmbiguous :: Monad m => String -> SCParser m p -> SCParser m p -> (SourcePos -> SCParser m ()) -> SCParser m p -readAmbiguous prefix expected alternative warner = do - pos <- getPosition - try . lookAhead $ string prefix - -- If the expected parser fails, try the alt. - -- If the alt fails, run the expected one again for the errors. - try expected <|> try (withAlt pos) <|> expected - where - withAlt pos = do - t <- forgetOnFailure alternative - warner pos - return t - -prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }" -prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}" -prop_readDollarBraceCommandExpansion3 = isOk readDollarBraceCommandExpansion "${| REPLY=42; }" -readDollarBraceCommandExpansion = called "ksh-style ${ ..; } command expansion" $ do - start <- startSpan - c <- try $ do - string "${" - char '|' <|> whitespace - allspacing - term <- readTerm - char '}' <|> fail "Expected } to end the ksh-style ${ ..; } command expansion" - id <- endSpan start - return $ T_DollarBraceCommandExpansion id (if c == '|' then Piped else Unpiped) term - -prop_readDollarBraced1 = isOk readDollarBraced "${foo//bar/baz}" -prop_readDollarBraced2 = isOk readDollarBraced "${foo/'{cow}'}" -prop_readDollarBraced3 = isOk readDollarBraced "${foo%%$(echo cow\\})}" -prop_readDollarBraced4 = isOk readDollarBraced "${foo#\\}}" -readDollarBraced = called "parameter expansion" $ do - start <- startSpan - try (string "${") - word <- readDollarBracedWord - char '}' - id <- endSpan start - return $ T_DollarBraced id True word - -prop_readDollarExpansion1 = isOk readDollarExpansion "$(echo foo; ls\n)" -prop_readDollarExpansion2 = isOk readDollarExpansion "$( )" -prop_readDollarExpansion3 = isOk readDollarExpansion "$( command \n#comment \n)" -readDollarExpansion = called "command expansion" $ do - start <- startSpan - try (string "$(") - cmds <- readCompoundListOrEmpty - char ')' <|> fail "Expected end of $(..) expression" - id <- endSpan start - return $ T_DollarExpansion id cmds - -prop_readDollarVariable = isOk readDollarVariable "$@" -prop_readDollarVariable2 = isOk (readDollarVariable >> anyChar) "$?!" -prop_readDollarVariable3 = isWarning (readDollarVariable >> anyChar) "$10" -prop_readDollarVariable4 = isWarning (readDollarVariable >> string "[@]") "$arr[@]" -prop_readDollarVariable5 = isWarning (readDollarVariable >> string "[f") "$arr[f" - -readDollarVariable :: Monad m => SCParser m Token -readDollarVariable = do - start <- startSpan - pos <- getPosition - - let singleCharred p = do - value <- wrapString ((:[]) <$> p) - id <- endSpan start - return $ (T_DollarBraced id False value) - - let positional = do - value <- singleCharred digit - return value `attempting` do - lookAhead digit - parseNoteAt pos ErrorC 1037 "Braces are required for positionals over 9, e.g. ${10}." - - let special = singleCharred specialVariable - - let regular = do - value <- wrapString readVariableName - id <- endSpan start - return (T_DollarBraced id False value) `attempting` do - lookAhead $ char '[' - parseNoteAt pos ErrorC 1087 "Use braces when expanding arrays, e.g. ${array[idx]} (or ${var}[.. to quiet)." - - try $ char '$' >> (positional <|> special <|> regular) - - where - wrapString p = do - start <- getPosition - s <- p - end <- getPosition - id1 <- getNextIdBetween start end - id2 <- getNextIdBetween start end - return $ T_NormalWord id1 [T_Literal id2 s] - -readVariableName = do - f <- variableStart - rest <- many variableChars - return (f:rest) - - -prop_readDollarLonely1 = isWarning readNormalWord "\"$\"var" -prop_readDollarLonely2 = isWarning readNormalWord "\"$\"\"var\"" -prop_readDollarLonely3 = isOk readNormalWord "\"$\"$var" -prop_readDollarLonely4 = isOk readNormalWord "\"$\"*" -prop_readDollarLonely5 = isOk readNormalWord "$\"str\"" -readDollarLonely quoted = do - start <- startSpan - char '$' - id <- endSpan start - when quoted $ do - isHack <- quoteForEscape - when isHack $ - parseProblemAtId id StyleC 1135 - "Prefer escape over ending quote to make $ literal. Instead of \"It costs $\"5, use \"It costs \\$5\"." - return $ T_Literal id "$" - where - quoteForEscape = option False $ try . lookAhead $ do - char '"' - -- Check for "foo $""bar" - optional $ char '"' - c <- anyVar - -- Don't trigger on [[ x == "$"* ]] or "$"$pattern - return $ c `notElem` "*$" - anyVar = variableStart <|> digit <|> specialVariable - - -prop_readHereDoc = isOk readScript "cat << foo\nlol\ncow\nfoo" -prop_readHereDoc2 = isNotOk readScript "cat <<- EOF\n cow\n EOF" -prop_readHereDoc3 = isOk readScript "cat << foo\n$\"\nfoo" -prop_readHereDoc4 = isNotOk readScript "cat << foo\n`\nfoo" -prop_readHereDoc5 = isOk readScript "cat <<- !foo\nbar\n!foo" -prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar" -prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo" -prop_readHereDoc8 = isOk readScript "cat <>bar\netc\nfoo" -prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n" -prop_readHereDoc10 = isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n" -prop_readHereDoc11 = isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n" -prop_readHereDoc12 = isOk readScript "cat << foo|cat\nbar\nfoo" -prop_readHereDoc13 = isOk readScript "cat <<'#!'\nHello World\n#!\necho Done" -prop_readHereDoc14 = isWarning readScript "cat << foo\nbar\nfoo \n" -prop_readHereDoc15 = isWarning readScript "cat <> return Dashed) <|> return Undashed - sp <- spacing - optional $ do - try . lookAhead $ char '(' - let message = "Shells are space sensitive. Use '< <(cmd)', not '<<" ++ sp ++ "(cmd)'." - parseProblemAt pos ErrorC 1038 message - start <- startSpan - (quoted, endToken) <- readToken - hid <- endSpan start - - -- add empty tokens for now, read the rest in readPendingHereDocs - let doc = T_HereDoc hid dashed quoted endToken [] - addPendingHereDoc hid dashed quoted endToken - return doc - where - unquote :: String -> (Quoted, String) - unquote "" = (Unquoted, "") - unquote [c] = (Unquoted, [c]) - unquote s@(cl:tl) = - case reverse tl of - (cr:tr) | cr == cl && cl `elem` "\"'" -> (Quoted, reverse tr) - _ -> (if '\\' `elem` s then (Quoted, filter ((/=) '\\') s) else (Unquoted, s)) - -- Fun fact: bash considers << foo"" quoted, but not << <("foo"). - readToken = do - str <- readStringForParser readNormalWord - -- A here doc actually works with \r\n because the \r becomes part of the token - crstr <- (carriageReturn >> (return $ str ++ "\r")) <|> return str - return $ unquote crstr - -readPendingHereDocs = do - docs <- popPendingHereDocs - mapM_ readDoc docs - where - readDoc (HereDocPending id dashed quoted endToken ctx) = - swapContext ctx $ - do - docStartPos <- getPosition - (terminated, wasWarned, lines) <- readDocLines dashed endToken - docEndPos <- getPosition - let hereData = unlines lines - unless terminated $ do - unless wasWarned $ - debugHereDoc id endToken hereData - fail "Here document was not correctly terminated" - list <- parseHereData quoted (docStartPos, docEndPos) hereData - addToHereDocMap id list - - -- Read the lines making up the here doc. Returns (IsTerminated, Lines) - readDocLines :: Monad m => Dashed -> String -> SCParser m (Bool, Bool, [String]) - readDocLines dashed endToken = do - pos <- getPosition - str <- rawLine - isEof <- option False (eof >> return True) - (isEnd, wasWarned) <- subParse pos checkEnd str - if - | isEnd -> return (True, wasWarned, []) - | isEof -> return (False, wasWarned, [str]) - | True -> do - (ok, previousWarning, rest) <- readDocLines dashed endToken - return (ok, wasWarned || previousWarning, str:rest) - where - -- Check if this is the actual end, or a plausible false end - checkEnd = option (False, False) $ try $ do - -- Match what's basically '^( *)token( *)(.*)$' - leadingSpacePos <- getPosition - leadingSpace <- linewhitespace `reluctantlyTill` string endToken - string endToken - trailingSpacePos <- getPosition - trailingSpace <- many linewhitespace - trailerPos <- getPosition - trailer <- many anyChar - - let leadingSpacesAreTabs = all (== '\t') leadingSpace - let thereIsNoTrailer = null trailingSpace && null trailer - let leaderIsOk = null leadingSpace - || dashed == Dashed && leadingSpacesAreTabs - let trailerStart = case trailer of [] -> '\0'; (h:_) -> h - let hasTrailingSpace = not $ null trailingSpace - let hasTrailer = not $ null trailer - let ppt = parseProblemAt trailerPos ErrorC - - if leaderIsOk && thereIsNoTrailer - then return (True, False) - else do - let foundCause = return (False, True) - let skipLine = return (False, False) - -- This may be intended as an end token. Debug why it isn't. - if - | trailerStart == ')' -> do - ppt 1119 $ "Add a linefeed between end token and terminating ')'." - foundCause - | trailerStart == '#' -> do - ppt 1120 "No comments allowed after here-doc token. Comment the next line instead." - foundCause - | trailerStart `elem` ";>|&" -> do - ppt 1121 "Add ;/& terminators (and other syntax) on the line with the <<, not here." - foundCause - | hasTrailingSpace && hasTrailer -> do - ppt 1122 "Nothing allowed after end token. To continue a command, put it on the line with the <<." - foundCause - | leaderIsOk && hasTrailingSpace && not hasTrailer -> do - parseProblemAt trailingSpacePos ErrorC 1118 "Delete whitespace after the here-doc end token." - -- Parse as if it's the actual end token. Will koala_man regret this once again? - return (True, True) - | not hasTrailingSpace && hasTrailer -> - -- The end token is just a prefix - skipLine - | hasTrailer -> - error $ pleaseReport "unexpected heredoc trailer" - - -- The following cases assume no trailing text: - | dashed == Undashed && (not $ null leadingSpace) -> do - parseProblemAt leadingSpacePos ErrorC 1039 "Remove indentation before end token (or use <<- and indent with tabs)." - foundCause - | dashed == Dashed && not leadingSpacesAreTabs -> do - parseProblemAt leadingSpacePos ErrorC 1040 "When using <<-, you can only indent with tabs." - foundCause - | True -> skipLine - - rawLine = do - c <- many $ noneOf "\n" - void (char '\n') <|> eof - return c - - parseHereData Quoted (start,end) hereData = do - id <- getNextIdBetween start end - return [T_Literal id hereData] - - parseHereData Unquoted (startPos, _) hereData = - subParse startPos readHereData hereData - - readHereData = many $ doubleQuotedPart <|> readHereLiteral - - readHereLiteral = do - start <- startSpan - chars <- many1 $ noneOf "`$\\" - id <- endSpan start - return $ T_Literal id chars - - debugHereDoc tokenId endToken doc - | endToken `isInfixOf` doc = - let lookAt line = when (endToken `isInfixOf` line) $ - parseProblemAtId tokenId ErrorC 1042 ("Close matches include '" ++ (e4m line) ++ "' (!= '" ++ (e4m endToken) ++ "').") - in do - parseProblemAtId tokenId ErrorC 1041 ("Found '" ++ (e4m endToken) ++ "' further down, but not on a separate line.") - mapM_ lookAt (lines doc) - | map toLower endToken `isInfixOf` map toLower doc = - parseProblemAtId tokenId ErrorC 1043 ("Found " ++ (e4m endToken) ++ " further down, but with wrong casing.") - | otherwise = - parseProblemAtId tokenId ErrorC 1044 ("Couldn't find end token `" ++ (e4m endToken) ++ "' in the here document.") - - -readFilename = readNormalWord -readIoFileOp = choice [g_DGREAT, g_LESSGREAT, g_GREATAND, g_LESSAND, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ] - -readIoDuplicate = try $ do - start <- startSpan - op <- g_GREATAND <|> g_LESSAND - target <- readIoVariable <|> digitsAndOrDash - id <- endSpan start - return $ T_IoDuplicate id op target - where - -- either digits with optional dash, or a required dash - digitsAndOrDash = do - str <- many digit - dash <- (if null str then id else option "") $ string "-" - return $ str ++ dash - - -prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\"" -readIoFile = called "redirection" $ do - start <- startSpan - op <- readIoFileOp - spacing - file <- readFilename - id <- endSpan start - return $ T_IoFile id op file - -readIoVariable = try $ do - char '{' - x <- readVariableName - char '}' - return $ "{" ++ x ++ "}" - -readIoSource = try $ do - x <- string "&" <|> readIoVariable <|> many digit - lookAhead $ void readIoFileOp <|> void (string "<<") - return x - -prop_readIoRedirect = isOk readIoRedirect "3>&2" -prop_readIoRedirect2 = isOk readIoRedirect "2> lol" -prop_readIoRedirect3 = isOk readIoRedirect "4>&-" -prop_readIoRedirect4 = isOk readIoRedirect "&> lol" -prop_readIoRedirect5 = isOk readIoRedirect "{foo}>&2" -prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-" -prop_readIoRedirect7 = isOk readIoRedirect "{foo}>&1-" -readIoRedirect = do - start <- startSpan - n <- readIoSource - redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile - id <- endSpan start - skipAnnotationAndWarn - spacing - return $ T_FdRedirect id n redir - -prop_readHereString = isOk readHereString "<<< \"Hello $world\"" -readHereString = called "here string" $ do - start <- startSpan - try $ string "<<<" - id <- endSpan start - spacing - word <- readNormalWord - return $ T_HereString id word - -prop_readNewlineList1 = isOk readScript "&> /dev/null echo foo" -readNewlineList = - many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak - where - checkBadBreak = optional $ do - pos <- getPosition - try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or && - notFollowedBy2 (string "&>") -- Except &> or &>> which is valid - parseProblemAt pos ErrorC 1133 - "Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one." -readLineBreak = optional readNewlineList - -prop_readSeparator1 = isWarning readScript "a &; b" -prop_readSeparator2 = isOk readScript "a & b" -prop_readSeparator3 = isWarning readScript "a & b" -prop_readSeparator4 = isWarning readScript "a > file; b" -prop_readSeparator5 = isWarning readScript "curl https://example.com/?foo=moo&bar=cow" -readSeparatorOp = do - notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator) - notFollowedBy2 (string "&>") - start <- getPosition - f <- try (do - pos <- getPosition - char '&' - optional $ choice [ - do - s <- lookAhead . choice . map (try . string) $ - ["amp;", "gt;", "lt;"] - parseProblemAt pos ErrorC 1109 "This is an unquoted HTML entity. Replace with corresponding character.", - - do - try . lookAhead $ variableStart - parseProblemAt pos WarningC 1132 "This & terminates the command. Escape it or add space after & to silence." - ] - - spacing - pos <- getPosition - char ';' - -- In case statements we might have foo & ;; - notFollowedBy2 $ char ';' - parseProblemAt pos ErrorC 1045 "It's not 'foo &; bar', just 'foo & bar'." - return '&' - ) <|> char ';' <|> char '&' - end <- getPosition - spacing - return (f, (start, end)) - -readSequentialSep = void (g_Semi >> readLineBreak) <|> void readNewlineList -readSeparator = - do - separator <- readSeparatorOp - readLineBreak - return separator - <|> - do - start <- getPosition - readNewlineList - end <- getPosition - return ('\n', (start, end)) - -prop_readSimpleCommand = isOk readSimpleCommand "echo test > file" -prop_readSimpleCommand2 = isOk readSimpleCommand "cmd &> file" -prop_readSimpleCommand3 = isOk readSimpleCommand "export foo=(bar baz)" -prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)" -prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi" -prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )" -prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls" -prop_readSimpleCommand7b = isOk readSimpleCommand "\\:" -prop_readSimpleCommand8 = isWarning readSimpleCommand "// Lol" -prop_readSimpleCommand9 = isWarning readSimpleCommand "/* Lolbert */" -prop_readSimpleCommand10 = isWarning readSimpleCommand "/**** Lolbert */" -prop_readSimpleCommand11 = isOk readSimpleCommand "/\\* foo" -prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo" -prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo" -prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]" -prop_readSimpleCommand15 = isWarning readSimpleCommand "trap 'foo\"bar' INT" -readSimpleCommand = called "simple command" $ do - prefix <- option [] readCmdPrefix - skipAnnotationAndWarn - cmd <- option Nothing $ Just <$> readCmdName - when (null prefix && isNothing cmd) $ fail "Expected a command" - - case cmd of - Nothing -> do - id1 <- getNextIdSpanningTokenList prefix - id2 <- getNewIdFor id1 - return $ makeSimpleCommand id1 id2 prefix [] [] - - Just cmd -> do - validateCommand cmd - -- We have to ignore possible parsing problems from the lookAhead parser - firstArgument <- ignoreProblemsOf . optionMaybe . try . lookAhead $ readCmdWord - suffix <- option [] $ getParser readCmdSuffix - -- If `export` or other modifier commands are called with `builtin` we have to look at the first argument - (if isCommand ["builtin"] cmd then fromMaybe cmd firstArgument else cmd) [ - (["declare", "export", "local", "readonly", "typeset"], readModifierSuffix), - (["time"], readTimeSuffix), - (["let"], readLetSuffix), - (["eval"], readEvalSuffix) - ] - - id1 <- getNextIdSpanningTokenList (prefix ++ (cmd:suffix)) - id2 <- getNewIdFor id1 - - let result = makeSimpleCommand id1 id2 prefix [cmd] suffix - case () of - _ | isCommand ["source", "."] cmd -> readSource result - _ | isCommand ["trap"] cmd -> do - syntaxCheckTrap result - return result - _ -> return result - where - isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings - isCommand _ _ = False - getParser def cmd [] = def - getParser def cmd ((list, action):rest) = - if isCommand list cmd - then action - else getParser def cmd rest - - validateCommand cmd = - case cmd of - (T_NormalWord _ [T_Literal _ "//"]) -> commentWarning (getId cmd) - (T_NormalWord _ (T_Literal _ "/" : T_Glob _ "*" :_)) -> commentWarning (getId cmd) - (T_NormalWord _ (T_Literal _ str:_)) -> do - let cmdString = map toLower $ takeWhile isAlpha str - when (cmdString `elem` ["elsif", "elseif"]) $ - parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch." - _ -> return () - - syntaxCheckTrap cmd = - case cmd of - (T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:arg:_))) -> checkArg arg (getLiteralString arg) - _ -> return () - where - checkArg _ Nothing = return () - checkArg arg (Just ('-':_)) = return () - checkArg arg (Just str) = do - (start,end) <- getSpanForId (getId arg) - subParse start (tryWithErrors (readCompoundListOrEmpty >> verifyEof) <|> return ()) str - - commentWarning id = - parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh." - - makeSimpleCommand id1 id2 prefix cmd suffix = - let - (preAssigned, preRest) = partition assignment prefix - (preRedirected, preRest2) = partition redirection preRest - (postRedirected, postRest) = partition redirection suffix - - redirs = preRedirected ++ postRedirected - assigns = preAssigned - args = cmd ++ preRest2 ++ postRest - in - T_Redirecting id1 redirs $ T_SimpleCommand id2 assigns args - where - assignment (T_Assignment {}) = True - assignment _ = False - redirection (T_FdRedirect {}) = True - redirection _ = False - - -readSource :: Monad m => Token -> SCParser m Token -readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:args'))) = do - let file = getFile args' - override <- getSourceOverride - let literalFile = do - name <- override `mplus` (getLiteralString =<< file) `mplus` (stripDynamicPrefix =<< file) - -- Hack to avoid 'source ~/foo' trying to read from literal tilde - guard . not $ "~/" `isPrefixOf` name - return name - let fileId = fromMaybe (getId cmd) (getId <$> file) - case literalFile of - Nothing -> do - parseNoteAtId fileId WarningC 1090 - "ShellCheck can't follow non-constant source. Use a directive to specify location." - return t - Just filename -> do - proceed <- shouldFollow filename - if not proceed - then do - -- FIXME: This actually gets squashed without -a - parseNoteAtId fileId InfoC 1093 - "This file appears to be recursively sourced. Ignoring." - return t - else do - sys <- Mr.asks systemInterface - (input, resolvedFile) <- - if filename == "/dev/null" -- always allow /dev/null - then return (Right "", filename) - else do - allAnnotations <- getCurrentAnnotations True - currentScript <- Mr.asks currentFilename - let paths = mapMaybe getSourcePath allAnnotations - let externalSources = listToMaybe $ mapMaybe getExternalSources allAnnotations - resolved <- system $ siFindSource sys currentScript externalSources paths filename - contents <- system $ siReadFile sys externalSources resolved - return (contents, resolved) - case input of - Left err -> do - parseNoteAtId fileId InfoC 1091 $ - "Not following: " ++ err - return t - Right script -> do - id1 <- getNewIdFor cmdId - id2 <- getNewIdFor cmdId - - let included = do - src <- subRead resolvedFile script - return $ T_SourceCommand id1 t (T_Include id2 src) - - let failed = do - parseNoteAtId fileId WarningC 1094 - "Parsing of sourced file failed. Ignoring it." - return t - - included <|> failed - where - getFile :: [Token] -> Maybe Token - getFile (first:rest) = - case getLiteralString first of - Just "--" -> rest !!! 0 - Just "-p" -> rest !!! 1 - _ -> return first - getFile _ = Nothing - - getSourcePath t = - case t of - SourcePath x -> Just x - _ -> Nothing - - getExternalSources t = - case t of - ExternalSources b -> Just b - _ -> Nothing - - -- If the word has a single expansion as the directory, try stripping it - -- This affects `$foo/bar` but not `${foo}-dir/bar` or `/foo/$file` - stripDynamicPrefix word = - case getWordParts word of - exp : rest | isStringExpansion exp -> do - str <- getLiteralString (T_NormalWord (Id 0) rest) - guard $ "/" `isPrefixOf` str - return $ "." ++ str - _ -> Nothing - - subRead name script = - withContext (ContextSource name) $ - inSeparateContext $ do - oldState <- getState - setState $ oldState { pendingHereDocs = [] } - result <- subParse (initialPos name) (readScriptFile True) script - newState <- getState - setState $ newState { pendingHereDocs = pendingHereDocs oldState } - return result -readSource t = return t - - -prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu" -prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu" -prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat" -prop_readPipeline4 = isOk readPipeline "! ! true" -prop_readPipeline5 = isOk readPipeline "true | ! true" -readPipeline = do - unexpecting "keyword/token" readKeyword - readBanged readPipeSequence - -readBanged parser = do - pos <- getPosition - (T_Bang id) <- g_Bang - next <- readBanged parser - return $ T_Banged id next - <|> parser - -prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1" -prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo" -prop_readAndOr2 = isOk readAndOr "# shellcheck disable=1\n# lol\n# shellcheck disable=3\nfoo" -readAndOr = do - start <- startSpan - apos <- getPosition - annotations <- readAnnotations - aid <- endSpan start - - unless (null annotations) $ optional $ do - try . lookAhead $ readKeyword - parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches." - - andOr <- withAnnotations annotations $ - chainl1 readPipeline $ do - op <- g_AND_IF <|> g_OR_IF - readLineBreak - return $ case op of T_AND_IF id -> T_AndIf id - T_OR_IF id -> T_OrIf id - - return $ if null annotations - then andOr - else T_Annotation aid annotations andOr - -readTermOrNone = do - allspacing - readTerm <|> do - eof - return [] - -prop_readTerm = isOk readTerm "time ( foo; bar; )" -readTerm = do - allspacing - m <- readAndOr - readTerm' m - where - readTerm' current = - do - (sep, (start, end)) <- readSeparator - id <- getNextIdBetween start end - more <- option (T_EOF id) readAndOr - case more of (T_EOF _) -> return [transformWithSeparator id sep current] - _ -> do - list <- readTerm' more - return (transformWithSeparator id sep current : list) - <|> - return [current] - where - transformWithSeparator i '&' = T_Backgrounded i - transformWithSeparator i _ = id - - -readPipeSequence = do - start <- startSpan - (cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand) - (readPipe `thenSkip` (spacing >> readLineBreak)) - id <- endSpan start - spacing - return $ T_Pipeline id pipes cmds - where - sepBy1WithSeparators p s = do - let elems = (\x -> ([x], [])) <$> p - let seps = do - separator <- s - return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator]) - elems `chainl1` seps - -readPipe = do - notFollowedBy2 g_OR_IF - start <- startSpan - char '|' - qualifier <- string "&" <|> return "" - id <- endSpan start - spacing - return $ T_Pipe id ('|':qualifier) - -readCommand = choice [ - readCompoundCommand, - readConditionCommand, - readCoProc, - readSimpleCommand - ] - -readCmdName = do - -- If the command name is `!` then - optional . lookAhead . try $ do - char '!' - whitespace - -- Ignore alias suppression - optional . try $ do - char '\\' - lookAhead $ variableChars <|> oneOf ":." - readCmdWord - -readCmdWord = do - skipAnnotationAndWarn - readNormalWord <* spacing - --- Due to poor planning, annotations after commands isn't handled well. --- At the time this function is used, it's usually too late to skip --- comments, so you end up with a parse failure instead. -skipAnnotationAndWarn = optional $ do - try . lookAhead $ readAnnotationPrefix - parseProblem ErrorC 1126 "Place shellcheck directives before commands, not after." - readAnyComment - -prop_readIfClause = isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi" -prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi" -prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi" -prop_readIfClause4 = isWarning readIfClause "if false; then true; else if true; then echo lol; fi; fi" -prop_readIfClause5 = isOk readIfClause "if false; then true; else\nif true; then echo lol; fi; fi" -prop_readIfClause6 = isWarning readIfClause "if true\nthen\nDo the thing\nfi" -readIfClause = called "if expression" $ do - start <- startSpan - pos <- getPosition - (condition, action) <- readIfPart - elifs <- many readElifPart - elses <- option [] readElsePart - - g_Fi `orFail` do - parseProblemAt pos ErrorC 1046 "Couldn't find 'fi' for this 'if'." - parseProblem ErrorC 1047 "Expected 'fi' matching previously mentioned 'if'." - return "Expected 'fi'" - id <- endSpan start - - return $ T_IfExpression id ((condition, action):elifs) elses - - -verifyNotEmptyIf s = - optional (do - emptyPos <- getPosition - try . lookAhead $ (g_Fi <|> g_Elif <|> g_Else) - parseProblemAt emptyPos ErrorC 1048 $ "Can't have empty " ++ s ++ " clauses (use 'true' as a no-op).") -readIfPart = do - pos <- getPosition - g_If - allspacing - condition <- readTerm - - ifNextToken (g_Fi <|> g_Elif <|> g_Else) $ - parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'if'?" - - called "then clause" $ do - g_Then `orFail` do - parseProblem ErrorC 1050 "Expected 'then'." - return "Expected 'then'" - - acceptButWarn g_Semi ErrorC 1051 "Semicolons directly after 'then' are not allowed. Just remove it." - allspacing - verifyNotEmptyIf "then" - - action <- readTerm - return (condition, action) - -readElifPart = called "elif clause" $ do - pos <- getPosition - g_Elif - allspacing - condition <- readTerm - ifNextToken (g_Fi <|> g_Elif <|> g_Else) $ - parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?" - - g_Then - acceptButWarn g_Semi ErrorC 1052 "Semicolons directly after 'then' are not allowed. Just remove it." - allspacing - verifyNotEmptyIf "then" - action <- readTerm - return (condition, action) - -readElsePart = called "else clause" $ do - pos <- getPosition - g_Else - optional $ do - try . lookAhead $ g_If - parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)." - - acceptButWarn g_Semi ErrorC 1053 "Semicolons directly after 'else' are not allowed. Just remove it." - allspacing - verifyNotEmptyIf "else" - readTerm - -ifNextToken parser action = - optional $ do - try . lookAhead $ parser - action - -prop_readSubshell = isOk readSubshell "( cd /foo; tar cf stuff.tar * )" -readSubshell = called "explicit subshell" $ do - start <- startSpan - char '(' - allspacing - list <- readCompoundList - allspacing - char ')' <|> fail "Expected ) closing the subshell" - id <- endSpan start - spacing - return $ T_Subshell id list - -prop_readBraceGroup = isOk readBraceGroup "{ a; b | c | d; e; }" -prop_readBraceGroup2 = isWarning readBraceGroup "{foo;}" -prop_readBraceGroup3 = isOk readBraceGroup "{(foo)}" -readBraceGroup = called "brace group" $ do - start <- startSpan - char '{' - void allspacingOrFail <|> optional (do - lookAhead $ noneOf "(" -- {( is legal - parseProblem ErrorC 1054 "You need a space after the '{'.") - optional $ do - pos <- getPosition - lookAhead $ char '}' - parseProblemAt pos ErrorC 1055 "You need at least one command here. Use 'true;' as a no-op." - list <- readTerm - char '}' <|> do - parseProblem ErrorC 1056 "Expected a '}'. If you have one, try a ; or \\n in front of it." - fail "Missing '}'" - id <- endSpan start - spacing - return $ T_BraceGroup id list - -prop_readBatsTest1 = isOk readBatsTest "@test 'can parse' {\n true\n}" -prop_readBatsTest2 = isOk readBatsTest "@test random text !(@*$Y&! {\n true\n}" -prop_readBatsTest3 = isOk readBatsTest "@test foo { bar { baz {\n true\n}" -prop_readBatsTest4 = isNotOk readBatsTest "@test foo \n{\n true\n}" -readBatsTest = called "bats @test" $ do - start <- startSpan - try $ string "@test " - spacing - name <- readBatsName - spacing - test <- readBraceGroup - id <- endSpan start - return $ T_BatsTest id name test - where - readBatsName = do - line <- try . lookAhead $ many1 $ noneOf "\n" - let name = reverse $ f $ reverse line - string name - - -- We want everything before the last " {" in a string, so we find everything after "{ " in its reverse - f ('{':' ':rest) = dropWhile isSpace rest - f (a:rest) = f rest - f [] = "" - -prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done" -readWhileClause = called "while loop" $ do - start <- startSpan - kwId <- getId <$> g_While - condition <- readTerm - statements <- readDoGroup kwId - id <- endSpan start - return $ T_WhileExpression id condition statements - -prop_readUntilClause = isOk readUntilClause "until kill -0 $PID; do sleep 1; done" -readUntilClause = called "until loop" $ do - start <- startSpan - kwId <- getId <$> g_Until - condition <- readTerm - statements <- readDoGroup kwId - id <- endSpan start - return $ T_UntilExpression id condition statements - -readDoGroup kwId = do - optional (do - try . lookAhead $ g_Done - parseProblemAtId kwId ErrorC 1057 "Did you forget the 'do' for this loop?") - - doKw <- g_Do `orFail` do - parseProblem ErrorC 1058 "Expected 'do'." - return "Expected 'do'" - - acceptButWarn g_Semi ErrorC 1059 "Semicolon is not allowed directly after 'do'. You can just delete it." - allspacing - - optional (do - try . lookAhead $ g_Done - parseProblemAtId (getId doKw) ErrorC 1060 "Can't have empty do clauses (use 'true' as a no-op).") - - commands <- readCompoundList - g_Done `orFail` do - parseProblemAtId (getId doKw) ErrorC 1061 "Couldn't find 'done' for this 'do'." - parseProblem ErrorC 1062 "Expected 'done' matching previously mentioned 'do'." - return "Expected 'done'" - - optional . lookAhead $ do - pos <- getPosition - try $ string "<(" - parseProblemAt pos ErrorC 1142 "Use 'done < <(cmd)' to redirect from process substitution (currently missing one '<')." - return commands - - -prop_readForClause = isOk readForClause "for f in *; do rm \"$f\"; done" -prop_readForClause1 = isOk readForClause "for f in *; { rm \"$f\"; }" -prop_readForClause3 = isOk readForClause "for f; do foo; done" -prop_readForClause4 = isOk readForClause "for((i=0; i<10; i++)); do echo $i; done" -prop_readForClause5 = isOk readForClause "for ((i=0;i<10 && n>x;i++,--n))\ndo \necho $i\ndone" -prop_readForClause6 = isOk readForClause "for ((;;))\ndo echo $i\ndone" -prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone" -prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone" -prop_readForClause9 = isOk readForClause "for i do true; done" -prop_readForClause10 = isOk readForClause "for ((;;)) { true; }" -prop_readForClause12 = isWarning readForClause "for $a in *; do echo \"$a\"; done" -prop_readForClause13 = isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done" -readForClause = called "for loop" $ do - pos <- getPosition - (T_For id) <- g_For - spacing - readArithmetic id <|> readRegular id - where - readArithmetic id = called "arithmetic for condition" $ do - readArithmeticDelimiter '(' "Missing second '(' to start arithmetic for ((;;)) loop" - x <- readArithmeticContents - char ';' >> spacing - y <- readArithmeticContents - char ';' >> spacing - z <- readArithmeticContents - spacing - readArithmeticDelimiter ')' "Missing second ')' to terminate 'for ((;;))' loop condition" - spacing - optional $ readSequentialSep >> spacing - group <- readBraced <|> readDoGroup id - return $ T_ForArithmetic id x y z group - - -- For c='(' read "((" and be lenient about spaces - readArithmeticDelimiter c msg = do - char c - startPos <- getPosition - sp <- spacing - endPos <- getPosition - char c <|> do - parseProblemAt startPos ErrorC 1137 msg - fail "" - unless (null sp) $ - parseProblemAtWithEnd startPos endPos ErrorC 1138 $ "Remove spaces between " ++ [c,c] ++ " in arithmetic for loop." - - readBraced = do - (T_BraceGroup _ list) <- readBraceGroup - return list - - readRegular id = do - acceptButWarn (char '$') ErrorC 1086 - "Don't use $ on the iterator name in for loops." - name <- readVariableName `thenSkip` allspacing - values <- readInClause <|> (optional readSequentialSep >> return []) - group <- readBraced <|> readDoGroup id - return $ T_ForIn id name values group - -prop_readSelectClause1 = isOk readSelectClause "select foo in *; do echo $foo; done" -prop_readSelectClause2 = isOk readSelectClause "select foo; do echo $foo; done" -readSelectClause = called "select loop" $ do - (T_Select id) <- g_Select - spacing - typ <- readRegular - group <- readDoGroup id - typ id group - where - readRegular = do - name <- readVariableName - spacing - values <- readInClause <|> (readSequentialSep >> return []) - return $ \id group -> (return $ T_SelectIn id name values group) - -readInClause = do - g_In - things <- readCmdWord `reluctantlyTill` - (void g_Semi <|> void linefeed <|> void g_Do) - - do { - lookAhead g_Do; - parseNote ErrorC 1063 "You need a line feed or semicolon before the 'do'."; - } <|> do { - optional g_Semi; - void allspacing; - } - - return things - -prop_readCaseClause = isOk readCaseClause "case foo in a ) lol; cow;; b|d) fooo; esac" -prop_readCaseClause2 = isOk readCaseClause "case foo\n in * ) echo bar;; esac" -prop_readCaseClause3 = isOk readCaseClause "case foo\n in * ) echo bar & ;; esac" -prop_readCaseClause4 = isOk readCaseClause "case foo\n in *) echo bar ;& bar) foo; esac" -prop_readCaseClause5 = isOk readCaseClause "case foo\n in *) echo bar;;& foo) baz;; esac" -prop_readCaseClause6 = isOk readCaseClause "case foo\n in if) :;; done) :;; esac" -readCaseClause = called "case expression" $ do - start <- startSpan - g_Case - word <- readNormalWord - allspacing - g_In <|> fail "Expected 'in'" - readLineBreak - list <- readCaseList - g_Esac <|> fail "Expected 'esac' to close the case statement" - id <- endSpan start - return $ T_CaseExpression id word list - -readCaseList = many readCaseItem - -readCaseItem = called "case item" $ do - notFollowedBy2 g_Esac - optional $ do - try . lookAhead $ readAnnotationPrefix - parseProblem ErrorC 1124 "ShellCheck directives are only valid in front of complete commands like 'case' statements, not individual case branches." - optional g_Lparen - spacing - pattern' <- readPattern - void g_Rparen <|> do - parseProblem ErrorC 1085 - "Did you forget to move the ;; after extending this case item?" - fail "Expected ) to open a new case item" - readLineBreak - list <- (lookAhead readCaseSeparator >> return []) <|> readCompoundList - separator <- readCaseSeparator `attempting` do - pos <- getPosition - lookAhead g_Rparen - parseProblemAt pos ErrorC 1074 - "Did you forget the ;; after the previous case item?" - readLineBreak - return (separator, pattern', list) - -readCaseSeparator = choice [ - tryToken ";;&" (const ()) >> return CaseContinue, - tryToken ";&" (const ()) >> return CaseFallThrough, - g_DSEMI >> return CaseBreak, - lookAhead (readLineBreak >> g_Esac) >> return CaseBreak - ] - -prop_readFunctionDefinition = isOk readFunctionDefinition "foo() { command foo --lol \"$@\"; }" -prop_readFunctionDefinition1 = isOk readFunctionDefinition "foo (){ command foo --lol \"$@\"; }" -prop_readFunctionDefinition4 = isWarning readFunctionDefinition "foo(a, b) { true; }" -prop_readFunctionDefinition5 = isOk readFunctionDefinition ":(){ :|:;}" -prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }" -prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }" -prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)" -prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }" -prop_readFunctionDefinition10 = isOk readFunctionDefinition "function foo () { true; }" -prop_readFunctionDefinition11 = isWarning readFunctionDefinition "function foo{\ntrue\n}" -prop_readFunctionDefinition12 = isOk readFunctionDefinition "function []!() { true; }" -prop_readFunctionDefinition13 = isOk readFunctionDefinition "@require(){ true; }" -prop_readFunctionDefinition14 = isOk readFunctionDefinition "foo#bar(){ :; }" -prop_readFunctionDefinition15 = isNotOk readFunctionDefinition "#bar(){ :; }" -readFunctionDefinition = called "function" $ do - start <- startSpan - functionSignature <- try readFunctionSignature - allspacing - void (lookAhead $ oneOf "{(") <|> parseProblem ErrorC 1064 "Expected a { to open the function definition." - group <- readBraceGroup <|> readSubshell - id <- endSpan start - return $ functionSignature id group - where - readFunctionSignature = - readWithFunction <|> readWithoutFunction - where - readWithFunction = do - try $ do - string "function" - whitespace - spacing - name <- (:) <$> extendedFunctionStartChars <*> many extendedFunctionChars - spaces <- spacing - hasParens <- wasIncluded readParens - when (not hasParens && null spaces) $ - acceptButWarn (lookAhead (oneOf "{(")) - ErrorC 1095 "You need a space or linefeed between the function name and body." - return $ \id -> T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name - - readWithoutFunction = try $ do - name <- (:) <$> functionStartChars <*> many functionChars - guard $ name /= "time" -- Interferes with time ( foo ) - spacing - readParens - return $ \id -> T_Function id (FunctionKeyword False) (FunctionParentheses True) name - - readParens = do - g_Lparen - spacing - g_Rparen <|> do - parseProblem ErrorC 1065 "Trying to declare parameters? Don't. Use () and refer to params as $1, $2.." - many $ noneOf "\n){" - g_Rparen - return () - -prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }" -prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }" -prop_readCoProc3 = isOk readCoProc "coproc echo bar" -prop_readCoProc4 = isOk readCoProc "coproc a=b echo bar" -prop_readCoProc5 = isOk readCoProc "coproc 'foo' { echo bar; }" -prop_readCoProc6 = isOk readCoProc "coproc \"foo$$\" { echo bar; }" -prop_readCoProc7 = isOk readCoProc "coproc 'foo' ( echo bar )" -prop_readCoProc8 = isOk readCoProc "coproc \"foo$$\" while true; do true; done" -readCoProc = called "coproc" $ do - start <- startSpan - try $ do - string "coproc" - spacing1 - choice [ try $ readCompoundCoProc start, readSimpleCoProc start ] - where - readCompoundCoProc start = do - notFollowedBy2 readAssignmentWord - (var, body) <- choice [ - try $ do - body <- readBody readCompoundCommand - return (Nothing, body), - try $ do - var <- readNormalWord `thenSkip` spacing - body <- readBody readCompoundCommand - return (Just var, body) - ] - id <- endSpan start - return $ T_CoProc id var body - readSimpleCoProc start = do - body <- readBody readSimpleCommand - id <- endSpan start - return $ T_CoProc id Nothing body - readBody parser = do - start <- startSpan - body <- parser - id <- endSpan start - return $ T_CoProcBody id body - -readPattern = (readPatternWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip` spacing) - -prop_readConditionCommand = isOk readConditionCommand "[[ x ]] > foo 2>&1" -readConditionCommand = do - cmd <- readCondition - redirs <- many readIoRedirect - id <- getNextIdSpanningTokenList (cmd:redirs) - - pos <- getPosition - hasDashAo <- isFollowedBy $ do - c <- choice $ try . string <$> ["-o", "-a", "or", "and"] - posEnd <- getPosition - parseProblemAtWithEnd pos posEnd ErrorC 1139 $ - "Use " ++ alt c ++ " instead of '" ++ c ++ "' between test commands." - - -- If the next word is a keyword, readNormalWord will trigger a warning - hasKeyword <- isFollowedBy readKeyword - hasWord <- isFollowedBy readNormalWord - - when (hasWord && not (hasKeyword || hasDashAo)) $ do - -- We have other words following, and no error has been emitted. - posEnd <- getPosition - parseProblemAtWithEnd pos posEnd ErrorC 1140 "Unexpected parameters after condition. Missing &&/||, or bad expression?" - - return $ T_Redirecting id redirs cmd - where - alt "or" = "||" - alt "-o" = "||" - alt "and" = "&&" - alt "-a" = "&&" - alt _ = "|| or &&" - -prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null" -readCompoundCommand = do - cmd <- choice [ - readBraceGroup, - readAmbiguous "((" readArithmeticExpression readSubshell (\pos -> - parseNoteAt pos ErrorC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."), - readSubshell, - readWhileClause, - readUntilClause, - readIfClause, - readForClause, - readSelectClause, - readCaseClause, - readBatsTest, - readFunctionDefinition - ] - redirs <- many readIoRedirect - id <- getNextIdSpanningTokenList (cmd:redirs) - optional . lookAhead $ do - notFollowedBy2 $ choice [readKeyword, g_Lbrace] - pos <- getPosition - many1 readNormalWord - posEnd <- getPosition - parseProblemAtWithEnd pos posEnd ErrorC 1141 "Unexpected tokens after compound command. Bad redirection or missing ;/&&/||/|?" - return $ T_Redirecting id redirs cmd - - -readCompoundList = readTerm -readCompoundListOrEmpty = do - allspacing - readTerm <|> return [] - -readCmdPrefix = many1 (readIoRedirect <|> readAssignmentWord) -readCmdSuffix = many1 (readIoRedirect <|> readCmdWord) -readModifierSuffix = many1 (readIoRedirect <|> readWellFormedAssignment <|> readCmdWord) -readTimeSuffix = do - flags <- many readFlag - pipeline <- readPipeline - return $ flags ++ [pipeline] - where - -- This fails for quoted variables and such. Fixme? - readFlag = do - lookAhead $ char '-' - readCmdWord - --- Fixme: this is a hack that doesn't handle let c='4'"5" or let a\>b -readLetSuffix :: Monad m => SCParser m [Token] -readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord) - where - readLetExpression :: Monad m => SCParser m Token - readLetExpression = do - startPos <- getPosition - expression <- readStringForParser readCmdWord - let (unQuoted, newPos) = kludgeAwayQuotes expression startPos - subParse newPos (readArithmeticContents <* eof) unQuoted - - kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos) - kludgeAwayQuotes s p = - case s of - first:second:rest -> - let (last NE.:| backwards) = NE.reverse (second NE.:| rest) - middle = reverse backwards - in - if first `elem` "'\"" && first == last - then (middle, updatePosChar p first) - else (s, p) - x -> (s, p) - - --- bash allows a=(b), ksh allows $a=(b). dash allows neither. Let's warn. -readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback) - where - evalFallback = do - pos <- getPosition - lookAhead $ char '(' - parseProblemAt pos WarningC 1098 "Quote/escape special characters when using eval, e.g. eval \"a=(b)\"." - fail "Unexpected parentheses. Make sure to quote when eval'ing as shell parsers differ." - --- Get whatever a parser would parse as a string -readStringForParser parser = do - pos <- inSeparateContext $ lookAhead (parser >> getPosition) - readUntil pos - where - readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos)) - --- Like readStringForParser, returning the span as a T_Literal -readLiteralForParser parser = do - start <- startSpan - str <- readStringForParser parser - id <- endSpan start - return $ T_Literal id str - -prop_readAssignmentWord = isOk readAssignmentWord "a=42" -prop_readAssignmentWord2 = isOk readAssignmentWord "b=(1 2 3)" -prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol" -prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42" -prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42" -prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= " -prop_readAssignmentWord9a = isOk readAssignmentWord "foo=" -prop_readAssignmentWord9b = isOk readAssignmentWord "foo= " -prop_readAssignmentWord9c = isOk readAssignmentWord "foo= #bar" -prop_readAssignmentWord11 = isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" -prop_readAssignmentWord12 = isOk readAssignmentWord "a[b <<= 3 + c]='thing'" -prop_readAssignmentWord13 = isOk readAssignmentWord "var=( (1 2) (3 4) )" -prop_readAssignmentWord14 = isOk readAssignmentWord "var=( 1 [2]=(3 4) )" -prop_readAssignmentWord15 = isOk readAssignmentWord "var=(1 [2]=(3 4))" -readAssignmentWord = readAssignmentWordExt True -readWellFormedAssignment = readAssignmentWordExt False -readAssignmentWordExt lenient = called "variable assignment" $ do - -- Parse up to and including the = in a 'try' - (id, variable, op, indices) <- try $ do - start <- startSpan - pos <- getPosition - -- Check for a leading $ at parse time, to warn for $foo=(bar) which - -- would otherwise cause a parse failure so it can't be checked later. - leadingDollarPos <- - if lenient - then optionMaybe $ getSpanPositionsFor (char '$') - else return Nothing - variable <- readVariableName - indices <- many readArrayIndex - hasLeftSpace <- fmap (not . null) spacing - opStart <- getPosition - id <- endSpan start - op <- readAssignmentOp - opEnd <- getPosition - - when (isJust leadingDollarPos || hasLeftSpace) $ do - hasParen <- isFollowedBy (spacing >> char '(') - when hasParen $ - sequence_ $ do - (l, r) <- leadingDollarPos - return $ parseProblemAtWithEnd l r ErrorC 1066 "Don't use $ on the left side of assignments." - - -- Fail so that this is not parsed as an assignment. - fail "" - -- At this point we know for sure. - return (id, variable, op, indices) - - rightPosStart <- getPosition - hasRightSpace <- fmap (not . null) spacing - rightPosEnd <- getPosition - isEndOfCommand <- fmap isJust $ optionMaybe (try . lookAhead $ (void (oneOf "\r\n;&|)") <|> eof)) - - if hasRightSpace || isEndOfCommand - then do - when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $ do - parseProblemAtWithEnd rightPosStart rightPosEnd WarningC 1007 - "Remove space after = if trying to assign a value (for empty string, use var='' ... )." - value <- readEmptyLiteral - return $ T_Assignment id op variable indices value - else do - optional $ do - lookAhead $ char '=' - parseProblem ErrorC 1097 "Unexpected ==. For assignment, use =. For comparison, use [/[[. Or quote for literal string." - - value <- readArray <|> readNormalWord - spacing - return $ T_Assignment id op variable indices value - where - readAssignmentOp = do - -- This is probably some kind of ascii art border - unexpecting "===" (string "===") - choice [ - string "+=" >> return Append, - string "=" >> return Assign - ] - -readEmptyLiteral = do - start <- startSpan - id <- endSpan start - return $ T_Literal id "" - -readArrayIndex = do - start <- startSpan - char '[' - pos <- getPosition - str <- readStringForParser readIndexSpan - char ']' - id <- endSpan start - return $ T_UnparsedIndex id pos str - -readArray :: Monad m => SCParser m Token -readArray = called "array assignment" $ do - start <- startSpan - opening <- getPosition - char '(' - optional $ do - lookAhead $ char '(' - parseProblemAt opening ErrorC 1116 "Missing $ on a $((..)) expression? (or use ( ( for arrays)." - allspacing - words <- readElement `reluctantlyTill` char ')' - char ')' <|> fail "Expected ) to close array assignment" - id <- endSpan start - return $ T_Array id words - where - readElement = (readIndexed <|> readRegular) `thenSkip` allspacing - readIndexed = do - start <- startSpan - index <- try $ do - x <- many1 readArrayIndex - char '=' - return x - value <- readRegular <|> nothing - id <- endSpan start - return $ T_IndexedElement id index value - readRegular = readArray <|> readNormalWord - - nothing = do - start <- startSpan - id <- endSpan start - return $ T_Literal id "" - -tryToken s t = try $ do - start <- startSpan - string s - id <- endSpan start - spacing - return $ t id - -redirToken c t = try $ do - start <- startSpan - char c - id <- endSpan start - notFollowedBy2 $ char '(' - return $ t id - -tryWordToken s t = tryParseWordToken s t `thenSkip` spacing -tryParseWordToken keyword t = try $ do - pos <- getPosition - start <- startSpan - str <- anycaseString keyword - id <- endSpan start - - optional $ do - c <- try . lookAhead $ anyChar - let warning code = parseProblem ErrorC code $ "You need a space before the " ++ [c] ++ "." - case c of - '[' -> warning 1069 - '#' -> warning 1099 - '!' -> warning 1129 - ':' -> warning 1130 - _ -> return () - - lookAhead keywordSeparator - when (str /= keyword) $ do - parseProblemAt pos ErrorC 1081 $ - "Scripts are case sensitive. Use '" ++ keyword ++ "', not '" ++ str ++ "' (or quote if literal)." - fail "" - return $ t id - -anycaseString = - mapM anycaseChar - where - anycaseChar c = char (toLower c) <|> char (toUpper c) - -g_AND_IF = tryToken "&&" T_AND_IF -g_OR_IF = tryToken "||" T_OR_IF -g_DSEMI = tryToken ";;" T_DSEMI -g_DLESS = tryToken "<<" T_DLESS -g_DGREAT = tryToken ">>" T_DGREAT -g_LESSAND = tryToken "<&" T_LESSAND -g_GREATAND = tryToken ">&" T_GREATAND -g_LESSGREAT = tryToken "<>" T_LESSGREAT -g_DLESSDASH = tryToken "<<-" T_DLESSDASH -g_CLOBBER = tryToken ">|" T_CLOBBER -g_OPERATOR = g_AND_IF <|> g_OR_IF <|> g_DSEMI <|> g_DLESSDASH <|> g_DLESS <|> g_DGREAT <|> g_LESSAND <|> g_GREATAND <|> g_LESSGREAT - -g_If = tryWordToken "if" T_If -g_Then = tryWordToken "then" T_Then -g_Else = tryWordToken "else" T_Else -g_Elif = tryWordToken "elif" T_Elif -g_Fi = tryWordToken "fi" T_Fi -g_Do = tryWordToken "do" T_Do -g_Done = tryWordToken "done" T_Done -g_Case = tryWordToken "case" T_Case -g_Esac = tryWordToken "esac" T_Esac -g_While = tryWordToken "while" T_While -g_Until = tryWordToken "until" T_Until -g_For = tryWordToken "for" T_For -g_Select = tryWordToken "select" T_Select -g_In = tryWordToken "in" T_In <* skipAnnotationAndWarn -g_Lbrace = tryWordToken "{" T_Lbrace -g_Rbrace = do -- handled specially due to ksh echo "${ foo; }bar" - start <- startSpan - char '}' - id <- endSpan start - return $ T_Rbrace id - -g_Lparen = tryToken "(" T_Lparen -g_Rparen = tryToken ")" T_Rparen -g_Bang = do - start <- startSpan - char '!' - id <- endSpan start - void spacing1 <|> do - pos <- getPosition - parseProblemAt pos ErrorC 1035 - "You are missing a required space after the !." - return $ T_Bang id - -g_Semi = do - notFollowedBy2 g_DSEMI - tryToken ";" T_Semi - -keywordSeparator = - eof <|> void (try allspacingOrFail) <|> void (oneOf ";()[<>&|") - -readKeyword = choice [ g_Then, g_Else, g_Elif, g_Fi, g_Do, g_Done, g_Esac, g_Rbrace, g_Rparen, g_DSEMI ] - -ifParse p t f = - (lookAhead (try p) >> t) <|> f - -prop_readShebang1 = isOk readShebang "#!/bin/sh\n" -prop_readShebang2 = isWarning readShebang "!# /bin/sh\n" -prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n" -prop_readShebang4 = isWarning readShebang "! /bin/sh" -prop_readShebang5 = isWarning readShebang "\n#!/bin/sh" -prop_readShebang6 = isWarning readShebang " # Copyright \n!#/bin/bash" -prop_readShebang7 = isNotOk readShebang "# Copyright \nfoo\n#!/bin/bash" -readShebang = do - start <- startSpan - anyShebang <|> try readMissingBang <|> withHeader - many linewhitespace - str <- many $ noneOf "\r\n" - id <- endSpan start - optional carriageReturn - optional linefeed - return $ T_Literal id str - where - anyShebang = choice $ map try [ - readCorrect, - readSwapped, - readTooManySpaces, - readMissingHash - ] - readCorrect = void $ string "#!" - - readSwapped = do - start <- startSpan - string "!#" - id <- endSpan start - parseProblemAtId id ErrorC 1084 - "Use #!, not !#, for the shebang." - - skipSpaces = fmap (not . null) $ many linewhitespace - readTooManySpaces = do - startPos <- getPosition - startSpaces <- skipSpaces - char '#' - middlePos <- getPosition - middleSpaces <- skipSpaces - char '!' - when startSpaces $ - parseProblemAt startPos ErrorC 1114 - "Remove leading spaces before the shebang." - when middleSpaces $ - parseProblemAt middlePos ErrorC 1115 - "Remove spaces between # and ! in the shebang." - - readMissingHash = do - pos <- getPosition - char '!' - ensurePathAhead - parseProblemAt pos ErrorC 1104 - "Use #!, not just !, for the shebang." - - readMissingBang = do - char '#' - pos <- getPosition - ensurePathAhead - parseProblemAt pos ErrorC 1113 - "Use #!, not just #, for the shebang." - - ensurePathAhead = lookAhead $ do - many linewhitespace - char '/' - - withHeader = try $ do - many1 headerLine - pos <- getPosition - anyShebang <* - parseProblemAt pos ErrorC 1128 "The shebang must be on the first line. Delete blanks and move comments." - - headerLine = do - notFollowedBy2 anyShebang - many linewhitespace - optional readAnyComment - linefeed - -verifyEof = eof <|> choice [ - ifParsable g_Lparen $ - parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?", - - ifParsable readKeyword $ - parseProblem ErrorC 1089 "Parsing stopped here. Is this keyword correctly matched up?", - - parseProblem ErrorC 1070 "Parsing stopped here. Mismatched keywords or invalid parentheses?" - ] - where - ifParsable p action = do - try (lookAhead p) - action - - -readConfigFile :: Monad m => FilePath -> SCParser m [Annotation] -readConfigFile filename = do - shouldIgnore <- Mr.asks ignoreRC - if shouldIgnore then return [] else read' filename - where - read' filename = do - sys <- Mr.asks systemInterface - contents <- system $ siGetConfig sys filename - case contents of - Nothing -> return [] - Just (file, str) -> readConfig file str - - readConfig filename contents = do - result <- lift $ runParserT readConfigKVs initialUserState filename contents - case result of - Right result -> - return result - - Left err -> do - parseProblem ErrorC 1134 $ errorFor filename err - return [] - - errorFor filename err = - let line = "line " ++ (show . sourceLine $ errorPos err) - suggestion = getStringFromParsec $ errorMessages err - in - "Failed to process " ++ (e4m filename) ++ ", " ++ line ++ ": " - ++ suggestion - -prop_readConfigKVs1 = isOk readConfigKVs "disable=1234" -prop_readConfigKVs2 = isOk readConfigKVs "# Comment\ndisable=1234 # Comment\n" -prop_readConfigKVs3 = isOk readConfigKVs "" -prop_readConfigKVs4 = isOk readConfigKVs "\n\n\n\n\t \n" -prop_readConfigKVs5 = isOk readConfigKVs "# shellcheck accepts annotation-like comments in rc files\ndisable=1234" -readConfigKVs = do - anySpacingOrComment - annotations <- many (readAnnotationWithoutPrefix False <* anySpacingOrComment) - eof - return $ concat annotations -anySpacingOrComment = - many (void allspacingOrFail <|> void readAnyComment) - -prop_readScript1 = isOk readScript "#!/bin/bash\necho hello world\n" -prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n" -prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world" -prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=(" -prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n" -prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n" -prop_readScript7 = isOk readScript "#!/bin/zsh\n# shellcheck disable=SC1071\nfor f (a b); echo $f\n" -readScriptFile sourced = do - start <- startSpan - pos <- getPosition - rcAnnotations <- if sourced - then return [] - else do - filename <- Mr.asks currentFilename - readConfigFile filename - - -- Put the rc annotations on the stack so that one can ignore e.g. SC1084 in .shellcheckrc - withAnnotations rcAnnotations $ do - hasBom <- wasIncluded readUtf8Bom - shebang <- readShebang <|> readEmptyLiteral - let (T_Literal _ shebangString) = shebang - allspacing - annotationStart <- startSpan - fileAnnotations <- readAnnotations - - -- Similarly put the filewide annotations on the stack to allow earlier suppression - withAnnotations fileAnnotations $ do - when (hasBom) $ - parseProblemAt pos ErrorC 1082 - "This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ." - let annotations = fileAnnotations ++ rcAnnotations - annotationId <- endSpan annotationStart - let shellAnnotationSpecified = - any (\x -> case x of ShellOverride {} -> True; _ -> False) annotations - shellFlagSpecified <- isJust <$> Mr.asks shellTypeOverride - let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified - - unless ignoreShebang $ - verifyShebang pos (executableFromShebang shebangString) - if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False - then do - commands <- readCompoundListOrEmpty - id <- endSpan start - readPendingHereDocs - verifyEof - let script = T_Annotation annotationId annotations $ - T_Script id shebang commands - userstate <- getState - reparseIndices $ reattachHereDocs script (hereDocMap userstate) - else do - many anyChar - id <- endSpan start - return $ T_Script id shebang [] - - where - verifyShebang pos s = do - case isValidShell s of - Just True -> return () - Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh/'busybox sh' scripts. Sorry!" - Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh/'busybox sh'. Add a 'shell' directive to specify." - - isValidShell s = - let good = null s || any (`isPrefixOf` s) goodShells - bad = any (`isPrefixOf` s) badShells - in - if good - then Just True - else if bad - then Just False - else Nothing - - goodShells = [ - "sh", - "ash", - "dash", - "busybox sh", - "bash", - "bats", - "ksh", - "oksh" - ] - badShells = [ - "awk", - "csh", - "expect", - "fish", - "perl", - "python", - "python3", - "ruby", - "tcsh", - "zsh" - ] - - readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF" - -readScript = readScriptFile False - --- Interactively run a specific parser in ghci: --- debugParse readSimpleCommand "echo 'hello world'" -debugParse p string = runIdentity $ do - (res, _) <- runParser testEnvironment p "-" string - return res - --- Interactively run the complete parser in ghci: --- debugParseScript "#!/bin/bash\necho 'Hello World'\n" -debugParseScript string = - result { - -- Remove the noisiest parts - prTokenPositions = Map.fromList [ - (Id 0, (newPosition { - posFile = "removed for clarity", - posLine = -1, - posColumn = -1 - }, newPosition { - posFile = "removed for clarity", - posLine = -1, - posColumn = -1 - }))] - } - where - result = runIdentity $ - parseScript (mockedSystemInterface []) $ newParseSpec { - psFilename = "debug", - psScript = string - } - -testEnvironment = - Environment { - systemInterface = (mockedSystemInterface []), - checkSourced = False, - currentFilename = "myscript", - ignoreRC = False, - shellTypeOverride = Nothing - } - - -isOk p s = parsesCleanly p s == Just True -- The string parses with no warnings -isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings -isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse - --- If the parser matches the string, return Right [ParseNotes+ParseProblems] --- If it does not match the string, return Left [ParseProblems] -getParseOutput parser string = runIdentity $ do - (res, systemState) <- runParser testEnvironment - (parser >> eof >> getState) "-" string - return $ case res of - Right userState -> - Right $ parseNotes userState ++ parseProblems systemState - Left _ -> Left $ parseProblems systemState - --- If the parser matches the string, return Just whether it was clean (without emitting suggestions) --- Otherwise, Nothing -parsesCleanly parser string = - case getParseOutput parser string of - Right list -> Just $ null list - Left _ -> Nothing - -parseWithNotes parser = do - item <- parser - state <- getState - return (item, state) - -compareNotes (ParseNote pos1 pos1' level1 _ s1) (ParseNote pos2 pos2' level2 _ s2) = compare (pos1, pos1', level1) (pos2, pos2', level2) -sortNotes = sortBy compareNotes - - -makeErrorFor parsecError = - ParseNote pos pos ErrorC 1072 $ - getStringFromParsec $ errorMessages parsecError - where - pos = errorPos parsecError - -getStringFromParsec errors = - headOrDefault "" (mapMaybe f $ reverse errors) ++ - " Fix any mentioned problems and try again." - where - f err = - case err of - UnExpect s -> Nothing -- Due to not knowing Parsec, none of these - SysUnExpect s -> Nothing -- are actually helpful. has been hidden - Expect s -> Nothing -- and we only show explicit fail statements. - Message s -> if null s then Nothing else return $ s ++ "." - -runParser :: Monad m => - Environment m -> - SCParser m v -> - String -> - String -> - m (Either ParseError v, SystemState) - -runParser env p filename contents = - Ms.runStateT - (Mr.runReaderT - (runParserT p initialUserState filename contents) - env) - initialSystemState -system = lift . lift . lift - -parseShell env name contents = do - (result, state) <- runParser env (parseWithNotes readScript) name contents - case result of - Right (script, userstate) -> - return newParseResult { - prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state, - prTokenPositions = Map.map startEndPosToPos (positionMap userstate), - prRoot = Just script - } - Left err -> do - let context = contextStack state - return newParseResult { - prComments = - map toPositionedComment $ - (filter (not . isIgnored context) $ - notesForContext context - ++ [makeErrorFor err]) - ++ parseProblems state, - prTokenPositions = Map.empty, - prRoot = Nothing - } - where - -- A final pass for ignoring parse errors after failed parsing - isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack - -notesForContext list = zipWith ($) [first, second] [(pos, str) | ContextName pos str <- list] - where - first (pos, str) = ParseNote pos pos ErrorC 1073 $ - "Couldn't parse this " ++ str ++ ". Fix to allow more checks." - second (pos, str) = ParseNote pos pos InfoC 1009 $ - "The mentioned syntax error was in this " ++ str ++ "." - --- Go over all T_UnparsedIndex and reparse them as either arithmetic or text --- depending on declare -A statements. -reparseIndices root = process root - where - process = analyze blank blank f - associative = getAssociativeArrays root - isAssociative s = s `elem` associative - f (T_Assignment id mode name indices value) = do - newIndices <- mapM (fixAssignmentIndex name) indices - newValue <- case value of - (T_Array id2 words) -> do - newWords <- mapM (fixIndexElement name) words - return $ T_Array id2 newWords - x -> return x - return $ T_Assignment id mode name newIndices newValue - f (TA_Variable id name indices) = do - newIndices <- mapM (fixAssignmentIndex name) indices - return $ TA_Variable id name newIndices - f t = return t - - fixIndexElement name word = - case word of - T_IndexedElement id indices value -> do - new <- mapM (fixAssignmentIndex name) indices - return $ T_IndexedElement id new value - _ -> return word - - fixAssignmentIndex name word = - case word of - T_UnparsedIndex id pos src -> do - idx <- parsed name pos src - process idx -- Recursively parse for cases like x[y[z=1]]=1 - _ -> return word - - parsed name pos src = - if isAssociative name - then subParse pos (called "associative array index" $ readIndexSpan) src - else subParse pos (called "arithmetic array index expression" $ optional space >> readArithmeticContents) src - -reattachHereDocs root map = - doTransform f root - where - f t@(T_HereDoc id dash quote string []) = fromMaybe t $ do - list <- Map.lookup id map - return $ T_HereDoc id dash quote string list - f t = t - -toPositionedComment :: ParseNote -> PositionedComment -toPositionedComment (ParseNote start end severity code message) = - newPositionedComment { - pcStartPos = (posToPos start) - , pcEndPos = (posToPos end) - , pcComment = newComment { - cSeverity = severity - , cCode = code - , cMessage = message - } - } - -posToPos :: SourcePos -> Position -posToPos sp = newPosition { - posFile = sourceName sp, - posLine = fromIntegral $ sourceLine sp, - posColumn = fromIntegral $ sourceColumn sp -} - -startEndPosToPos :: (SourcePos, SourcePos) -> (Position, Position) -startEndPosToPos (s, e) = (posToPos s, posToPos e) - --- TODO: Clean up crusty old code that this is layered on top of -parseScript :: Monad m => - SystemInterface m -> ParseSpec -> m ParseResult -parseScript sys spec = - parseShell env (psFilename spec) (psScript spec) - where - env = Environment { - systemInterface = sys, - checkSourced = psCheckSourced spec, - currentFilename = psFilename spec, - ignoreRC = psIgnoreRC spec, - shellTypeOverride = psShellTypeOverride spec - } - --- Same as 'try' but emit syntax errors if the parse fails. -tryWithErrors :: Monad m => SCParser m v -> SCParser m v -tryWithErrors parser = do - userstate <- getState - oldContext <- getCurrentContexts - input <- getInput - pos <- getPosition - result <- lift $ runParserT (setPosition pos >> getResult parser) userstate (sourceName pos) input - case result of - Right (result, endPos, endInput, endState) -> do - -- 'many' objects if we don't consume anything at all, so read a dummy value - void anyChar <|> eof - putState endState - setPosition endPos - setInput endInput - return result - - Left err -> do - newContext <- getCurrentContexts - addParseProblem $ makeErrorFor err - mapM_ addParseProblem $ notesForContext newContext - setCurrentContexts oldContext - fail "" - where - getResult p = do - result <- p - endPos <- getPosition - endInput <- getInput - endState <- getState - return (result, endPos, endInput, endState) - -return [] -runTests = $quickCheckAll diff --git a/src/ShellCheck/Prelude.hs b/src/ShellCheck/Prelude.hs deleted file mode 100644 index 7610c46..0000000 --- a/src/ShellCheck/Prelude.hs +++ /dev/null @@ -1,51 +0,0 @@ -{- - Copyright 2022 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} - --- Generic basic utility functions -module ShellCheck.Prelude where - -import Data.Semigroup - - --- Get element 0 or a default. Like `head` but safe. -headOrDefault _ (a:_) = a -headOrDefault def _ = def - --- Get the last element or a default. Like `last` but safe. -lastOrDefault def [] = def -lastOrDefault _ list = last list - ---- Get element n of a list, or Nothing. Like `!!` but safe. -(!!!) list i = - case drop i list of - [] -> Nothing - (r:_) -> Just r - - --- Like mconcat but for Semigroups -sconcat1 :: (Semigroup t) => [t] -> t -sconcat1 [x] = x -sconcat1 (x:xs) = x <> sconcat1 xs - -sconcatOrDefault def [] = def -sconcatOrDefault _ list = sconcat1 list - --- For more actionable "impossible" errors -pleaseReport str = "ShellCheck internal error, please report: " ++ str diff --git a/src/ShellCheck/Regex.hs b/src/ShellCheck/Regex.hs deleted file mode 100644 index 9367ee7..0000000 --- a/src/ShellCheck/Regex.hs +++ /dev/null @@ -1,80 +0,0 @@ -{- - Copyright 2012-2019 Vidar Holen - - This file is part of ShellCheck. - https://www.shellcheck.net - - ShellCheck is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ShellCheck is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . --} -{-# LANGUAGE FlexibleContexts #-} - --- Basically Text.Regex based on regex-tdfa instead of the buggy regex-posix. -module ShellCheck.Regex where - -import Data.List -import Data.Maybe -import Control.Monad -import Text.Regex.TDFA - --- Precompile the regex -mkRegex :: String -> Regex -mkRegex str = - let make :: String -> Regex - make = makeRegex - in - make str - --- Does the regex match? -matches :: String -> Regex -> Bool -matches = flip match - --- Get all subgroups of the first match -matchRegex :: Regex -> String -> Maybe [String] -matchRegex re str = do - (_, _, _, groups) <- matchM re str :: Maybe (String,String,String,[String]) - return groups - --- Get all full matches -matchAllStrings :: Regex -> String -> [String] -matchAllStrings re = unfoldr f - where - f :: String -> Maybe (String, String) - f str = do - (_, match, rest, _) <- matchM re str :: Maybe (String, String, String, [String]) - return (match, rest) - --- Get all subgroups from all matches -matchAllSubgroups :: Regex -> String -> [[String]] -matchAllSubgroups re = unfoldr f - where - f :: String -> Maybe ([String], String) - f str = do - (_, _, rest, groups) <- matchM re str :: Maybe (String, String, String, [String]) - return (groups, rest) - --- Replace regex in input with string -subRegex :: Regex -> String -> String -> String -subRegex re input replacement = f input - where - f str = fromMaybe str $ do - (before, match, after) <- matchM re str :: Maybe (String, String, String) - when (null match) $ error ("Internal error: substituted empty in " ++ str) - return $ before ++ replacement ++ f after - --- Split a string based on a regex. -splitOn :: String -> Regex -> [String] -splitOn input re = - case matchM re input :: Maybe (String, String, String) of - Just (before, match, after) -> before : after `splitOn` re - Nothing -> [input] diff --git a/stack.yaml b/stack.yaml deleted file mode 100644 index 4cf5c74..0000000 --- a/stack.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# This file was automatically generated by stack init -# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/ - -# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) -resolver: lts-18.15 - -# Local packages, usually specified by relative directory name -packages: -- '.' -# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3) -extra-deps: [] - -# Override default flag values for local packages and extra-deps -flags: {} - -# Extra package databases containing global packages -extra-package-dbs: [] - -# Control whether we use the GHC we find on the path -# system-ghc: true - -# Require a specific version of stack, using version ranges -# require-stack-version: -any # Default -# require-stack-version: >= 1.0.0 - -# Override the architecture used by stack, especially useful on Windows -# arch: i386 -# arch: x86_64 - -# Extra directories used by stack for building -# extra-include-dirs: [/path/to/dir] -# extra-lib-dirs: [/path/to/dir] - -# Allow a newer minor version of GHC than the snapshot specifies -# compiler-check: newer-minor diff --git a/striptests b/striptests deleted file mode 100755 index 6e64607..0000000 --- a/striptests +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -# This file strips all unit tests from ShellCheck, removing -# the dependency on QuickCheck and Template Haskell and -# reduces the binary size considerably. -set -o pipefail - -sponge() { - local data - data="$(cat)" - printf '%s\n' "$data" > "$1" -} - -modify() { - if ! "${@:2}" < "$1" | sponge "$1" - then - { - printf 'Failed to modify %s: ' "$1" - printf '%q ' "${@:2}" - printf '\n' - } >&2 - exit 1 - fi -} - -detestify() { - printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.' - awk ' - BEGIN { - state = 0; - } - - /STRIP/ { next; } - /LANGUAGE TemplateHaskell/ { next; } - /^import.*Test\./ { next; } - - /^module/ { - sub(/,[^,)]*runTests/, ""); - } - - # Delete tests - /^prop_/ { state = 1; next; } - - # ..and any blank lines following them. - state == 1 && /^ / { next; } - - # Template Haskell marker - /^return / { - exit; - } - - { state = 0; print; } - ' -} - - - -if [[ ! -e 'ShellCheck.cabal' ]] -then - echo "Run me from the ShellCheck directory." >&2 - exit 1 -fi - -if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1 -then - echo "You have local changes! These may be overwritten." >&2 - exit 2 -fi - -modify 'ShellCheck.cabal' sed -e ' - /QuickCheck/d - /^test-suite/{ s/.*//; q; } - ' - -find . -name '.git' -prune -o -type f -name '*.hs' -print | - while IFS= read -r file - do - modify "$file" detestify - done diff --git a/test/buildtest b/test/buildtest deleted file mode 100755 index 3e0847e..0000000 --- a/test/buildtest +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# This script configures, builds and runs tests. -# It's meant for automatic cross-distro testing. - -die() { echo "$*" >&2; exit 1; } - -[ -e "ShellCheck.cabal" ] || - die "ShellCheck.cabal not in current dir" -command -v cabal || - die "cabal is missing" - -cabal update || - die "can't update" - -if [ -e "cabal.project.freeze" ] -then - echo "Renaming cabal.project.freeze to .bak to avoid it interferring" >&2 - mv "cabal.project.freeze" "cabal.project.freeze.bak" || die "Couldn't rename" -fi - -if [ -e /etc/arch-release ] -then - # Arch has an unconventional packaging setup - flags=(--disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic) -else - flags=() -fi - -cabal install --dependencies-only --enable-tests "${flags[@]}" || - cabal install --dependencies-only "${flags[@]}" || - cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" || - die "can't install dependencies" -cabal configure --enable-tests "${flags[@]}" || - die "configure failed" -cabal build || - die "build failed" -cabal test || - die "test failed" -cabal haddock || - die "haddock failed" - -sc="$(find . -name shellcheck -type f -perm -111)" -[ -x "$sc" ] || die "Can't find executable" - -"$sc" - << 'EOF' || die "execution failed" -#!/bin/sh -echo "Hello World" -EOF - -"$sc" - << 'EOF' && die "negative execution failed" -#!/bin/sh -echo $1 -EOF - - -echo "Success" -exit 0 diff --git a/test/check_release b/test/check_release deleted file mode 100755 index cbb741b..0000000 --- a/test/check_release +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2257 - -failed=0 -fail() { - echo "$(tput setaf 1)$*$(tput sgr0)" - failed=1 -} - -i=1 j=1 -cat << EOF - -Manual Checklist - -$((i++)). Make sure README.md examples are up to date -$((i++)). Format and read over the manual for bad formatting and outdated info. -$((i++)). Run \`builders/build_builder build/*/\` to update all builder images. -$((i++)). \`builders/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz builders/*/\` to verify that they work. -$((i++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them. -$((i++)). Run test/distrotest to ensure that most distros can build OOTB. -$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions -$((i++)). Make sure SnapCraft build currently works: https://snapcraft.io/shellcheck/builds -$((i++)). Make sure the Hackage package builds locally. -$((i++)). Make sure none of the automated checks below fail - -Release Steps - -$((j++)). \`cabal sdist\` to generate a Hackage package -$((j++)). \`git push --follow-tags\` to push commit -$((j++)). Wait for GitHub Actions to build. (v0.11.0 "Deploy" failed, but worked on retry) -$((j++)). Verify release: - a. Check that the new versions are uploaded: https://github.com/koalaman/shellcheck/tags - b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman -$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload -$((j++)). Run 'autoupdate' from https://github.com/koalaman/shellcheck-precommit -$((j++)). Release new snap versions on https://snapcraft.io/shellcheck/releases -$((j++)). Push a new commit that updates CHANGELOG.md - -Automated Checks - -EOF - -if git diff | grep -q "" -then - fail "There are uncommitted changes" -fi - -if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]] -then - fail "Expected git log message to contain CHANGELOG" -fi - -version=${current#v} -if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version" -then - fail "The cabal file does not match tag version $version" -fi - -if ! grep -qF "## $current" CHANGELOG.md -then - fail "CHANGELOG.md does not contain '## $current'" -fi - -current=$(git tag --points-at) -if [[ -z "$current" ]] -then - fail "No git tag on the current commit" - echo "Create one with: git tag -a v0.0.0" -fi - -if [[ "$current" != v* ]] -then - fail "Bad tag format: expected v0.0.0" -fi - -if [[ "$(git cat-file -t "$current")" != "tag" ]] -then - fail "Current tag is not annotated (required for Snap)." -fi - -if [[ "$(git tag --points-at master)" != "$current" ]] -then - fail "You are not on master" -fi - -if [[ $(git log -1 --pretty=%B) != "Stable version "* ]] -then - fail "Expected git log message to be 'Stable version ...'" -fi -exit "$failed" diff --git a/test/distrotest b/test/distrotest deleted file mode 100755 index f1d01c1..0000000 --- a/test/distrotest +++ /dev/null @@ -1,94 +0,0 @@ -#!/bin/bash -# This script runs 'buildtest' on each of several distros -# via Docker. -set -o pipefail - -exec 3>&1 4>&2 -die() { echo "$*" >&4; exit 1; } - -[ -e "ShellCheck.cabal" ] || die "ShellCheck.cabal not in this dir" - -if ( snap list | grep -q docker ) > /dev/null 2>&1 -then - # Snap docker can't mount /tmp in containers - echo "You appear to be using Docker from snap. Creating ~/tmp for temp files." >&2 - echo >&2 - export TMPDIR="$HOME/tmp" - mkdir -p "$TMPDIR" -fi - -[ "$1" = "--run" ] || { -cat << EOF -This script pulls multiple distros via Docker and compiles -ShellCheck and dependencies for each one. It takes hours, -and is still highly experimental. - -Make sure you're plugged in and have screen/tmux in place, -then re-run with $0 --run to continue. - -Also note that dist*/ and .stack-work/ will be deleted. -EOF -exit 0 -} - -echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..." -rm -rf dist dist-newstyle .stack-work - -execs=$(find . -name shellcheck) - -if [ -n "$execs" ] -then - die "Found unexpected executables. Remove and try again: $execs" -fi - -log=$(mktemp) || die "Can't create temp file" -date >> "$log" || die "Can't write to log" - -echo "Logging to $log" >&3 -exec >> "$log" 2>&1 - -final=0 -while read -r distro setup -do - [[ "$distro" = "#"* || -z "$distro" ]] && continue - printf '%s ' "$distro" >&3 - docker pull "$distro" || die "Can't pull $distro" - printf 'pulled. ' >&3 - - tmp=$(mktemp -d) || die "Can't make temp dir" - cp -r . "$tmp/" || die "Can't populate test dir" - printf 'Result: ' >&3 - < /dev/null docker run -v "$tmp:/mnt" "$distro" sh -c " - $setup - cd /mnt || exit 1 - test/buildtest - " - ret=$? - if [ "$ret" = 0 ] - then - echo "OK" >&3 - else - echo "FAIL with $ret. See $log" >&3 - final=1 - fi - rm -rf "$tmp" -done << EOF -# Docker tag Setup command -debian:stable apt-get update && apt-get install -y cabal-install -debian:testing apt-get update && apt-get install -y cabal-install -ubuntu:latest apt-get update && apt-get install -y cabal-install -haskell:latest true -opensuse/leap:latest zypper install -y cabal-install ghc -fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++ -archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel - -# Ubuntu LTS -ubuntu:24.04 apt-get update && apt-get install -y cabal-install -ubuntu:22.04 apt-get update && apt-get install -y cabal-install -ubuntu:20.04 apt-get update && apt-get install -y cabal-install - -# Stack on Ubuntu LTS -ubuntu:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest -EOF - -exit "$final" diff --git a/test/shellcheck.hs b/test/shellcheck.hs deleted file mode 100644 index d5e056d..0000000 --- a/test/shellcheck.hs +++ /dev/null @@ -1,41 +0,0 @@ -module Main where - -import Control.Monad -import System.Exit -import qualified ShellCheck.Analytics -import qualified ShellCheck.AnalyzerLib -import qualified ShellCheck.ASTLib -import qualified ShellCheck.CFG -import qualified ShellCheck.CFGAnalysis -import qualified ShellCheck.Checker -import qualified ShellCheck.Checks.Commands -import qualified ShellCheck.Checks.ControlFlow -import qualified ShellCheck.Checks.Custom -import qualified ShellCheck.Checks.ShellSupport -import qualified ShellCheck.Fixer -import qualified ShellCheck.Formatter.Diff -import qualified ShellCheck.Parser - -main = do - putStrLn "Running ShellCheck tests..." - failures <- filter (not . snd) <$> mapM sequenceA tests - if null failures then exitSuccess else do - putStrLn "Tests failed for the following module(s):" - mapM (putStrLn . ("- ShellCheck." ++) . fst) failures - exitFailure - where - tests = - [ ("Analytics" , ShellCheck.Analytics.runTests) - , ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests) - , ("ASTLib" , ShellCheck.ASTLib.runTests) - , ("CFG" , ShellCheck.CFG.runTests) - , ("CFGAnalysis" , ShellCheck.CFGAnalysis.runTests) - , ("Checker" , ShellCheck.Checker.runTests) - , ("Checks.Commands" , ShellCheck.Checks.Commands.runTests) - , ("Checks.ControlFlow" , ShellCheck.Checks.ControlFlow.runTests) - , ("Checks.Custom" , ShellCheck.Checks.Custom.runTests) - , ("Checks.ShellSupport", ShellCheck.Checks.ShellSupport.runTests) - , ("Fixer" , ShellCheck.Fixer.runTests) - , ("Formatter.Diff" , ShellCheck.Formatter.Diff.runTests) - , ("Parser" , ShellCheck.Parser.runTests) - ] diff --git a/test/stacktest b/test/stacktest deleted file mode 100755 index b486c31..0000000 --- a/test/stacktest +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -# This script builds ShellCheck through `stack` using -# various resolvers. It's run via distrotest. - -resolvers=( -# nightly-"$(date -d "3 days ago" +"%Y-%m-%d")" -) - -die() { echo "$*" >&2; exit 1; } - -[ -e "ShellCheck.cabal" ] || - die "ShellCheck.cabal not in current dir" -[ -e "stack.yaml" ] || - die "stack.yaml not in current dir" -command -v stack || - die "stack is missing" - -stack setup --allow-different-user || die "Failed to setup with default resolver" -stack build --test || die "Failed to build/test with default resolver" - -# Nice to haves, but not necessary -for resolver in "${resolvers[@]}" -do - stack --resolver="$resolver" setup || die "Failed to setup $resolver. This probably doesn't matter." - stack --resolver="$resolver" build --test || die "Failed build/test with $resolver! This probably doesn't matter." -done - -echo "Success"