mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-10-02 18:49:25 +08:00
Compare commits
124 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
21462b11b3 | ||
|
88aef838f1 | ||
|
3d61b73e91 | ||
|
138080bdc7 | ||
|
5b3f17c29d | ||
|
b47e083ee3 | ||
|
3cba76dc7d | ||
|
eb588f62f6 | ||
|
bcd13614eb | ||
|
a8376a09a9 | ||
|
5ed89d2241 | ||
|
4a87d2a3de | ||
|
41613babd9 | ||
|
cb57b4a74f | ||
|
e3f0243c0e | ||
|
66b5f13c6f | ||
|
a7a404a5a8 | ||
|
0761f5c923 | ||
|
b55149b22d | ||
|
4097bb5154 | ||
|
1b207b3d43 | ||
|
135b4aa485 | ||
|
cb76951ad2 | ||
|
705e476e4c | ||
|
e705552c97 | ||
|
198aa4fc3d | ||
|
f4044fbcc7 | ||
|
2827b35696 | ||
|
de95c376ea | ||
|
5e1b1e010a | ||
|
620c9c2023 | ||
|
359b1467a2 | ||
|
df0a0d41fa | ||
|
b815242506 | ||
|
07b5aa2971 | ||
|
75949fe51e | ||
|
f7b82658f4 | ||
|
e0e46e979a | ||
|
79319558a5 | ||
|
8d13add1ed | ||
|
8940e60300 | ||
|
5f1c969546 | ||
|
dadfdfde97 | ||
|
3e2cb26119 | ||
|
1a6ae4f19e | ||
|
95a376aad1 | ||
|
a06d7c1841 | ||
|
5202072a34 | ||
|
72af1cfd59 | ||
|
228af7df54 | ||
|
6db392511b | ||
|
d510a3ef6c | ||
|
5516596b26 | ||
|
9e7539c10b | ||
|
a5a7b332f1 | ||
|
a68e3aeb26 | ||
|
259b1a5dc6 | ||
|
07f04e13ce | ||
|
493ecd6f73 | ||
|
f0a2e688c4 | ||
|
0cee8a993d | ||
|
3d03b0ab3b | ||
|
488d6dcb41 | ||
|
d02a9bbcce | ||
|
165e408114 | ||
|
932e2b3538 | ||
|
76b1482f64 | ||
|
49250eadae | ||
|
3fe11927bb | ||
|
b16da4b242 | ||
|
c8e0797350 | ||
|
15aaacf715 | ||
|
5ef4229f61 | ||
|
afada43978 | ||
|
8be76b13b9 | ||
|
581be5878b | ||
|
0f835a5a2c | ||
|
4b0a35d4c9 | ||
|
51e0c1be62 | ||
|
d8a32da07f | ||
|
0d1a34a291 | ||
|
5005dc0fa1 | ||
|
b8ee7436e5 | ||
|
da8e450386 | ||
|
c3ac4c3d87 | ||
|
03ce3b15b6 | ||
|
10edba3ab8 | ||
|
797b424917 | ||
|
84e678e9ff | ||
|
3a672968f3 | ||
|
8c7efae393 | ||
|
f91b5bc270 | ||
|
b01f1128c7 | ||
|
db33294838 | ||
|
75fb4da387 | ||
|
366262af18 | ||
|
6869c2fa18 | ||
|
868a7be33e | ||
|
7138abff4b | ||
|
9d3e79b576 | ||
|
402e635f86 | ||
|
91cbcddd9d | ||
|
963b39b002 | ||
|
0cc45447d3 | ||
|
32a53f21b5 | ||
|
12b8720bd8 | ||
|
7adeaccd11 | ||
|
b63483d44c | ||
|
4111ce8fde | ||
|
b9a9eb2529 | ||
|
e717802de1 | ||
|
1699c9e9ba | ||
|
bfc32200e2 | ||
|
52e8a42d9d | ||
|
00360af672 | ||
|
8ff35fb4af | ||
|
29e8c0a16e | ||
|
3848788c2d | ||
|
0c459ae2cb | ||
|
e496b413bd | ||
|
48ac654a93 | ||
|
4470fe715c | ||
|
379321d1f3 | ||
|
0adea473fd |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,8 +1,8 @@
|
|||||||
#### For bugs
|
#### For bugs
|
||||||
- Rule Id (if any, e.g. SC1000):
|
- Rule Id (if any, e.g. SC1000):
|
||||||
- My shellcheck version (`shellcheck --version` or "online"):
|
- 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 shellcheck.net and verified that this is still a problem on the latest commit
|
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
||||||
- [ ] It's not reproducible on shellcheck.net, but I think that's because it's an OS, configuration or encoding issue
|
|
||||||
|
|
||||||
#### For new checks and feature suggestions
|
#### For new checks and feature suggestions
|
||||||
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,6 +13,9 @@ cabal-dev
|
|||||||
cabal.sandbox.config
|
cabal.sandbox.config
|
||||||
cabal.config
|
cabal.config
|
||||||
.stack-work
|
.stack-work
|
||||||
|
dist-newstyle/
|
||||||
|
.ghc.environment.*
|
||||||
|
cabal.project.local
|
||||||
|
|
||||||
### Snap ###
|
### Snap ###
|
||||||
/snap/.snapcraft/
|
/snap/.snapcraft/
|
||||||
|
@@ -27,7 +27,7 @@ do
|
|||||||
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
||||||
done
|
done
|
||||||
|
|
||||||
for file in *.linux
|
for file in *.linux-x86_64
|
||||||
do
|
do
|
||||||
base="${file%.*}"
|
base="${file%.*}"
|
||||||
cp "$file" "shellcheck"
|
cp "$file" "shellcheck"
|
||||||
@@ -35,6 +35,14 @@ do
|
|||||||
rm "shellcheck"
|
rm "shellcheck"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
for file in *.linux-armv6hf
|
||||||
|
do
|
||||||
|
base="${file%.*}"
|
||||||
|
cp "$file" "shellcheck"
|
||||||
|
tar -cJf "$base.linux.armv6hf.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
|
||||||
|
rm "shellcheck"
|
||||||
|
done
|
||||||
|
|
||||||
for file in ./*
|
for file in ./*
|
||||||
do
|
do
|
||||||
sha512sum "$file" > "$file.sha512sum"
|
sha512sum "$file" > "$file.sha512sum"
|
||||||
|
14
.snapsquid.conf
Normal file
14
.snapsquid.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# In 2015, cabal-install had a http bug triggered when proxies didn't keep
|
||||||
|
# the connection open. This version made it into Ubuntu Xenial as used by
|
||||||
|
# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug.
|
||||||
|
#
|
||||||
|
# https://bugs.launchpad.net/launchpad-buildd/+bug/1797809
|
||||||
|
#
|
||||||
|
# Workaround: add more proxy
|
||||||
|
|
||||||
|
visible_hostname localhost
|
||||||
|
http_port 8888
|
||||||
|
cache_peer 10.10.10.1 parent 8222 0 no-query default
|
||||||
|
cache_peer_domain localhost !.internal
|
||||||
|
http_access allow all
|
||||||
|
|
12
.travis.yml
12
.travis.yml
@@ -11,6 +11,7 @@ before_install:
|
|||||||
- TAGS=""
|
- TAGS=""
|
||||||
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
||||||
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
||||||
|
- echo "Tags are $TAGS"
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- mkdir deploy
|
- mkdir deploy
|
||||||
@@ -29,18 +30,21 @@ script:
|
|||||||
- docker rm "$id"
|
- docker rm "$id"
|
||||||
- ls -l shellcheck
|
- ls -l shellcheck
|
||||||
- ./shellcheck myscript
|
- ./shellcheck myscript
|
||||||
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux"; done
|
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64"; done
|
||||||
# Linux Alpine based Docker image
|
# Linux Alpine based Docker image
|
||||||
- name="$DOCKER_BASE-alpine"
|
- name="$DOCKER_BASE-alpine"
|
||||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||||
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
|
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
|
||||||
- docker build -f Dockerfile.alpine -t "$name:current" .
|
- docker build -f Dockerfile.alpine -t "$name:current" .
|
||||||
- docker run "$name:current" sh -c 'shellcheck --version'
|
- docker run "$name:current" sh -c 'shellcheck --version'
|
||||||
|
# Linux armv6hf static executable
|
||||||
|
- docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
|
||||||
|
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf"; done
|
||||||
|
- rm -f shellcheck || true
|
||||||
# Windows .exe
|
# Windows .exe
|
||||||
- docker pull koalaman/winghc
|
- docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
|
||||||
- docker run --user="$UID" --rm -v "$PWD:/appdata" koalaman/winghc cuib
|
|
||||||
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
|
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
|
||||||
- rm -rf dist || true
|
- rm -rf dist shellcheck || true
|
||||||
# Misc packaging
|
# Misc packaging
|
||||||
- ./.prepare_deploy
|
- ./.prepare_deploy
|
||||||
|
|
||||||
|
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,3 +1,31 @@
|
|||||||
|
## Since previous release
|
||||||
|
### Added
|
||||||
|
- Preliminary support for fix suggestions
|
||||||
|
|
||||||
|
## 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
|
## v0.5.0 - 2018-05-31
|
||||||
### Added
|
### Added
|
||||||
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
|
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
|
||||||
|
49
Dockerfile
49
Dockerfile
@@ -1,22 +1,55 @@
|
|||||||
# Build-only image
|
# Build-only image
|
||||||
FROM ubuntu:17.10 AS build
|
FROM ubuntu:18.04 AS build
|
||||||
USER root
|
USER root
|
||||||
WORKDIR /opt/shellCheck
|
WORKDIR /opt/shellCheck
|
||||||
|
|
||||||
# Install OS deps
|
# Install OS deps, including GHC from HVR-PPA
|
||||||
RUN apt-get update && apt-get install -y ghc cabal-install
|
# https://launchpad.net/~hvr/+archive/ubuntu/ghc
|
||||||
|
RUN apt-get -yq update \
|
||||||
|
&& apt-get -yq install software-properties-common \
|
||||||
|
&& apt-add-repository -y "ppa:hvr/ghc" \
|
||||||
|
&& apt-get -yq update \
|
||||||
|
&& apt-get -yq install cabal-install-2.4 ghc-8.4.3 pandoc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PATH="/opt/ghc/bin:${PATH}"
|
||||||
|
|
||||||
|
# Use gold linker and check tools versions
|
||||||
|
RUN ln -s $(which ld.gold) /usr/local/bin/ld && \
|
||||||
|
cabal --version \
|
||||||
|
&& ghc --version \
|
||||||
|
&& ld --version
|
||||||
|
|
||||||
# Install Haskell deps
|
# Install Haskell deps
|
||||||
# (This is a separate copy/run so that source changes don't require rebuilding)
|
# (This is a separate copy/run so that source changes don't require rebuilding)
|
||||||
|
#
|
||||||
|
# We also patch regex-tdfa and aeson removing hard-coded -O2 flag.
|
||||||
|
# This makes compilation faster and binary smaller.
|
||||||
|
# Performance loss is unnoticeable for ShellCheck
|
||||||
|
#
|
||||||
|
# Remember to update versions, once in a while.
|
||||||
COPY ShellCheck.cabal ./
|
COPY ShellCheck.cabal ./
|
||||||
RUN cabal update && cabal install --dependencies-only
|
RUN cabal update && \
|
||||||
|
cabal get regex-tdfa-1.2.3.1 && sed -i 's/-O2//' regex-tdfa-1.2.3.1/regex-tdfa.cabal && \
|
||||||
|
cabal get aeson-1.4.0.0 && sed -i 's/-O2//' aeson-1.4.0.0/aeson.cabal && \
|
||||||
|
echo 'packages: . regex-tdfa-1.2.3.1 aeson-1.4.0.0 > cabal.project' && \
|
||||||
|
cabal new-build --dependencies-only \
|
||||||
|
--disable-executable-dynamic --enable-split-sections --disable-tests
|
||||||
|
|
||||||
# Copy source and build it
|
# Copy source and build it
|
||||||
COPY LICENSE Setup.hs shellcheck.hs ./
|
COPY LICENSE Setup.hs shellcheck.hs shellcheck.1.md ./
|
||||||
COPY src src
|
COPY src src
|
||||||
RUN cabal build Paths_ShellCheck && \
|
COPY test test
|
||||||
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck && \
|
# This SED is the only "nastyness" we have to do
|
||||||
strip --strip-all shellcheck
|
# Hopefully soon we could add per-component ld-options to cabal.project
|
||||||
|
RUN sed -i 's/-- STATIC/ld-options: -static -pthread -Wl,--gc-sections/' ShellCheck.cabal && \
|
||||||
|
cat ShellCheck.cabal && \
|
||||||
|
cabal new-build \
|
||||||
|
--disable-executable-dynamic --enable-split-sections --disable-tests && \
|
||||||
|
cp $(find dist-newstyle -type f -name shellcheck) . && \
|
||||||
|
strip --strip-all shellcheck && \
|
||||||
|
file shellcheck && \
|
||||||
|
ls -l shellcheck
|
||||||
|
|
||||||
RUN mkdir -p /out/bin && \
|
RUN mkdir -p /out/bin && \
|
||||||
cp shellcheck /out/bin/
|
cp shellcheck /out/bin/
|
||||||
|
10
LICENSE
10
LICENSE
@@ -1,3 +1,13 @@
|
|||||||
|
Employer mandated disclaimer:
|
||||||
|
|
||||||
|
I am providing code in the repository to you under an open source license.
|
||||||
|
Because this is my personal repository, the license you receive to my code is
|
||||||
|
from me and other individual contributors, and not my employer (Facebook).
|
||||||
|
|
||||||
|
- Vidar "koala_man" Holen
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
41
README.md
41
README.md
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
|
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
|
||||||
|
|
||||||
.
|

|
||||||
|
|
||||||
The goals of ShellCheck are
|
The goals of ShellCheck are
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
|||||||
|
|
||||||
.
|
.
|
||||||
|
|
||||||
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
|
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck) or [Flymake](https://github.com/federicotdn/flymake-shellcheck):
|
||||||
|
|
||||||
.
|
.
|
||||||
|
|
||||||
@@ -133,6 +133,10 @@ On OS X with homebrew:
|
|||||||
|
|
||||||
brew install shellcheck
|
brew install shellcheck
|
||||||
|
|
||||||
|
On OpenBSD:
|
||||||
|
|
||||||
|
pkg_add shellcheck
|
||||||
|
|
||||||
On openSUSE
|
On openSUSE
|
||||||
|
|
||||||
zypper in ShellCheck
|
zypper in ShellCheck
|
||||||
@@ -163,27 +167,35 @@ or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based imag
|
|||||||
Alternatively, you can download pre-compiled binaries for the latest release here:
|
Alternatively, you can download pre-compiled binaries for the latest release here:
|
||||||
|
|
||||||
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||||
|
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
||||||
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
|
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
|
||||||
|
|
||||||
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
|
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
|
||||||
|
|
||||||
|
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||||
|
|
||||||
|
pandoc -s -t man shellcheck.1.md -o shellcheck.1
|
||||||
|
sudo mv shellcheck.1 /usr/share/man/man1
|
||||||
|
|
||||||
## Travis CI
|
## Travis CI
|
||||||
|
|
||||||
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
|
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 the latest release:
|
If you still want to do so in order to upgrade at your leisure or ensure the latest release, follow the steps to install the shellcheck binary, bellow.
|
||||||
|
|
||||||
install:
|
## Installing the shellcheck binary
|
||||||
|
|
||||||
# Install a custom version of shellcheck instead of Travis CI's default
|
*Pre-requisite*: the program 'xz' needs to be installed on the system.
|
||||||
- scversion="stable" # or "v0.4.7", or "latest"
|
To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.
|
||||||
- wget "https://storage.googleapis.com/shellcheck/shellcheck-$scversion.linux.x86_64.tar.xz"
|
To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.
|
||||||
- tar --xz -xvf "shellcheck-$scversion.linux.x86_64.tar.xz"
|
|
||||||
- shellcheck() { "shellcheck-$scversion/shellcheck" "$@"; }
|
|
||||||
- shellcheck --version
|
|
||||||
|
|
||||||
script:
|
```bash
|
||||||
- shellcheck *.sh
|
export scversion="stable" # or "v0.4.7", or "latest"
|
||||||
|
wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz"
|
||||||
|
tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz
|
||||||
|
cp shellcheck-"${scversion}"/shellcheck /usr/bin/
|
||||||
|
shellcheck --version
|
||||||
|
```
|
||||||
|
|
||||||
## Compiling from source
|
## Compiling from source
|
||||||
|
|
||||||
@@ -442,3 +454,8 @@ ShellCheck is licensed under the GNU General Public License, v3. A copy of this
|
|||||||
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
|
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
|
||||||
|
|
||||||
Happy ShellChecking!
|
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)!
|
||||||
|
49
Setup.hs
49
Setup.hs
@@ -1,3 +1,8 @@
|
|||||||
|
{-# LANGUAGE CPP #-}
|
||||||
|
{-# OPTIONS_GHC -Wall #-}
|
||||||
|
|
||||||
|
module Main (main) where
|
||||||
|
|
||||||
import Distribution.PackageDescription (
|
import Distribution.PackageDescription (
|
||||||
HookedBuildInfo,
|
HookedBuildInfo,
|
||||||
emptyHookedBuildInfo )
|
emptyHookedBuildInfo )
|
||||||
@@ -9,12 +14,42 @@ import Distribution.Simple (
|
|||||||
import Distribution.Simple.Setup ( SDistFlags )
|
import Distribution.Simple.Setup ( SDistFlags )
|
||||||
|
|
||||||
import System.Process ( system )
|
import System.Process ( system )
|
||||||
|
import System.Directory ( doesFileExist, getModificationTime )
|
||||||
|
|
||||||
|
#ifndef MIN_VERSION_cabal_doctest
|
||||||
|
#define MIN_VERSION_cabal_doctest(x,y,z) 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if MIN_VERSION_cabal_doctest(1,0,0)
|
||||||
|
|
||||||
|
import Distribution.Extra.Doctest ( addDoctestsUserHook )
|
||||||
|
main :: IO ()
|
||||||
|
main = defaultMainWithHooks $ addDoctestsUserHook "doctests" myHooks
|
||||||
|
where
|
||||||
|
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#ifdef MIN_VERSION_Cabal
|
||||||
|
-- If the macro is defined, we have new cabal-install,
|
||||||
|
-- but for some reason we don't have cabal-doctest in package-db
|
||||||
|
--
|
||||||
|
-- Probably we are running cabal sdist, when otherwise using new-build
|
||||||
|
-- workflow
|
||||||
|
#warning You are configuring this package without cabal-doctest installed. \
|
||||||
|
The doctests test-suite will not work as a result. \
|
||||||
|
To fix this, install cabal-doctest before configuring.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
main = defaultMainWithHooks myHooks
|
main = defaultMainWithHooks myHooks
|
||||||
where
|
where
|
||||||
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- | This hook will be executed before e.g. @cabal sdist@. It runs
|
-- | This hook will be executed before e.g. @cabal sdist@. It runs
|
||||||
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
|
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
|
||||||
-- command is not found, this will fail with an error message:
|
-- command is not found, this will fail with an error message:
|
||||||
@@ -27,10 +62,20 @@ main = defaultMainWithHooks myHooks
|
|||||||
--
|
--
|
||||||
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
|
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
|
||||||
myPreSDist _ _ = do
|
myPreSDist _ _ = do
|
||||||
|
exists <- doesFileExist "shellcheck.1"
|
||||||
|
if exists
|
||||||
|
then do
|
||||||
|
source <- getModificationTime "shellcheck.1.md"
|
||||||
|
target <- getModificationTime "shellcheck.1"
|
||||||
|
if target < source
|
||||||
|
then makeManPage
|
||||||
|
else putStrLn "shellcheck.1 is more recent than shellcheck.1.md"
|
||||||
|
else makeManPage
|
||||||
|
return emptyHookedBuildInfo
|
||||||
|
where
|
||||||
|
makeManPage = do
|
||||||
putStrLn "Building the man page (shellcheck.1) with pandoc..."
|
putStrLn "Building the man page (shellcheck.1) with pandoc..."
|
||||||
putStrLn pandoc_cmd
|
putStrLn pandoc_cmd
|
||||||
result <- system pandoc_cmd
|
result <- system pandoc_cmd
|
||||||
putStrLn $ "pandoc exited with " ++ show result
|
putStrLn $ "pandoc exited with " ++ show result
|
||||||
return emptyHookedBuildInfo
|
|
||||||
where
|
|
||||||
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
Version: 0.5.0
|
Version: 0.6.0
|
||||||
Synopsis: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
License: GPL-3
|
License: GPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
@@ -28,14 +28,14 @@ Extra-Source-Files:
|
|||||||
shellcheck.1.md
|
shellcheck.1.md
|
||||||
-- built with a cabal sdist hook
|
-- built with a cabal sdist hook
|
||||||
shellcheck.1
|
shellcheck.1
|
||||||
-- tests
|
|
||||||
test/shellcheck.hs
|
|
||||||
|
|
||||||
custom-setup
|
custom-setup
|
||||||
setup-depends:
|
setup-depends:
|
||||||
base >= 4 && <5,
|
base >= 4 && <5,
|
||||||
|
directory >= 1.2 && <1.4,
|
||||||
process >= 1.0 && <1.7,
|
process >= 1.0 && <1.7,
|
||||||
Cabal >= 1.10 && <2.3
|
cabal-doctest >= 1.0.6 && <1.1,
|
||||||
|
Cabal >= 1.10 && <2.5
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
@@ -53,11 +53,11 @@ library
|
|||||||
base > 4.6.0.1 && < 5,
|
base > 4.6.0.1 && < 5,
|
||||||
bytestring,
|
bytestring,
|
||||||
containers >= 0.5,
|
containers >= 0.5,
|
||||||
|
deepseq >= 1.4.0.0,
|
||||||
directory,
|
directory,
|
||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec,
|
||||||
regex-tdfa,
|
regex-tdfa,
|
||||||
QuickCheck >= 2.7.4,
|
|
||||||
-- When cabal supports it, move this to setup-depends:
|
-- When cabal supports it, move this to setup-depends:
|
||||||
process
|
process
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
@@ -89,27 +89,29 @@ executable shellcheck
|
|||||||
aeson,
|
aeson,
|
||||||
base >= 4 && < 5,
|
base >= 4 && < 5,
|
||||||
bytestring,
|
bytestring,
|
||||||
|
deepseq >= 1.4.0.0,
|
||||||
ShellCheck,
|
ShellCheck,
|
||||||
containers,
|
containers,
|
||||||
directory,
|
directory,
|
||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec >= 3.0,
|
parsec >= 3.0,
|
||||||
QuickCheck >= 2.7.4,
|
|
||||||
regex-tdfa
|
regex-tdfa
|
||||||
main-is: shellcheck.hs
|
main-is: shellcheck.hs
|
||||||
|
|
||||||
test-suite test-shellcheck
|
-- Marker to add flags for static linking
|
||||||
type: exitcode-stdio-1.0
|
-- STATIC
|
||||||
build-depends:
|
|
||||||
aeson,
|
|
||||||
base >= 4 && < 5,
|
|
||||||
bytestring,
|
|
||||||
ShellCheck,
|
|
||||||
containers,
|
|
||||||
directory,
|
|
||||||
mtl >= 2.2.1,
|
|
||||||
parsec,
|
|
||||||
QuickCheck >= 2.7.4,
|
|
||||||
regex-tdfa
|
|
||||||
main-is: test/shellcheck.hs
|
|
||||||
|
|
||||||
|
test-suite doctests
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
main-is: doctests.hs
|
||||||
|
build-depends:
|
||||||
|
base,
|
||||||
|
doctest >= 0.16.0 && <0.17,
|
||||||
|
QuickCheck >=2.11 && <2.13,
|
||||||
|
ShellCheck,
|
||||||
|
template-haskell
|
||||||
|
|
||||||
|
x-doctest-options: --fast
|
||||||
|
|
||||||
|
ghc-options: -Wall -threaded
|
||||||
|
hs-source-dirs: test
|
||||||
|
9
quickrun
9
quickrun
@@ -1,5 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# quickrun runs ShellCheck in an interpreted mode.
|
# quickrun runs ShellCheck in an interpreted mode.
|
||||||
# This allows testing changes without recompiling.
|
# This allows testing changes without recompiling.
|
||||||
|
|
||||||
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
|
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
|
||||||
|
|
||||||
|
# Note: with new-build you can
|
||||||
|
#
|
||||||
|
# % cabal new-run --disable-optimization -- shellcheck "$@"
|
||||||
|
#
|
||||||
|
# This does build the executable, but as the optimisation is disabled,
|
||||||
|
# the build is quite fast.
|
||||||
|
37
quicktest
37
quicktest
@@ -1,22 +1,21 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# quicktest runs the ShellCheck unit tests in an interpreted mode.
|
# shellcheck disable=SC2091
|
||||||
# This allows running tests without compiling, which can be faster.
|
|
||||||
|
# quicktest runs the ShellCheck unit tests.
|
||||||
|
# Once `doctests` test executable is build, we can just run it
|
||||||
|
# This allows running tests without compiling library, which is faster.
|
||||||
# 'cabal test' remains the source of truth.
|
# 'cabal test' remains the source of truth.
|
||||||
|
|
||||||
(
|
$(find dist -type f -name doctests)
|
||||||
var=$(echo 'liftM and $ sequence [
|
|
||||||
ShellCheck.Analytics.runTests
|
# Note: if you have build the project with new-build
|
||||||
,ShellCheck.Parser.runTests
|
#
|
||||||
,ShellCheck.Checker.runTests
|
# % cabal new-build -w ghc-8.4.3 --enable-tests
|
||||||
,ShellCheck.Checks.Commands.runTests
|
#
|
||||||
,ShellCheck.Checks.ShellSupport.runTests
|
# and have cabal-plan installed (e.g. with cabal new-install cabal-plan),
|
||||||
,ShellCheck.AnalyzerLib.runTests
|
# then you can quicktest with
|
||||||
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
|
#
|
||||||
if [[ $var == *$'\nTrue'* ]]
|
# % $(cabal-plan list-bin doctests)
|
||||||
then
|
#
|
||||||
exit 0
|
# Once the test executable exists, we can simply run it to perform doctests
|
||||||
else
|
# which use GHCi under the hood.
|
||||||
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
) 2>&1
|
|
||||||
|
@@ -56,6 +56,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||||||
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
standard output. Subsequent **-f** options are ignored, see **FORMATS**
|
||||||
below for more information.
|
below for more information.
|
||||||
|
|
||||||
|
**-S**\ *SEVERITY*,\ **--severity=***severity*
|
||||||
|
|
||||||
|
: Specify minimum severity of errors to consider. Valid values are *error*,
|
||||||
|
*warning*, *info* and *style*. The default is *style*.
|
||||||
|
|
||||||
**-s**\ *shell*,\ **--shell=***shell*
|
**-s**\ *shell*,\ **--shell=***shell*
|
||||||
|
|
||||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
||||||
@@ -66,6 +71,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||||||
|
|
||||||
: Print version information and exit.
|
: 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**
|
**-x**,\ **--external-sources**
|
||||||
|
|
||||||
: Follow 'source' statements even when the file is not specified as input.
|
: Follow 'source' statements even when the file is not specified as input.
|
||||||
|
@@ -67,15 +67,17 @@ instance Monoid Status where
|
|||||||
data Options = Options {
|
data Options = Options {
|
||||||
checkSpec :: CheckSpec,
|
checkSpec :: CheckSpec,
|
||||||
externalSources :: Bool,
|
externalSources :: Bool,
|
||||||
formatterOptions :: FormatterOptions
|
formatterOptions :: FormatterOptions,
|
||||||
|
minSeverity :: Severity
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultOptions = Options {
|
defaultOptions = Options {
|
||||||
checkSpec = emptyCheckSpec,
|
checkSpec = emptyCheckSpec,
|
||||||
externalSources = False,
|
externalSources = False,
|
||||||
formatterOptions = FormatterOptions {
|
formatterOptions = newFormatterOptions {
|
||||||
foColorOption = ColorAuto
|
foColorOption = ColorAuto
|
||||||
}
|
},
|
||||||
|
minSeverity = StyleC
|
||||||
}
|
}
|
||||||
|
|
||||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||||
@@ -93,8 +95,14 @@ options = [
|
|||||||
Option "s" ["shell"]
|
Option "s" ["shell"]
|
||||||
(ReqArg (Flag "shell") "SHELLNAME")
|
(ReqArg (Flag "shell") "SHELLNAME")
|
||||||
"Specify dialect (sh, bash, dash, ksh)",
|
"Specify dialect (sh, bash, dash, ksh)",
|
||||||
|
Option "S" ["severity"]
|
||||||
|
(ReqArg (Flag "severity") "SEVERITY")
|
||||||
|
"Minimum severity of errors to consider (error, warning, info, style)",
|
||||||
Option "V" ["version"]
|
Option "V" ["version"]
|
||||||
(NoArg $ Flag "version" "true") "Print version information",
|
(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"]
|
Option "x" ["external-sources"]
|
||||||
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
|
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
|
||||||
]
|
]
|
||||||
@@ -137,12 +145,6 @@ split char str =
|
|||||||
else split' rest (a:element)
|
else split' rest (a:element)
|
||||||
split' [] element = [reverse element]
|
split' [] element = [reverse element]
|
||||||
|
|
||||||
getExclusions options =
|
|
||||||
let elements = concatMap (split ',') $ getOptions options "exclude"
|
|
||||||
clean = dropWhile (not . isDigit)
|
|
||||||
in
|
|
||||||
map (Prelude.read . clean) elements :: [Int]
|
|
||||||
|
|
||||||
toStatus = fmap (either id id) . runExceptT
|
toStatus = fmap (either id id) . runExceptT
|
||||||
|
|
||||||
getEnvArgs = do
|
getEnvArgs = do
|
||||||
@@ -222,12 +224,28 @@ runFormatter sys format options files = do
|
|||||||
then NoProblems
|
then NoProblems
|
||||||
else SomeProblems
|
else SomeProblems
|
||||||
|
|
||||||
parseColorOption colorOption =
|
parseEnum name value list =
|
||||||
case colorOption of
|
case filter ((== value) . fst) list of
|
||||||
"auto" -> ColorAuto
|
[(name, value)] -> return value
|
||||||
"always" -> ColorAlways
|
[] -> do
|
||||||
"never" -> ColorNever
|
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
||||||
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
|
"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 =
|
parseOption flag options =
|
||||||
case flag of
|
case flag of
|
||||||
@@ -241,7 +259,7 @@ parseOption flag options =
|
|||||||
}
|
}
|
||||||
|
|
||||||
Flag "exclude" str -> do
|
Flag "exclude" str -> do
|
||||||
new <- mapM parseNum $ split ',' str
|
new <- mapM parseNum $ filter (not . null) $ split ',' str
|
||||||
let old = csExcludedWarnings . checkSpec $ options
|
let old = csExcludedWarnings . checkSpec $ options
|
||||||
return options {
|
return options {
|
||||||
checkSpec = (checkSpec options) {
|
checkSpec = (checkSpec options) {
|
||||||
@@ -258,10 +276,11 @@ parseOption flag options =
|
|||||||
externalSources = True
|
externalSources = True
|
||||||
}
|
}
|
||||||
|
|
||||||
Flag "color" color ->
|
Flag "color" color -> do
|
||||||
|
option <- parseColorOption color
|
||||||
return options {
|
return options {
|
||||||
formatterOptions = (formatterOptions options) {
|
formatterOptions = (formatterOptions options) {
|
||||||
foColorOption = parseColorOption color
|
foColorOption = option
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +291,22 @@ parseOption flag options =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ -> return options
|
_ -> return options
|
||||||
where
|
where
|
||||||
die s = do
|
die s = do
|
||||||
@@ -280,7 +315,7 @@ parseOption flag options =
|
|||||||
parseNum ('S':'C':str) = parseNum str
|
parseNum ('S':'C':str) = parseNum str
|
||||||
parseNum num = do
|
parseNum num = do
|
||||||
unless (all isDigit num) $ do
|
unless (all isDigit num) $ do
|
||||||
printErr $ "Bad exclusion: " ++ num
|
printErr $ "Invalid number: " ++ num
|
||||||
throwError SyntaxFailure
|
throwError SyntaxFailure
|
||||||
return (Prelude.read num :: Integer)
|
return (Prelude.read num :: Integer)
|
||||||
|
|
||||||
|
@@ -37,9 +37,16 @@ parts:
|
|||||||
source: ./
|
source: ./
|
||||||
build-packages:
|
build-packages:
|
||||||
- cabal-install
|
- cabal-install
|
||||||
|
- squid3
|
||||||
build: |
|
build: |
|
||||||
|
# See comments in .snapsquid.conf
|
||||||
|
[ "$http_proxy" ] && {
|
||||||
|
squid3 -f .snapsquid.conf
|
||||||
|
export http_proxy="http://localhost:8888"
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
cabal sandbox init
|
cabal sandbox init
|
||||||
cabal update
|
cabal update || cat /var/log/squid/*
|
||||||
cabal install -j
|
cabal install -j
|
||||||
install: |
|
install: |
|
||||||
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
||||||
|
@@ -17,14 +17,17 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
|
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
|
||||||
module ShellCheck.AST where
|
module ShellCheck.AST where
|
||||||
|
|
||||||
|
import GHC.Generics (Generic)
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
|
import Control.DeepSeq
|
||||||
import Text.Parsec
|
import Text.Parsec
|
||||||
import qualified ShellCheck.Regex as Re
|
import qualified ShellCheck.Regex as Re
|
||||||
import Prelude hiding (id)
|
import Prelude hiding (id)
|
||||||
|
|
||||||
newtype Id = Id Int deriving (Show, Eq, Ord)
|
newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData)
|
||||||
|
|
||||||
data Quoted = Quoted | Unquoted deriving (Show, Eq)
|
data Quoted = Quoted | Unquoted deriving (Show, Eq)
|
||||||
data Dashed = Dashed | Undashed deriving (Show, Eq)
|
data Dashed = Dashed | Undashed deriving (Show, Eq)
|
||||||
|
@@ -23,6 +23,7 @@ import ShellCheck.AST
|
|||||||
|
|
||||||
import Control.Monad.Writer
|
import Control.Monad.Writer
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Data.Char
|
||||||
import Data.Functor
|
import Data.Functor
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
@@ -226,8 +227,43 @@ getLiteralStringExt more = g
|
|||||||
g (T_SingleQuoted _ s) = return s
|
g (T_SingleQuoted _ s) = return s
|
||||||
g (T_Literal _ s) = return s
|
g (T_Literal _ s) = return s
|
||||||
g (T_ParamSubSpecialChar _ s) = return s
|
g (T_ParamSubSpecialChar _ s) = return s
|
||||||
|
g (T_DollarSingleQuoted _ s) = return $ decodeEscapes s
|
||||||
g x = more x
|
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) ->
|
||||||
|
if isHexDigit x && isHexDigit y
|
||||||
|
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
||||||
|
else '\\':c:rest
|
||||||
|
_ | isOctDigit c ->
|
||||||
|
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
||||||
|
num = parseOct digits
|
||||||
|
in (if num < 256 then chr num else '?') : rest
|
||||||
|
_ -> '\\' : c : rest
|
||||||
|
where
|
||||||
|
rest = decodeEscapes cs
|
||||||
|
parseOct = f 0
|
||||||
|
where
|
||||||
|
f n "" = n
|
||||||
|
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
||||||
|
decodeEscapes (c:cs) = c : decodeEscapes cs
|
||||||
|
decodeEscapes [] = []
|
||||||
|
|
||||||
-- Is this token a string literal?
|
-- Is this token a string literal?
|
||||||
isLiteral t = isJust $ getLiteralString t
|
isLiteral t = isJust $ getLiteralString t
|
||||||
|
|
||||||
@@ -257,17 +293,27 @@ getCommand t =
|
|||||||
T_Annotation _ _ t -> getCommand t
|
T_Annotation _ _ t -> getCommand t
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
-- Maybe get the command name of a token representing a command
|
-- Maybe get the command name string of a token representing a command
|
||||||
getCommandName t = do
|
getCommandName :: Token -> Maybe String
|
||||||
|
getCommandName = fst . getCommandNameAndToken
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
getCommandNameAndToken :: Token -> (Maybe String, Token)
|
||||||
|
getCommandNameAndToken t = fromMaybe (Nothing, t) $ do
|
||||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||||
s <- getLiteralString w
|
s <- getLiteralString w
|
||||||
if "busybox" `isSuffixOf` s || "builtin" == s
|
if "busybox" `isSuffixOf` s || "builtin" == s
|
||||||
then
|
then
|
||||||
case rest of
|
case rest of
|
||||||
(applet:_) -> getLiteralString applet
|
(applet:_) -> return (getLiteralString applet, applet)
|
||||||
_ -> return s
|
_ -> return (Just s, w)
|
||||||
else
|
else
|
||||||
return s
|
return (Just s, w)
|
||||||
|
|
||||||
|
|
||||||
-- If a command substitution is a single command, get its name.
|
-- If a command substitution is a single command, get its name.
|
||||||
-- $(date +%s) = Just "date"
|
-- $(date +%s) = Just "date"
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ import qualified ShellCheck.Checks.ShellSupport
|
|||||||
|
|
||||||
-- TODO: Clean up the cruft this is layered on
|
-- TODO: Clean up the cruft this is layered on
|
||||||
analyzeScript :: AnalysisSpec -> AnalysisResult
|
analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||||
analyzeScript spec = AnalysisResult {
|
analyzeScript spec = newAnalysisResult {
|
||||||
arComments =
|
arComments =
|
||||||
filterByAnnotation spec params . nub $
|
filterByAnnotation spec params . nub $
|
||||||
runAnalytics spec
|
runAnalytics spec
|
||||||
|
@@ -18,7 +18,6 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
module ShellCheck.AnalyzerLib where
|
module ShellCheck.AnalyzerLib where
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
@@ -28,6 +27,7 @@ import ShellCheck.Parser
|
|||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Arrow (first)
|
import Control.Arrow (first)
|
||||||
|
import Control.DeepSeq
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
import Control.Monad.RWS
|
import Control.Monad.RWS
|
||||||
import Control.Monad.State
|
import Control.Monad.State
|
||||||
@@ -38,8 +38,9 @@ import qualified Data.Map as Map
|
|||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Semigroup
|
import Data.Semigroup
|
||||||
|
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
prop :: Bool -> IO ()
|
||||||
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
|
prop False = putStrLn "FAIL"
|
||||||
|
prop True = return ()
|
||||||
|
|
||||||
type Analysis = AnalyzerM ()
|
type Analysis = AnalyzerM ()
|
||||||
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
||||||
@@ -81,8 +82,9 @@ data Parameters = Parameters {
|
|||||||
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
|
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
|
||||||
shellType :: Shell, -- The shell type, such as Bash or Ksh
|
shellType :: Shell, -- The shell type, such as Bash or Ksh
|
||||||
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
|
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
|
||||||
rootNode :: Token -- The root node of the AST
|
rootNode :: Token, -- The root node of the AST
|
||||||
}
|
tokenPositions :: Map.Map Id (Position, Position) -- map from token id to start and end position
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
-- TODO: Cache results of common AST ops here
|
-- TODO: Cache results of common AST ops here
|
||||||
data Cache = Cache {}
|
data Cache = Cache {}
|
||||||
@@ -109,35 +111,42 @@ data DataSource =
|
|||||||
|
|
||||||
data VariableState = Dead Token String | Alive deriving (Show)
|
data VariableState = Dead Token String | Alive deriving (Show)
|
||||||
|
|
||||||
defaultSpec root = AnalysisSpec {
|
defaultSpec pr = spec {
|
||||||
asScript = root,
|
|
||||||
asShellType = Nothing,
|
asShellType = Nothing,
|
||||||
asCheckSourced = False,
|
asCheckSourced = False,
|
||||||
asExecutionMode = Executed
|
asExecutionMode = Executed,
|
||||||
}
|
asTokenPositions = prTokenPositions pr
|
||||||
|
} where spec = newAnalysisSpec (fromJust $ prRoot pr)
|
||||||
|
|
||||||
pScript s =
|
pScript s =
|
||||||
let
|
let
|
||||||
pSpec = ParseSpec {
|
pSpec = newParseSpec {
|
||||||
psFilename = "script",
|
psFilename = "script",
|
||||||
psScript = s,
|
psScript = s
|
||||||
psCheckSourced = False
|
|
||||||
}
|
}
|
||||||
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
in runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||||
|
|
||||||
-- For testing. If parsed, returns whether there are any comments
|
-- For testing. If parsed, returns whether there are any comments
|
||||||
producesComments :: Checker -> String -> Maybe Bool
|
producesComments :: Checker -> String -> Maybe Bool
|
||||||
producesComments c s = do
|
producesComments c s = do
|
||||||
root <- pScript s
|
let pr = pScript s
|
||||||
let spec = defaultSpec root
|
prRoot pr
|
||||||
|
let spec = defaultSpec pr
|
||||||
let params = makeParameters spec
|
let params = makeParameters spec
|
||||||
return . not . null $ runChecker params c
|
return . not . null $ runChecker params c
|
||||||
|
|
||||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||||
makeComment severity id code note =
|
makeComment severity id code note =
|
||||||
TokenComment id $ Comment severity code note
|
newTokenComment {
|
||||||
|
tcId = id,
|
||||||
|
tcComment = newComment {
|
||||||
|
cSeverity = severity,
|
||||||
|
cCode = code,
|
||||||
|
cMessage = note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addComment note = tell [note]
|
addComment note = note `deepseq` tell [note]
|
||||||
|
|
||||||
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
|
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
|
||||||
warn id code str = addComment $ makeComment WarningC id code str
|
warn id code str = addComment $ makeComment WarningC id code str
|
||||||
@@ -145,6 +154,20 @@ err id code str = addComment $ makeComment ErrorC id code str
|
|||||||
info id code str = addComment $ makeComment InfoC id code str
|
info id code str = addComment $ makeComment InfoC id code str
|
||||||
style id code str = addComment $ makeComment StyleC id code str
|
style id code str = addComment $ makeComment StyleC id code str
|
||||||
|
|
||||||
|
warnWithFix id code str fix = addComment $
|
||||||
|
let comment = makeComment WarningC id code str in
|
||||||
|
comment {
|
||||||
|
tcFix = Just fix
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
|
||||||
|
makeCommentWithFix severity id code str fix =
|
||||||
|
let comment = makeComment severity id code str
|
||||||
|
withFix = comment {
|
||||||
|
tcFix = Just fix
|
||||||
|
}
|
||||||
|
in withFix `deepseq` withFix
|
||||||
|
|
||||||
makeParameters spec =
|
makeParameters spec =
|
||||||
let params = Parameters {
|
let params = Parameters {
|
||||||
rootNode = root,
|
rootNode = root,
|
||||||
@@ -159,7 +182,8 @@ makeParameters spec =
|
|||||||
|
|
||||||
shellTypeSpecified = isJust $ asShellType spec,
|
shellTypeSpecified = isJust $ asShellType spec,
|
||||||
parentMap = getParentTree root,
|
parentMap = getParentTree root,
|
||||||
variableFlow = getVariableFlow params root
|
variableFlow = getVariableFlow params root,
|
||||||
|
tokenPositions = asTokenPositions spec
|
||||||
} in params
|
} in params
|
||||||
where root = asScript spec
|
where root = asScript spec
|
||||||
|
|
||||||
@@ -191,16 +215,16 @@ containsLastpipe root =
|
|||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
|
||||||
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
|
-- |
|
||||||
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
|
-- >>> prop $ determineShellTest "#!/bin/sh" == Sh
|
||||||
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
|
-- >>> prop $ determineShellTest "#!/usr/bin/env ksh" == Ksh
|
||||||
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
|
-- >>> prop $ determineShellTest "" == Bash
|
||||||
prop_determineShell4 = determineShell (fromJust $ pScript
|
-- >>> prop $ determineShellTest "#!/bin/sh -e" == Sh
|
||||||
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
|
-- >>> prop $ determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
|
||||||
prop_determineShell5 = determineShell (fromJust $ pScript
|
-- >>> prop $ determineShellTest "#shellcheck shell=sh\nfoo" == Sh
|
||||||
"#shellcheck shell=sh\nfoo") == Sh
|
-- >>> prop $ determineShellTest "#! /bin/sh" == Sh
|
||||||
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
|
-- >>> prop $ determineShellTest "#! /bin/ash" == Dash
|
||||||
prop_determineShell7 = determineShell (fromJust $ pScript "#! /bin/ash") == Dash
|
determineShellTest = determineShell . fromJust . prRoot . pScript
|
||||||
determineShell t = fromMaybe Bash $ do
|
determineShell t = fromMaybe Bash $ do
|
||||||
shellString <- foldl mplus Nothing $ getCandidates t
|
shellString <- foldl mplus Nothing $ getCandidates t
|
||||||
shellForExecutable shellString
|
shellForExecutable shellString
|
||||||
@@ -235,8 +259,9 @@ getParentTree t =
|
|||||||
where
|
where
|
||||||
pre t = modify (first ((:) t))
|
pre t = modify (first ((:) t))
|
||||||
post t = do
|
post t = do
|
||||||
(_:rest, map) <- get
|
(x, map) <- get
|
||||||
case rest of [] -> put (rest, map)
|
case x of
|
||||||
|
_:rest -> case rest of [] -> put (rest, map)
|
||||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||||
|
|
||||||
-- Given a root node, make a map from Id to Token
|
-- Given a root node, make a map from Id to Token
|
||||||
@@ -520,12 +545,22 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
|||||||
|
|
||||||
getReferencedVariableCommand _ = []
|
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 _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||||
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
||||||
case x of
|
case x of
|
||||||
"read" ->
|
"read" ->
|
||||||
let params = map getLiteral rest in
|
let params = map getLiteral rest
|
||||||
catMaybes . takeWhile isJust . reverse $ params
|
readArrayVars = getReadArrayVariables rest
|
||||||
|
in
|
||||||
|
catMaybes . (++ readArrayVars) . takeWhile isJust . reverse $ params
|
||||||
"getopts" ->
|
"getopts" ->
|
||||||
case rest of
|
case rest of
|
||||||
opts:var:_ -> maybeToList $ getLiteral var
|
opts:var:_ -> maybeToList $ getLiteral var
|
||||||
@@ -568,10 +603,14 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
|||||||
where
|
where
|
||||||
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||||
|
|
||||||
getLiteral t = do
|
getLiteralOfDataType t d = do
|
||||||
s <- getLiteralString t
|
s <- getLiteralString t
|
||||||
when ("-" `isPrefixOf` s) $ fail "argument"
|
when ("-" `isPrefixOf` s) $ fail "argument"
|
||||||
return (base, t, s, DataString SourceExternal)
|
return (base, t, s, d)
|
||||||
|
|
||||||
|
getLiteral t = getLiteralOfDataType t (DataString SourceExternal)
|
||||||
|
|
||||||
|
getLiteralArray t = getLiteralOfDataType t (DataArray SourceExternal)
|
||||||
|
|
||||||
getModifierParamString = getModifierParam DataString
|
getModifierParamString = getModifierParam DataString
|
||||||
|
|
||||||
@@ -613,6 +652,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
|||||||
guard $ isVariableName name
|
guard $ isVariableName name
|
||||||
return (base, lastArg, name, DataArray SourceExternal)
|
return (base, lastArg, name, DataArray SourceExternal)
|
||||||
|
|
||||||
|
-- get all the array variables used in read, e.g. read -a arr
|
||||||
|
getReadArrayVariables args = do
|
||||||
|
map (getLiteralArray . snd)
|
||||||
|
(filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
|
||||||
|
|
||||||
getModifiedVariableCommand _ = []
|
getModifiedVariableCommand _ = []
|
||||||
|
|
||||||
getIndexReferences s = fromMaybe [] $ do
|
getIndexReferences s = fromMaybe [] $ do
|
||||||
@@ -622,10 +666,11 @@ getIndexReferences s = fromMaybe [] $ do
|
|||||||
where
|
where
|
||||||
re = mkRegex "(\\[.*\\])"
|
re = mkRegex "(\\[.*\\])"
|
||||||
|
|
||||||
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
-- |
|
||||||
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
-- >>> prop $ getOffsetReferences ":bar" == ["bar"]
|
||||||
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
-- >>> prop $ getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||||
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
-- >>> prop $ getOffsetReferences "[foo]:bar" == ["bar"]
|
||||||
|
-- >>> prop $ getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||||
getOffsetReferences mods = fromMaybe [] $ do
|
getOffsetReferences mods = fromMaybe [] $ do
|
||||||
-- if mods start with [, then drop until ]
|
-- if mods start with [, then drop until ]
|
||||||
match <- matchRegex re mods
|
match <- matchRegex re mods
|
||||||
@@ -700,9 +745,15 @@ isUnqualifiedCommand token str = isCommandMatch token (== str)
|
|||||||
isCommandMatch token matcher = fromMaybe False $
|
isCommandMatch token matcher = fromMaybe False $
|
||||||
fmap matcher (getCommandName token)
|
fmap matcher (getCommandName token)
|
||||||
|
|
||||||
|
-- |
|
||||||
-- Does this regex look like it was intended as a glob?
|
-- Does this regex look like it was intended as a glob?
|
||||||
-- True: *foo*
|
--
|
||||||
-- False: .*foo.*
|
-- >>> isConfusedGlobRegex "*foo*"
|
||||||
|
-- True
|
||||||
|
--
|
||||||
|
-- >>> isConfusedGlobRegex ".*foo.*"
|
||||||
|
-- False
|
||||||
|
--
|
||||||
isConfusedGlobRegex :: String -> Bool
|
isConfusedGlobRegex :: String -> Bool
|
||||||
isConfusedGlobRegex ('*':_) = True
|
isConfusedGlobRegex ('*':_) = True
|
||||||
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
||||||
@@ -712,9 +763,10 @@ isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
|||||||
isVariableChar x = isVariableStartChar x || isDigit x
|
isVariableChar x = isVariableStartChar x || isDigit x
|
||||||
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||||
|
|
||||||
prop_isVariableName1 = isVariableName "_fo123"
|
-- |
|
||||||
prop_isVariableName2 = not $ isVariableName "4"
|
-- >>> prop $ isVariableName "_fo123"
|
||||||
prop_isVariableName3 = not $ isVariableName "test: "
|
-- >>> prop $ not $ isVariableName "4"
|
||||||
|
-- >>> prop $ not $ isVariableName "test: "
|
||||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||||
isVariableName _ = False
|
isVariableName _ = False
|
||||||
|
|
||||||
@@ -723,27 +775,28 @@ getVariablesFromLiteralToken token =
|
|||||||
|
|
||||||
-- Try to get referenced variables from a literal string like "$foo"
|
-- Try to get referenced variables from a literal string like "$foo"
|
||||||
-- Ignores tons of cases like arithmetic evaluation and array indices.
|
-- Ignores tons of cases like arithmetic evaluation and array indices.
|
||||||
prop_getVariablesFromLiteral1 =
|
-- >>> prop $ getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
||||||
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
|
||||||
getVariablesFromLiteral string =
|
getVariablesFromLiteral string =
|
||||||
map (!! 0) $ matchAllSubgroups variableRegex string
|
map (!! 0) $ matchAllSubgroups variableRegex string
|
||||||
where
|
where
|
||||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||||
|
|
||||||
|
-- |
|
||||||
-- Get the variable name from an expansion like ${var:-foo}
|
-- Get the variable name from an expansion like ${var:-foo}
|
||||||
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
--
|
||||||
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
-- >>> prop $ getBracedReference "foo" == "foo"
|
||||||
prop_getBracedReference3 = getBracedReference "#" == "#"
|
-- >>> prop $ getBracedReference "#foo" == "foo"
|
||||||
prop_getBracedReference4 = getBracedReference "##" == "#"
|
-- >>> prop $ getBracedReference "#" == "#"
|
||||||
prop_getBracedReference5 = getBracedReference "#!" == "!"
|
-- >>> prop $ getBracedReference "##" == "#"
|
||||||
prop_getBracedReference6 = getBracedReference "!#" == "#"
|
-- >>> prop $ getBracedReference "#!" == "!"
|
||||||
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
|
-- >>> prop $ getBracedReference "!#" == "#"
|
||||||
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
-- >>> prop $ getBracedReference "!foo#?" == "foo"
|
||||||
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
-- >>> prop $ getBracedReference "foo-bar" == "foo"
|
||||||
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
|
-- >>> prop $ getBracedReference "foo:-bar" == "foo"
|
||||||
prop_getBracedReference11= getBracedReference "!os*" == ""
|
-- >>> prop $ getBracedReference "foo: -1" == "foo"
|
||||||
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
|
-- >>> prop $ getBracedReference "!os*" == ""
|
||||||
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
|
-- >>> prop $ getBracedReference "!os?bar**" == ""
|
||||||
|
-- >>> prop $ getBracedReference "foo[bar]" == "foo"
|
||||||
getBracedReference s = fromMaybe s $
|
getBracedReference s = fromMaybe s $
|
||||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||||
where
|
where
|
||||||
@@ -766,9 +819,10 @@ getBracedReference s = fromMaybe s $
|
|||||||
return ""
|
return ""
|
||||||
nameExpansion _ = Nothing
|
nameExpansion _ = Nothing
|
||||||
|
|
||||||
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
-- |
|
||||||
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
-- >>> prop $ getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||||
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
-- >>> prop $ getBracedModifier "!var:-foo" == ":-foo"
|
||||||
|
-- >>> prop $ getBracedModifier "foo[bar]" == "[bar]"
|
||||||
getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||||
let var = getBracedReference s
|
let var = getBracedReference s
|
||||||
a <- dropModifier s
|
a <- dropModifier s
|
||||||
@@ -785,10 +839,13 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
|||||||
|
|
||||||
-- Run an action in a Maybe (or do nothing).
|
-- Run an action in a Maybe (or do nothing).
|
||||||
-- Example:
|
-- Example:
|
||||||
|
--
|
||||||
|
-- @
|
||||||
-- potentially $ do
|
-- potentially $ do
|
||||||
-- s <- getLiteralString cmd
|
-- s <- getLiteralString cmd
|
||||||
-- guard $ s `elem` ["--recursive", "-r"]
|
-- guard $ s `elem` ["--recursive", "-r"]
|
||||||
-- return $ warn .. "Something something recursive"
|
-- return $ warn .. "Something something recursive"
|
||||||
|
-- @
|
||||||
potentially :: Monad m => Maybe (m ()) -> m ()
|
potentially :: Monad m => Maybe (m ()) -> m ()
|
||||||
potentially = fromMaybe (return ())
|
potentially = fromMaybe (return ())
|
||||||
|
|
||||||
@@ -812,10 +869,9 @@ filterByAnnotation asSpec params =
|
|||||||
filter (not . shouldIgnore)
|
filter (not . shouldIgnore)
|
||||||
where
|
where
|
||||||
token = asScript asSpec
|
token = asScript asSpec
|
||||||
idFor (TokenComment id _) = id
|
|
||||||
shouldIgnore note =
|
shouldIgnore note =
|
||||||
any (shouldIgnoreFor (getCode note)) $
|
any (shouldIgnoreFor (getCode note)) $
|
||||||
getPath parents (T_Bang $ idFor note)
|
getPath parents (T_Bang $ tcId note)
|
||||||
shouldIgnoreFor num (T_Annotation _ anns _) =
|
shouldIgnoreFor num (T_Annotation _ anns _) =
|
||||||
any hasNum anns
|
any hasNum anns
|
||||||
where
|
where
|
||||||
@@ -824,7 +880,7 @@ filterByAnnotation asSpec params =
|
|||||||
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
|
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
|
||||||
shouldIgnoreFor _ _ = False
|
shouldIgnoreFor _ _ = False
|
||||||
parents = parentMap params
|
parents = parentMap params
|
||||||
getCode (TokenComment _ (Comment _ c _)) = c
|
getCode = cCode . tcComment
|
||||||
|
|
||||||
-- Is this a ${#anything}, to get string length or array count?
|
-- Is this a ${#anything}, to get string length or array count?
|
||||||
isCountingReference (T_DollarBraced id token) =
|
isCountingReference (T_DollarBraced id token) =
|
||||||
@@ -874,6 +930,3 @@ getOpts flagTokenizer string cmd = process flags
|
|||||||
else do
|
else do
|
||||||
more <- process rest2
|
more <- process rest2
|
||||||
return $ (flag1, token1) : more
|
return $ (flag1, token1) : more
|
||||||
|
|
||||||
return []
|
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
|
||||||
|
@@ -17,8 +17,7 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
module ShellCheck.Checker (checkScript) where
|
||||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
|
||||||
|
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Parser
|
import ShellCheck.Parser
|
||||||
@@ -35,58 +34,75 @@ import qualified System.IO
|
|||||||
import Prelude hiding (readFile)
|
import Prelude hiding (readFile)
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
|
||||||
import Test.QuickCheck.All
|
tokenToPosition startMap t = fromMaybe fail $ do
|
||||||
|
span <- Map.lookup (tcId t) startMap
|
||||||
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
return $ newPositionedComment {
|
||||||
position <- Map.lookup id map
|
pcStartPos = fst span,
|
||||||
return $ PositionedComment position position c
|
pcEndPos = snd span,
|
||||||
|
pcComment = tcComment t,
|
||||||
|
pcFix = tcFix t
|
||||||
|
}
|
||||||
where
|
where
|
||||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||||
|
|
||||||
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||||
checkScript sys spec = do
|
checkScript sys spec = do
|
||||||
results <- checkScript (csScript spec)
|
results <- checkScript (csScript spec)
|
||||||
return CheckResult {
|
return emptyCheckResult {
|
||||||
crFilename = csFilename spec,
|
crFilename = csFilename spec,
|
||||||
crComments = results
|
crComments = results
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
checkScript contents = do
|
checkScript contents = do
|
||||||
result <- parseScript sys ParseSpec {
|
result <- parseScript sys newParseSpec {
|
||||||
psFilename = csFilename spec,
|
psFilename = csFilename spec,
|
||||||
psScript = contents,
|
psScript = contents,
|
||||||
psCheckSourced = csCheckSourced spec
|
psCheckSourced = csCheckSourced spec,
|
||||||
|
psShellTypeOverride = csShellTypeOverride spec
|
||||||
}
|
}
|
||||||
let parseMessages = prComments result
|
let parseMessages = prComments result
|
||||||
|
let tokenPositions = prTokenPositions result
|
||||||
|
let analysisSpec root =
|
||||||
|
as {
|
||||||
|
asScript = root,
|
||||||
|
asShellType = csShellTypeOverride spec,
|
||||||
|
asCheckSourced = csCheckSourced spec,
|
||||||
|
asExecutionMode = Executed,
|
||||||
|
asTokenPositions = tokenPositions
|
||||||
|
} where as = newAnalysisSpec root
|
||||||
let analysisMessages =
|
let analysisMessages =
|
||||||
fromMaybe [] $
|
fromMaybe [] $
|
||||||
(arComments . analyzeScript . analysisSpec)
|
(arComments . analyzeScript . analysisSpec)
|
||||||
<$> prRoot result
|
<$> prRoot result
|
||||||
let translator = tokenToPosition (prTokenPositions result)
|
let translator = tokenToPosition tokenPositions
|
||||||
return . nub . sortMessages . filter shouldInclude $
|
return . nub . sortMessages . filter shouldInclude $
|
||||||
(parseMessages ++ map translator analysisMessages)
|
(parseMessages ++ map translator analysisMessages)
|
||||||
|
|
||||||
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
|
shouldInclude pc =
|
||||||
code `notElem` csExcludedWarnings spec
|
let code = cCode (pcComment pc)
|
||||||
|
severity = cSeverity (pcComment pc)
|
||||||
|
in
|
||||||
|
code `notElem` csExcludedWarnings spec &&
|
||||||
|
severity <= csMinSeverity spec
|
||||||
|
|
||||||
sortMessages = sortBy (comparing order)
|
sortMessages = sortBy (comparing order)
|
||||||
order (PositionedComment pos _ (Comment severity code message)) =
|
order pc =
|
||||||
(posFile pos, posLine pos, posColumn pos, severity, code, message)
|
let pos = pcStartPos pc
|
||||||
getPosition (PositionedComment pos _ _) = pos
|
comment = pcComment pc in
|
||||||
|
(posFile pos,
|
||||||
|
posLine pos,
|
||||||
|
posColumn pos,
|
||||||
|
cSeverity comment,
|
||||||
|
cCode comment,
|
||||||
|
cMessage comment)
|
||||||
|
getPosition = pcStartPos
|
||||||
|
|
||||||
analysisSpec root =
|
|
||||||
AnalysisSpec {
|
|
||||||
asScript = root,
|
|
||||||
asShellType = csShellTypeOverride spec,
|
|
||||||
asCheckSourced = csCheckSourced spec,
|
|
||||||
asExecutionMode = Executed
|
|
||||||
}
|
|
||||||
|
|
||||||
getErrors sys spec =
|
getErrors sys spec =
|
||||||
sort . map getCode . crComments $
|
sort . map getCode . crComments $
|
||||||
runIdentity (checkScript sys spec)
|
runIdentity (checkScript sys spec)
|
||||||
where
|
where
|
||||||
getCode (PositionedComment _ _ (Comment _ code _)) = code
|
getCode = cCode . pcComment
|
||||||
|
|
||||||
check = checkWithIncludes []
|
check = checkWithIncludes []
|
||||||
|
|
||||||
@@ -106,97 +122,132 @@ checkRecursive includes src =
|
|||||||
csCheckSourced = True
|
csCheckSourced = True
|
||||||
}
|
}
|
||||||
|
|
||||||
prop_findsParseIssue = check "echo \"$12\"" == [1037]
|
-- | Dummy binding for doctest to run
|
||||||
|
--
|
||||||
prop_commentDisablesParseIssue1 =
|
-- >>> check "echo \"$12\""
|
||||||
null $ check "#shellcheck disable=SC1037\necho \"$12\""
|
-- [1037]
|
||||||
prop_commentDisablesParseIssue2 =
|
--
|
||||||
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
-- >>> check "#shellcheck disable=SC1037\necho \"$12\""
|
||||||
|
-- []
|
||||||
prop_findsAnalysisIssue =
|
--
|
||||||
check "echo $1" == [2086]
|
-- >>> check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
|
||||||
prop_commentDisablesAnalysisIssue1 =
|
-- []
|
||||||
null $ check "#shellcheck disable=SC2086\necho $1"
|
--
|
||||||
prop_commentDisablesAnalysisIssue2 =
|
-- >>> check "echo $1"
|
||||||
null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
|
-- [2086]
|
||||||
|
--
|
||||||
prop_optionDisablesIssue1 =
|
-- >>> check "#shellcheck disable=SC2086\necho $1"
|
||||||
null $ getErrors
|
-- []
|
||||||
(mockedSystemInterface [])
|
--
|
||||||
emptyCheckSpec {
|
-- >>> check "#shellcheck disable=SC2086\n#lol\necho $1"
|
||||||
csScript = "echo $1",
|
-- []
|
||||||
csExcludedWarnings = [2148, 2086]
|
--
|
||||||
}
|
-- >>> :{
|
||||||
|
-- getErrors
|
||||||
prop_optionDisablesIssue2 =
|
-- (mockedSystemInterface [])
|
||||||
null $ getErrors
|
-- emptyCheckSpec {
|
||||||
(mockedSystemInterface [])
|
-- csScript = "echo $1",
|
||||||
emptyCheckSpec {
|
-- csExcludedWarnings = [2148, 2086]
|
||||||
csScript = "echo \"$10\"",
|
-- }
|
||||||
csExcludedWarnings = [2148, 1037]
|
-- :}
|
||||||
}
|
-- []
|
||||||
|
--
|
||||||
prop_canParseDevNull =
|
-- >>> :{
|
||||||
[] == check "source /dev/null"
|
-- getErrors
|
||||||
|
-- (mockedSystemInterface [])
|
||||||
prop_failsWhenNotSourcing =
|
-- emptyCheckSpec {
|
||||||
[1091, 2154] == check "source lol; echo \"$bar\""
|
-- csScript = "echo \"$10\"",
|
||||||
|
-- csExcludedWarnings = [2148, 1037]
|
||||||
prop_worksWhenSourcing =
|
-- }
|
||||||
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
|
-- :}
|
||||||
|
-- []
|
||||||
prop_worksWhenDotting =
|
--
|
||||||
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
|
-- >>> check "#!/usr/bin/python\ntrue $1\n"
|
||||||
|
-- [1071]
|
||||||
prop_noInfiniteSourcing =
|
--
|
||||||
[] == checkWithIncludes [("lib", "source lib")] "source lib"
|
-- >>> :{
|
||||||
|
-- getErrors
|
||||||
prop_canSourceBadSyntax =
|
-- (mockedSystemInterface [])
|
||||||
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
|
-- emptyCheckSpec {
|
||||||
|
-- csScript = "#!/usr/bin/python\ntrue\n",
|
||||||
prop_cantSourceDynamic =
|
-- csShellTypeOverride = Just Sh
|
||||||
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
|
-- }
|
||||||
|
-- :}
|
||||||
prop_cantSourceDynamic2 =
|
-- []
|
||||||
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
|
--
|
||||||
|
-- >>> check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
|
||||||
prop_canSourceDynamicWhenRedirected =
|
-- []
|
||||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
--
|
||||||
|
-- >>> check "source /dev/null"
|
||||||
prop_recursiveAnalysis =
|
-- []
|
||||||
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
|
--
|
||||||
|
-- >>> check "source lol; echo \"$bar\""
|
||||||
prop_recursiveParsing =
|
-- [1091,2154]
|
||||||
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
|
--
|
||||||
|
-- >>> checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
|
||||||
prop_sourceDirectiveDoesntFollowFile =
|
-- []
|
||||||
null $ checkWithIncludes
|
--
|
||||||
[("foo", "source bar"), ("bar", "baz=3")]
|
-- >>> checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
|
||||||
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
|
-- []
|
||||||
|
--
|
||||||
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
|
-- >>> checkWithIncludes [("lib", "source lib")] "source lib"
|
||||||
prop_filewideAnnotation1 = null $
|
-- []
|
||||||
check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
|
--
|
||||||
prop_filewideAnnotation2 = null $
|
-- >>> checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
|
||||||
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
-- [1094,2086]
|
||||||
prop_filewideAnnotation3 = null $
|
--
|
||||||
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"
|
-- >>> checkWithIncludes [("lib", "")] ". \"$1\""
|
||||||
prop_filewideAnnotation4 = null $
|
-- [1090]
|
||||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
--
|
||||||
prop_filewideAnnotation5 = null $
|
-- >>> checkWithIncludes [("lib", "")] "source ~/foo"
|
||||||
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
|
-- [1090]
|
||||||
prop_filewideAnnotation6 = null $
|
--
|
||||||
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
|
-- >>> checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||||
prop_filewideAnnotation7 = null $
|
-- []
|
||||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
--
|
||||||
|
-- >>> checkRecursive [("lib", "echo $1")] "source lib"
|
||||||
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
|
-- [2086]
|
||||||
prop_filewideAnnotation8 = null $
|
--
|
||||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
-- >>> checkRecursive [("lib", "echo \"$10\"")] "source lib"
|
||||||
|
-- [1037]
|
||||||
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
--
|
||||||
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
-- >>> checkWithIncludes [("foo", "source bar"), ("bar", "baz=3")] "#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
|
||||||
|
-- []
|
||||||
|
--
|
||||||
return []
|
-- >>> check "#!/bin/sh\necho $1"
|
||||||
runTests = $quickCheckAll
|
-- [2086]
|
||||||
|
--
|
||||||
|
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
|
||||||
|
-- []
|
||||||
|
--
|
||||||
|
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
||||||
|
-- []
|
||||||
|
--
|
||||||
|
-- >>> check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
|
||||||
|
-- []
|
||||||
|
--
|
||||||
|
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||||
|
-- []
|
||||||
|
--
|
||||||
|
-- >>> check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
|
||||||
|
-- []
|
||||||
|
--
|
||||||
|
-- >>> check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
|
||||||
|
-- []
|
||||||
|
--
|
||||||
|
-- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||||
|
-- []
|
||||||
|
--
|
||||||
|
-- check "true\n[ $? == 0 ] && echo $1"
|
||||||
|
-- [2086, 2181]
|
||||||
|
--
|
||||||
|
-- check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||||
|
-- []
|
||||||
|
--
|
||||||
|
-- >>> 2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
||||||
|
-- True
|
||||||
|
--
|
||||||
|
-- >>> check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
|
||||||
|
-- []
|
||||||
|
doctests :: ()
|
||||||
|
doctests = ()
|
||||||
|
@@ -17,11 +17,9 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
-- This module contains checks that examine specific commands by name.
|
-- This module contains checks that examine specific commands by name.
|
||||||
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
|
module ShellCheck.Checks.Commands (checker) where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
@@ -37,8 +35,6 @@ import Data.Char
|
|||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import qualified Data.Map.Strict as Map
|
import qualified Data.Map.Strict as Map
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
|
||||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
|
||||||
|
|
||||||
data CommandName = Exactly String | Basename String
|
data CommandName = Exactly String | Basename String
|
||||||
deriving (Eq, Ord)
|
deriving (Eq, Ord)
|
||||||
@@ -46,7 +42,6 @@ data CommandName = Exactly String | Basename String
|
|||||||
data CommandCheck =
|
data CommandCheck =
|
||||||
CommandCheck CommandName (Token -> Analysis)
|
CommandCheck CommandName (Token -> Analysis)
|
||||||
|
|
||||||
|
|
||||||
verify :: CommandCheck -> String -> Bool
|
verify :: CommandCheck -> String -> Bool
|
||||||
verify f s = producesComments (getChecker [f]) s == Just True
|
verify f s = producesComments (getChecker [f]) s == Just True
|
||||||
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
||||||
@@ -61,6 +56,7 @@ commandChecks = [
|
|||||||
,checkGrepRe
|
,checkGrepRe
|
||||||
,checkTrapQuotes
|
,checkTrapQuotes
|
||||||
,checkReturn
|
,checkReturn
|
||||||
|
,checkExit
|
||||||
,checkFindExecWithSingleArgument
|
,checkFindExecWithSingleArgument
|
||||||
,checkUnusedEchoEscapes
|
,checkUnusedEchoEscapes
|
||||||
,checkInjectableFindSh
|
,checkInjectableFindSh
|
||||||
@@ -92,6 +88,7 @@ commandChecks = [
|
|||||||
,checkWhich
|
,checkWhich
|
||||||
,checkSudoRedirect
|
,checkSudoRedirect
|
||||||
,checkSudoArgs
|
,checkSudoArgs
|
||||||
|
,checkSourceArgs
|
||||||
]
|
]
|
||||||
|
|
||||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||||
@@ -128,19 +125,21 @@ getChecker list = Checker {
|
|||||||
checker :: Parameters -> Checker
|
checker :: Parameters -> Checker
|
||||||
checker params = getChecker commandChecks
|
checker params = getChecker commandChecks
|
||||||
|
|
||||||
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
-- |
|
||||||
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
-- >>> prop $ verify checkTr "tr [a-f] [A-F]"
|
||||||
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
|
-- >>> prop $ verify checkTr "tr 'a-z' 'A-Z'"
|
||||||
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
|
-- >>> prop $ verify checkTr "tr '[a-z]' '[A-Z]'"
|
||||||
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
|
-- >>> prop $ verifyNot checkTr "tr -d '[:lower:]'"
|
||||||
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
|
-- >>> prop $ verifyNot checkTr "tr -d '[:upper:]'"
|
||||||
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
|
-- >>> prop $ verifyNot checkTr "tr -d '|/_[:upper:]'"
|
||||||
prop_checkTr5 = verify checkTr "tr foo bar"
|
-- >>> prop $ verifyNot checkTr "ls [a-z]"
|
||||||
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
|
-- >>> prop $ verify checkTr "tr foo bar"
|
||||||
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
-- >>> prop $ verify checkTr "tr 'hello' 'world'"
|
||||||
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
-- >>> prop $ verifyNot checkTr "tr aeiou _____"
|
||||||
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
-- >>> prop $ verifyNot checkTr "a-z n-za-m"
|
||||||
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
-- >>> prop $ verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||||
|
-- >>> prop $ verifyNot checkTr "tr abc '[d*]'"
|
||||||
|
-- >>> prop $ verifyNot checkTr "tr '[=e=]' 'e'"
|
||||||
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||||
where
|
where
|
||||||
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
||||||
@@ -152,7 +151,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
|||||||
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
|
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
|
||||||
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
|
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
|
||||||
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
|
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
|
||||||
unless ("[:" `isPrefixOf` s) $
|
unless ("[:" `isPrefixOf` s || "[=" `isPrefixOf` s) $
|
||||||
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` 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."
|
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
|
||||||
Nothing -> return ()
|
Nothing -> return ()
|
||||||
@@ -161,9 +160,10 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
|||||||
let relevant = filter isAlpha s
|
let relevant = filter isAlpha s
|
||||||
in relevant /= nub relevant
|
in relevant /= nub relevant
|
||||||
|
|
||||||
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
|
-- |
|
||||||
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
|
-- >>> prop $ verify checkFindNameGlob "find / -name *.php"
|
||||||
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
|
-- >>> prop $ verify checkFindNameGlob "find / -type f -ipath *(foo)"
|
||||||
|
-- >>> prop $ verifyNot checkFindNameGlob "find * -name '*.php'"
|
||||||
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
||||||
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
||||||
acceptsGlob _ = False
|
acceptsGlob _ = False
|
||||||
@@ -176,35 +176,37 @@ checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
|||||||
f (b:r)
|
f (b:r)
|
||||||
|
|
||||||
|
|
||||||
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
-- |
|
||||||
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
|
-- >>> prop $ verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
||||||
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
|
-- >>> prop $ verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
|
||||||
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
|
-- >>> prop $ verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
|
||||||
|
-- >>> prop $ verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
|
||||||
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
||||||
f t =
|
f t =
|
||||||
when (all (`notElem` exceptions) (words $ arguments t)) $
|
when (all (`notElem` exceptions) (words $ arguments t)) $
|
||||||
style (getId t) 2003
|
style (getId $ getCommandTokenOrThis t) 2003
|
||||||
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
||||||
-- These operators are hard to replicate in POSIX
|
-- These operators are hard to replicate in POSIX
|
||||||
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
||||||
words = mapMaybe getLiteralString
|
words = mapMaybe getLiteralString
|
||||||
|
|
||||||
|
|
||||||
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
|
-- |
|
||||||
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
|
-- >>> prop $ verify checkGrepRe "cat foo | grep *.mp3"
|
||||||
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
|
-- >>> prop $ verify checkGrepRe "grep -Ev cow*test *.mp3"
|
||||||
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
|
-- >>> prop $ verify checkGrepRe "grep --regex=*.mp3 file"
|
||||||
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *"
|
-- >>> prop $ verifyNot checkGrepRe "grep foo *.mp3"
|
||||||
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
|
-- >>> prop $ verifyNot checkGrepRe "grep-v --regex=moo *"
|
||||||
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
|
-- >>> prop $ verifyNot checkGrepRe "grep foo \\*.mp3"
|
||||||
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
|
-- >>> prop $ verify checkGrepRe "grep *foo* file"
|
||||||
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
|
-- >>> prop $ verify checkGrepRe "ls | grep foo*.jpg"
|
||||||
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
|
-- >>> prop $ verifyNot checkGrepRe "grep '[0-9]*' file"
|
||||||
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
|
-- >>> prop $ verifyNot checkGrepRe "grep '^aa*' file"
|
||||||
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
|
-- >>> prop $ verifyNot checkGrepRe "grep --include=*.png foo"
|
||||||
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"
|
-- >>> prop $ verifyNot checkGrepRe "grep -F 'Foo*' file"
|
||||||
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"
|
-- >>> prop $ verifyNot checkGrepRe "grep -- -foo bar*"
|
||||||
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"
|
-- >>> prop $ verifyNot checkGrepRe "grep -e -foo bar*"
|
||||||
|
-- >>> prop $ verifyNot checkGrepRe "grep --regex -foo bar*"
|
||||||
|
|
||||||
checkGrepRe = CommandCheck (Basename "grep") check where
|
checkGrepRe = CommandCheck (Basename "grep") check where
|
||||||
check cmd = f cmd (arguments cmd)
|
check cmd = f cmd (arguments cmd)
|
||||||
@@ -255,10 +257,11 @@ checkGrepRe = CommandCheck (Basename "grep") check where
|
|||||||
contra = 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 $ verify checkTrapQuotes "trap \"echo $num\" INT"
|
||||||
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
|
-- >>> prop $ verify checkTrapQuotes "trap \"echo `ls`\" INT"
|
||||||
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
|
-- >>> prop $ verifyNot checkTrapQuotes "trap 'echo $num' INT"
|
||||||
|
-- >>> prop $ verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
|
||||||
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
||||||
f (x:_) = checkTrap x
|
f (x:_) = checkTrap x
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
@@ -272,22 +275,37 @@ checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
|||||||
checkExpansions _ = return ()
|
checkExpansions _ = return ()
|
||||||
|
|
||||||
|
|
||||||
prop_checkReturn1 = verifyNot checkReturn "return"
|
-- |
|
||||||
prop_checkReturn2 = verifyNot checkReturn "return 1"
|
-- >>> prop $ verifyNot checkReturn "return"
|
||||||
prop_checkReturn3 = verifyNot checkReturn "return $var"
|
-- >>> prop $ verifyNot checkReturn "return 1"
|
||||||
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
|
-- >>> prop $ verifyNot checkReturn "return $var"
|
||||||
prop_checkReturn5 = verify checkReturn "return -1"
|
-- >>> prop $ verifyNot checkReturn "return $((a|b))"
|
||||||
prop_checkReturn6 = verify checkReturn "return 1000"
|
-- >>> prop $ verify checkReturn "return -1"
|
||||||
prop_checkReturn7 = verify checkReturn "return 'hello world'"
|
-- >>> prop $ verify checkReturn "return 1000"
|
||||||
checkReturn = CommandCheck (Exactly "return") (f . arguments)
|
-- >>> prop $ 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 $ verifyNot checkExit "exit"
|
||||||
|
-- >>> prop $ verifyNot checkExit "exit 1"
|
||||||
|
-- >>> prop $ verifyNot checkExit "exit $var"
|
||||||
|
-- >>> prop $ verifyNot checkExit "exit $((a|b))"
|
||||||
|
-- >>> prop $ verify checkExit "exit -1"
|
||||||
|
-- >>> prop $ verify checkExit "exit 1000"
|
||||||
|
-- >>> prop $ 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
|
where
|
||||||
f (first:second:_) =
|
f (first:second:_) =
|
||||||
err (getId second) 2151
|
multi (getId first)
|
||||||
"Only one integer 0-255 can be returned. Use stdout for other data."
|
|
||||||
f [value] =
|
f [value] =
|
||||||
when (isInvalid $ literal value) $
|
when (isInvalid $ literal value) $
|
||||||
err (getId value) 2152
|
invalid (getId value)
|
||||||
"Can only return 0-255. Other data should be written to stdout."
|
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
||||||
@@ -301,9 +319,10 @@ checkReturn = CommandCheck (Exactly "return") (f . arguments)
|
|||||||
lit _ = return "WTF"
|
lit _ = return "WTF"
|
||||||
|
|
||||||
|
|
||||||
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
|
-- |
|
||||||
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
|
-- >>> prop $ verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
|
||||||
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
|
-- >>> prop $ verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
|
||||||
|
-- >>> prop $ verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
|
||||||
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
|
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
|
||||||
where
|
where
|
||||||
f = void . sequence . mapMaybe check . tails
|
f = void . sequence . mapMaybe check . tails
|
||||||
@@ -319,36 +338,30 @@ checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
|
|||||||
commandRegex = mkRegex "[ |;]"
|
commandRegex = mkRegex "[ |;]"
|
||||||
|
|
||||||
|
|
||||||
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
|
-- |
|
||||||
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
|
-- >>> prop $ verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
|
||||||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
-- >>> prop $ verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
|
||||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
-- >>> prop $ verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
-- >>> prop $ verifyNot checkUnusedEchoEscapes "echo lol"
|
||||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
|
-- >>> prop $ verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||||
|
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
|
||||||
where
|
where
|
||||||
isDashE = mkRegex "^-.*e"
|
|
||||||
hasEscapes = mkRegex "\\\\[rnt]"
|
hasEscapes = mkRegex "\\\\[rnt]"
|
||||||
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
|
f cmd =
|
||||||
return ()
|
whenShell [Sh, Bash, Ksh] $
|
||||||
where allButLast = reverse . drop 1 . reverse $ args
|
unless (cmd `hasFlag` "e") $
|
||||||
f args = mapM_ checkEscapes args
|
mapM_ examine $ arguments cmd
|
||||||
|
|
||||||
checkEscapes (T_NormalWord _ args) =
|
examine token = do
|
||||||
mapM_ checkEscapes args
|
let str = onlyLiteralString token
|
||||||
checkEscapes (T_DoubleQuoted id args) =
|
|
||||||
mapM_ checkEscapes args
|
|
||||||
checkEscapes (T_Literal id str) = examine id str
|
|
||||||
checkEscapes (T_SingleQuoted id str) = examine id str
|
|
||||||
checkEscapes _ = return ()
|
|
||||||
|
|
||||||
examine id str =
|
|
||||||
when (str `matches` hasEscapes) $
|
when (str `matches` hasEscapes) $
|
||||||
info id 2028 "echo won't expand escape sequences. Consider printf."
|
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 $ verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
|
||||||
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
|
-- >>> prop $ verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
|
||||||
|
-- >>> prop $ verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
|
||||||
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
|
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
|
||||||
where
|
where
|
||||||
check args = do
|
check args = do
|
||||||
@@ -371,9 +384,10 @@ checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
|
|||||||
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
|
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 $ verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
|
||||||
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
|
-- >>> prop $ verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
|
||||||
|
-- >>> prop $ verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
|
||||||
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
|
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
|
||||||
where
|
where
|
||||||
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
|
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
|
||||||
@@ -390,28 +404,29 @@ checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
|
|||||||
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
|
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 $ verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
|
||||||
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir -pm 0755 $dir"
|
||||||
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
|
||||||
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
|
||||||
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
|
||||||
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
|
||||||
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
|
||||||
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
|
||||||
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p a/b"
|
||||||
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
|
||||||
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir a/b"
|
||||||
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir --parents a/b"
|
||||||
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
|
||||||
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
|
||||||
prop_checkMkdirDashPM15 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
|
||||||
prop_checkMkdirDashPM16 = verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"
|
||||||
prop_checkMkdirDashPM17 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"
|
||||||
prop_checkMkdirDashPM18 = verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"
|
||||||
prop_checkMkdirDashPM19 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin"
|
-- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"
|
||||||
prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin"
|
||||||
prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
|
||||||
|
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
|
||||||
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
||||||
where
|
where
|
||||||
check t = potentially $ do
|
check t = potentially $ do
|
||||||
@@ -427,13 +442,14 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
|||||||
re = mkRegex "^(\\.\\.?\\/)+[^/]+$"
|
re = mkRegex "^(\\.\\.?\\/)+[^/]+$"
|
||||||
|
|
||||||
|
|
||||||
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
|
-- |
|
||||||
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
|
-- >>> prop $ verify checkNonportableSignals "trap f 8"
|
||||||
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
|
-- >>> prop $ verifyNot checkNonportableSignals "trap f 0"
|
||||||
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
|
-- >>> prop $ verifyNot checkNonportableSignals "trap f 14"
|
||||||
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
|
-- >>> prop $ verify checkNonportableSignals "trap f SIGKILL"
|
||||||
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
|
-- >>> prop $ verify checkNonportableSignals "trap f 9"
|
||||||
prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' int"
|
-- >>> prop $ verify checkNonportableSignals "trap f stop"
|
||||||
|
-- >>> prop $ verifyNot checkNonportableSignals "trap 'stop' int"
|
||||||
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
||||||
where
|
where
|
||||||
f args = case args of
|
f args = case args of
|
||||||
@@ -462,10 +478,11 @@ checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
|||||||
"SIGKILL/SIGSTOP can not be trapped."
|
"SIGKILL/SIGSTOP can not be trapped."
|
||||||
|
|
||||||
|
|
||||||
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
|
-- |
|
||||||
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
|
-- >>> prop $ verify checkInteractiveSu "su; rm file; su $USER"
|
||||||
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
|
-- >>> prop $ verify checkInteractiveSu "su foo; something; exit"
|
||||||
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
|
-- >>> prop $ verifyNot checkInteractiveSu "echo rm | su foo"
|
||||||
|
-- >>> prop $ verifyNot checkInteractiveSu "su root < script"
|
||||||
checkInteractiveSu = CommandCheck (Basename "su") f
|
checkInteractiveSu = CommandCheck (Basename "su") f
|
||||||
where
|
where
|
||||||
f cmd = when (length (arguments cmd) <= 1) $ do
|
f cmd = when (length (arguments cmd) <= 1) $ do
|
||||||
@@ -480,11 +497,13 @@ checkInteractiveSu = CommandCheck (Basename "su") f
|
|||||||
undirected _ = True
|
undirected _ = True
|
||||||
|
|
||||||
|
|
||||||
|
-- |
|
||||||
-- This is hard to get right without properly parsing ssh args
|
-- 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 $ verify checkSshCommandString "ssh host \"echo $PS1\""
|
||||||
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
-- >>> prop $ verifyNot checkSshCommandString "ssh host \"ls foo\""
|
||||||
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\""
|
-- >>> prop $ verifyNot checkSshCommandString "ssh \"$host\""
|
||||||
|
-- >>> prop $ verifyNot checkSshCommandString "ssh -i key \"$host\""
|
||||||
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||||
where
|
where
|
||||||
isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
|
isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
|
||||||
@@ -500,21 +519,25 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
|||||||
checkArg _ = return ()
|
checkArg _ = return ()
|
||||||
|
|
||||||
|
|
||||||
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
|
-- |
|
||||||
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
|
-- >>> prop $ verify checkPrintfVar "printf \"Lol: $s\""
|
||||||
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
|
-- >>> prop $ verifyNot checkPrintfVar "printf 'Lol: $s'"
|
||||||
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
|
-- >>> prop $ verify checkPrintfVar "printf -v cow $(cmd)"
|
||||||
prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar"
|
-- >>> prop $ verifyNot checkPrintfVar "printf \"%${count}s\" var"
|
||||||
prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
|
-- >>> prop $ verify checkPrintfVar "printf '%s %s %s' foo bar"
|
||||||
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
|
-- >>> prop $ verify checkPrintfVar "printf foo bar baz"
|
||||||
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
|
-- >>> prop $ verify checkPrintfVar "printf -- foo bar baz"
|
||||||
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
-- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
|
||||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
-- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||||
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
-- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||||
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
-- >>> prop $ verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||||
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
-- >>> prop $ verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
||||||
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
|
-- >>> prop $ verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
||||||
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
-- >>> prop $ verify checkPrintfVar "printf '%*s\\n' 1"
|
||||||
|
-- >>> prop $ verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
||||||
|
-- >>> prop $ verifyNot checkPrintfVar "printf $'string'"
|
||||||
|
-- >>> prop $ verify checkPrintfVar "printf '%-*s\\n' 1"
|
||||||
|
-- >>> prop $ verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
||||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||||
@@ -525,11 +548,20 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||||||
case string of
|
case string of
|
||||||
'%':'%':rest -> countFormats rest
|
'%':'%':rest -> countFormats rest
|
||||||
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
|
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
|
||||||
'%':'*':rest -> 2 + countFormats rest -- width is specified as an argument
|
'%':rest -> regexBasedCountFormats rest + countFormats (dropWhile (/= '%') rest)
|
||||||
'%':rest -> 1 + countFormats rest
|
|
||||||
_:rest -> countFormats rest
|
_:rest -> countFormats rest
|
||||||
[] -> 0
|
[] -> 0
|
||||||
|
|
||||||
|
regexBasedCountFormats rest =
|
||||||
|
maybe 1 (foldl (\acc group -> acc + (if group == "*" then 1 else 0)) 1) (matchRegex re rest)
|
||||||
|
where
|
||||||
|
-- constructed based on specifications in "man printf"
|
||||||
|
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*).?(\\d*|\\*)[diouxXfFeEgGaAcsb]"
|
||||||
|
-- \____ _____/\___ ____/ \____ ____/\________ ________/
|
||||||
|
-- V V V V
|
||||||
|
-- flags field width precision format character
|
||||||
|
-- field width and precision can be specified with a '*' instead of a digit,
|
||||||
|
-- in which case printf will accept one more argument for each '*' used
|
||||||
check format more = do
|
check format more = do
|
||||||
fromMaybe (return ()) $ do
|
fromMaybe (return ()) $ do
|
||||||
string <- getLiteralString format
|
string <- getLiteralString format
|
||||||
@@ -554,24 +586,26 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
|
-- |
|
||||||
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
|
-- >>> prop $ verify checkUuoeCmd "echo $(date)"
|
||||||
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
|
-- >>> prop $ verify checkUuoeCmd "echo `date`"
|
||||||
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
|
-- >>> prop $ verify checkUuoeCmd "echo \"$(date)\""
|
||||||
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
|
-- >>> prop $ verify checkUuoeCmd "echo \"`date`\""
|
||||||
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
|
-- >>> prop $ verifyNot checkUuoeCmd "echo \"The time is $(date)\""
|
||||||
|
-- >>> prop $ verifyNot checkUuoeCmd "echo \"$(<file)\""
|
||||||
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
|
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
|
||||||
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
|
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
|
||||||
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
|
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
|
|
||||||
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
|
-- |
|
||||||
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
|
-- >>> prop $ verify checkSetAssignment "set foo 42"
|
||||||
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
|
-- >>> prop $ verify checkSetAssignment "set foo = 42"
|
||||||
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
|
-- >>> prop $ verify checkSetAssignment "set foo=42"
|
||||||
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
|
-- >>> prop $ verifyNot checkSetAssignment "set -- if=/dev/null"
|
||||||
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
-- >>> prop $ verifyNot checkSetAssignment "set 'a=5'"
|
||||||
|
-- >>> prop $ verifyNot checkSetAssignment "set"
|
||||||
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
||||||
where
|
where
|
||||||
f (var:value:rest) =
|
f (var:value:rest) =
|
||||||
@@ -591,10 +625,11 @@ checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
|||||||
literal _ = "*"
|
literal _ = "*"
|
||||||
|
|
||||||
|
|
||||||
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
|
-- |
|
||||||
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
-- >>> prop $ verify checkExportedExpansions "export $foo"
|
||||||
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
|
-- >>> prop $ verify checkExportedExpansions "export \"$foo\""
|
||||||
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}"
|
-- >>> prop $ verifyNot checkExportedExpansions "export foo"
|
||||||
|
-- >>> prop $ verifyNot checkExportedExpansions "export ${foo?}"
|
||||||
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
||||||
where
|
where
|
||||||
check t = potentially $ do
|
check t = potentially $ do
|
||||||
@@ -603,14 +638,15 @@ checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . argumen
|
|||||||
return . warn (getId t) 2163 $
|
return . warn (getId t) 2163 $
|
||||||
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||||
|
|
||||||
prop_checkReadExpansions1 = verify checkReadExpansions "read $var"
|
-- |
|
||||||
prop_checkReadExpansions2 = verify checkReadExpansions "read -r $var"
|
-- >>> prop $ verify checkReadExpansions "read $var"
|
||||||
prop_checkReadExpansions3 = verifyNot checkReadExpansions "read -p $var"
|
-- >>> prop $ verify checkReadExpansions "read -r $var"
|
||||||
prop_checkReadExpansions4 = verifyNot checkReadExpansions "read -rd $delim name"
|
-- >>> prop $ verifyNot checkReadExpansions "read -p $var"
|
||||||
prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
|
-- >>> prop $ verifyNot checkReadExpansions "read -rd $delim name"
|
||||||
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
|
-- >>> prop $ verify checkReadExpansions "read \"$var\""
|
||||||
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
|
-- >>> prop $ verify checkReadExpansions "read -a $var"
|
||||||
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
|
-- >>> prop $ verifyNot checkReadExpansions "read $1"
|
||||||
|
-- >>> prop $ verifyNot checkReadExpansions "read ${var?}"
|
||||||
checkReadExpansions = CommandCheck (Exactly "read") check
|
checkReadExpansions = CommandCheck (Exactly "read") check
|
||||||
where
|
where
|
||||||
options = getGnuOpts "sreu:n:N:i:p:a:"
|
options = getGnuOpts "sreu:n:N:i:p:a:"
|
||||||
@@ -637,9 +673,10 @@ getSingleUnmodifiedVariable word =
|
|||||||
in guard (contents == name) >> return t
|
in guard (contents == name) >> return t
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
-- |
|
||||||
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
-- >>> prop $ verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||||
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
|
-- >>> prop $ verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
||||||
|
-- >>> prop $ verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
|
||||||
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
|
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
|
||||||
where
|
where
|
||||||
re = mkRegex "\\$\\{?[0-9*@]"
|
re = mkRegex "\\$\\{?[0-9*@]"
|
||||||
@@ -651,9 +688,10 @@ checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
|
|||||||
"Aliases can't use positional parameters. Use a function."
|
"Aliases can't use positional parameters. Use a function."
|
||||||
|
|
||||||
|
|
||||||
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
|
-- |
|
||||||
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
|
-- >>> prop $ verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
|
||||||
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
|
-- >>> prop $ verifyNot checkAliasesExpandEarly "alias -p"
|
||||||
|
-- >>> prop $ verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
|
||||||
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
||||||
where
|
where
|
||||||
f = mapM_ checkArg
|
f = mapM_ checkArg
|
||||||
@@ -663,8 +701,8 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
|||||||
checkArg _ = return ()
|
checkArg _ = return ()
|
||||||
|
|
||||||
|
|
||||||
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
|
-- >>> prop $ verify checkUnsetGlobs "unset foo[1]"
|
||||||
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
|
-- >>> prop $ verifyNot checkUnsetGlobs "unset foo"
|
||||||
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
|
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
|
||||||
where
|
where
|
||||||
check arg =
|
check arg =
|
||||||
@@ -672,32 +710,38 @@ checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
|
|||||||
warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded."
|
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 $ verify checkFindWithoutPath "find -type f"
|
||||||
prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
|
-- >>> prop $ verify checkFindWithoutPath "find"
|
||||||
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
|
-- >>> prop $ verifyNot checkFindWithoutPath "find . -type f"
|
||||||
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
|
-- >>> prop $ verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
|
||||||
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
|
-- >>> prop $ verifyNot checkFindWithoutPath "find -O3 ."
|
||||||
|
-- >>> prop $ verifyNot checkFindWithoutPath "find -D exec ."
|
||||||
|
-- >>> prop $ verifyNot checkFindWithoutPath "find --help"
|
||||||
|
-- >>> prop $ verifyNot checkFindWithoutPath "find -Hx . -print"
|
||||||
checkFindWithoutPath = CommandCheck (Basename "find") f
|
checkFindWithoutPath = CommandCheck (Basename "find") f
|
||||||
where
|
where
|
||||||
f (T_SimpleCommand _ _ (cmd:args)) =
|
f t@(T_SimpleCommand _ _ (cmd:args)) =
|
||||||
unless (hasPath args) $
|
unless (t `hasFlag` "help" || hasPath args) $
|
||||||
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
|
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,
|
-- This is a bit of a kludge. find supports flag arguments both before and
|
||||||
-- as well as multiple non-flag arguments that are not the path. We assume that all the
|
-- after the path, as well as multiple non-flag arguments that are not the
|
||||||
-- pre-path flags are single characters, which is generally the case except for -O3.
|
-- path. We assume that all the pre-path flags are single characters from a
|
||||||
|
-- list of GNU and macOS flags.
|
||||||
hasPath (first:rest) =
|
hasPath (first:rest) =
|
||||||
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
|
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
|
||||||
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
|
not ("-" `isPrefixOf` flag) || isLeadingFlag flag && hasPath rest
|
||||||
hasPath [] = False
|
hasPath [] = False
|
||||||
isLeadingFlag flag = length flag <= 2 || "-O" `isPrefixOf` flag
|
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 $ verify checkTimeParameters "time -f lol sleep 10"
|
||||||
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
|
-- >>> prop $ verifyNot checkTimeParameters "time sleep 10"
|
||||||
prop_checkTimeParameters4 = verifyNot checkTimeParameters "command time -f lol sleep 10"
|
-- >>> prop $ verifyNot checkTimeParameters "time -p foo"
|
||||||
|
-- >>> prop $ verifyNot checkTimeParameters "command time -f lol sleep 10"
|
||||||
checkTimeParameters = CommandCheck (Exactly "time") f
|
checkTimeParameters = CommandCheck (Exactly "time") f
|
||||||
where
|
where
|
||||||
f (T_SimpleCommand _ _ (cmd:args:_)) =
|
f (T_SimpleCommand _ _ (cmd:args:_)) =
|
||||||
@@ -708,9 +752,10 @@ checkTimeParameters = CommandCheck (Exactly "time") f
|
|||||||
|
|
||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
prop_checkTimedCommand1 = verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
|
-- |
|
||||||
prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
|
-- >>> prop $ verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
|
||||||
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
|
-- >>> prop $ verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
|
||||||
|
-- >>> prop $ verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
|
||||||
checkTimedCommand = CommandCheck (Exactly "time") f where
|
checkTimedCommand = CommandCheck (Exactly "time") f where
|
||||||
f (T_SimpleCommand _ _ (c:args@(_:_))) =
|
f (T_SimpleCommand _ _ (c:args@(_:_))) =
|
||||||
whenShell [Sh, Dash] $ do
|
whenShell [Sh, Dash] $ do
|
||||||
@@ -734,32 +779,37 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
|
|||||||
T_SimpleCommand {} -> return True
|
T_SimpleCommand {} -> return True
|
||||||
_ -> return False
|
_ -> return False
|
||||||
|
|
||||||
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
|
-- |
|
||||||
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
|
-- >>> prop $ verify checkLocalScope "local foo=3"
|
||||||
|
-- >>> prop $ verifyNot checkLocalScope "f() { local foo=3; }"
|
||||||
checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
||||||
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
|
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
|
||||||
path <- getPathM t
|
path <- getPathM t
|
||||||
unless (any isFunction path) $
|
unless (any isFunction path) $
|
||||||
err (getId t) 2168 "'local' is only valid in functions."
|
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
|
||||||
|
|
||||||
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
-- |
|
||||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
-- >>> prop $ verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||||
|
-- >>> prop $ verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||||
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||||
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
|
\t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."
|
||||||
|
|
||||||
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
|
-- |
|
||||||
|
-- >>> prop $ verify checkDeprecatedEgrep "egrep '.+'"
|
||||||
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
|
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
|
||||||
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
|
\t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
|
||||||
|
|
||||||
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
|
-- |
|
||||||
|
-- >>> prop $ verify checkDeprecatedFgrep "fgrep '*' files"
|
||||||
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
||||||
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
\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 $ verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
|
||||||
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
|
-- >>> prop $ verify checkWhileGetoptsCase "while getopts 'a:' 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 $ verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
|
||||||
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
|
-- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
|
||||||
|
-- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
|
||||||
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||||
where
|
where
|
||||||
f :: Token -> Analysis
|
f :: Token -> Analysis
|
||||||
@@ -824,19 +874,20 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
|||||||
T_Redirecting _ _ x@(T_CaseExpression {}) -> return x
|
T_Redirecting _ _ x@(T_CaseExpression {}) -> return x
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"
|
-- |
|
||||||
prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"
|
-- >>> prop $ verify checkCatastrophicRm "rm -r $1/$2"
|
||||||
prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
|
-- >>> prop $ verify checkCatastrophicRm "rm -r /home/$foo"
|
||||||
prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
|
-- >>> prop $ verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
|
||||||
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
|
-- >>> prop $ verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
|
||||||
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
|
-- >>> prop $ verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
|
||||||
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
|
-- >>> prop $ verify checkCatastrophicRm "rm --recursive /etc/*$config*"
|
||||||
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
|
-- >>> prop $ verify checkCatastrophicRm "rm -rf /home"
|
||||||
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
|
-- >>> prop $ verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
|
||||||
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
|
-- >>> prop $ verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
|
||||||
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
|
-- >>> prop $ verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
|
||||||
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
|
-- >>> prop $ verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
|
||||||
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
|
-- >>> prop $ verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
|
||||||
|
-- >>> prop $ verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
|
||||||
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||||
when (isRecursive t) $
|
when (isRecursive t) $
|
||||||
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
|
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
|
||||||
@@ -885,8 +936,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
|||||||
["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
|
["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
|
||||||
|
|
||||||
|
|
||||||
prop_checkLetUsage1 = verify checkLetUsage "let a=1"
|
-- |
|
||||||
prop_checkLetUsage2 = verifyNot checkLetUsage "(( a=1 ))"
|
-- >>> prop $ verify checkLetUsage "let a=1"
|
||||||
|
-- >>> prop $ verifyNot checkLetUsage "(( a=1 ))"
|
||||||
checkLetUsage = CommandCheck (Exactly "let") f
|
checkLetUsage = CommandCheck (Exactly "let") f
|
||||||
where
|
where
|
||||||
f t = whenShell [Bash,Ksh] $ do
|
f t = whenShell [Bash,Ksh] $ do
|
||||||
@@ -906,15 +958,16 @@ missingDestination handler token = do
|
|||||||
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
|
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
|
||||||
map snd args
|
map snd args
|
||||||
|
|
||||||
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'"
|
-- |
|
||||||
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
|
-- >>> prop $ verify checkMvArguments "mv 'foo bar'"
|
||||||
prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}"
|
-- >>> prop $ verifyNot checkMvArguments "mv foo bar"
|
||||||
prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\""
|
-- >>> prop $ verifyNot checkMvArguments "mv 'foo bar'{,bak}"
|
||||||
prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar"
|
-- >>> prop $ verifyNot checkMvArguments "mv \"$@\""
|
||||||
prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar"
|
-- >>> prop $ verifyNot checkMvArguments "mv -t foo bar"
|
||||||
prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar"
|
-- >>> prop $ verifyNot checkMvArguments "mv --target-directory=foo bar"
|
||||||
prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version"
|
-- >>> prop $ verifyNot checkMvArguments "mv --target-direc=foo bar"
|
||||||
prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\""
|
-- >>> prop $ verifyNot checkMvArguments "mv --version"
|
||||||
|
-- >>> prop $ verifyNot checkMvArguments "mv \"${!var}\""
|
||||||
checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
|
checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
|
||||||
where
|
where
|
||||||
f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
|
f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
|
||||||
@@ -928,9 +981,10 @@ checkLnArguments = CommandCheck (Basename "ln") $ missingDestination f
|
|||||||
f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly."
|
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 $ verify checkFindRedirections "find . -exec echo {} > file \\;"
|
||||||
prop_checkFindRedirections3 = verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
|
-- >>> prop $ verifyNot checkFindRedirections "find . -exec echo {} \\; > file"
|
||||||
|
-- >>> prop $ verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
|
||||||
checkFindRedirections = CommandCheck (Basename "find") f
|
checkFindRedirections = CommandCheck (Basename "find") f
|
||||||
where
|
where
|
||||||
f t = do
|
f t = do
|
||||||
@@ -945,17 +999,18 @@ checkFindRedirections = CommandCheck (Basename "find") f
|
|||||||
"Redirection applies to the find command itself. Rewrite to work per action (or move to end)."
|
"Redirection applies to the find command itself. Rewrite to work per action (or move to end)."
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
|
|
||||||
prop_checkWhich = verify checkWhich "which '.+'"
|
-- >>> prop $ verify checkWhich "which '.+'"
|
||||||
checkWhich = CommandCheck (Basename "which") $
|
checkWhich = CommandCheck (Basename "which") $
|
||||||
\t -> info (getId t) 2230 "which is non-standard. Use builtin 'command -v' instead."
|
\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 $ verify checkSudoRedirect "sudo echo 3 > /proc/file"
|
||||||
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
|
-- >>> prop $ verify checkSudoRedirect "sudo cmd < input"
|
||||||
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
|
-- >>> prop $ verify checkSudoRedirect "sudo cmd >> file"
|
||||||
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
|
-- >>> prop $ verify checkSudoRedirect "sudo cmd &> file"
|
||||||
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
|
-- >>> prop $ verifyNot checkSudoRedirect "sudo cmd 2>&1"
|
||||||
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
|
-- >>> prop $ verifyNot checkSudoRedirect "sudo cmd 2> log"
|
||||||
|
-- >>> prop $ verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
|
||||||
checkSudoRedirect = CommandCheck (Basename "sudo") f
|
checkSudoRedirect = CommandCheck (Basename "sudo") f
|
||||||
where
|
where
|
||||||
f t = do
|
f t = do
|
||||||
@@ -979,13 +1034,14 @@ checkSudoRedirect = CommandCheck (Basename "sudo") f
|
|||||||
warnAbout _ = return ()
|
warnAbout _ = return ()
|
||||||
special file = concat (oversimplify file) == "/dev/null"
|
special file = concat (oversimplify file) == "/dev/null"
|
||||||
|
|
||||||
prop_checkSudoArgs1 = verify checkSudoArgs "sudo cd /root"
|
-- |
|
||||||
prop_checkSudoArgs2 = verify checkSudoArgs "sudo export x=3"
|
-- >>> prop $ verify checkSudoArgs "sudo cd /root"
|
||||||
prop_checkSudoArgs3 = verifyNot checkSudoArgs "sudo ls /usr/local/protected"
|
-- >>> prop $ verify checkSudoArgs "sudo export x=3"
|
||||||
prop_checkSudoArgs4 = verifyNot checkSudoArgs "sudo ls && export x=3"
|
-- >>> prop $ verifyNot checkSudoArgs "sudo ls /usr/local/protected"
|
||||||
prop_checkSudoArgs5 = verifyNot checkSudoArgs "sudo echo ls"
|
-- >>> prop $ verifyNot checkSudoArgs "sudo ls && export x=3"
|
||||||
prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls"
|
-- >>> prop $ verifyNot checkSudoArgs "sudo echo ls"
|
||||||
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
|
-- >>> prop $ verifyNot checkSudoArgs "sudo -n -u export ls"
|
||||||
|
-- >>> prop $ verifyNot checkSudoArgs "sudo docker export foo"
|
||||||
checkSudoArgs = CommandCheck (Basename "sudo") f
|
checkSudoArgs = CommandCheck (Basename "sudo") f
|
||||||
where
|
where
|
||||||
f t = potentially $ do
|
f t = potentially $ do
|
||||||
@@ -999,5 +1055,14 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
|
|||||||
-- This mess is why ShellCheck prefers not to know.
|
-- This mess is why ShellCheck prefers not to know.
|
||||||
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
|
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
|
||||||
|
|
||||||
return []
|
-- |
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
-- >>> prop $ verify checkSourceArgs "#!/bin/sh\n. script arg"
|
||||||
|
-- >>> prop $ verifyNot checkSourceArgs "#!/bin/sh\n. script"
|
||||||
|
-- >>> prop $ 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 ()
|
||||||
|
@@ -17,9 +17,8 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
module ShellCheck.Checks.ShellSupport (checker) where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
@@ -33,8 +32,6 @@ import Data.Char
|
|||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
|
||||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
|
||||||
|
|
||||||
data ForShell = ForShell [Shell] (Token -> Analysis)
|
data ForShell = ForShell [Shell] (Token -> Analysis)
|
||||||
|
|
||||||
@@ -67,9 +64,10 @@ testChecker (ForShell _ t) =
|
|||||||
verify c s = producesComments (testChecker c) s == Just True
|
verify c s = producesComments (testChecker c) s == Just True
|
||||||
verifyNot c s = producesComments (testChecker c) s == Just False
|
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 $ verify checkForDecimals "((3.14*c))"
|
||||||
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
-- >>> prop $ verify checkForDecimals "foo[1.2]=bar"
|
||||||
|
-- >>> prop $ verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
||||||
checkForDecimals = ForShell [Sh, Dash, Bash] f
|
checkForDecimals = ForShell [Sh, Dash, Bash] f
|
||||||
where
|
where
|
||||||
f t@(TA_Expansion id _) = potentially $ do
|
f t@(TA_Expansion id _) = potentially $ do
|
||||||
@@ -80,62 +78,63 @@ checkForDecimals = ForShell [Sh, Dash, Bash] f
|
|||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
|
|
||||||
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
|
-- |
|
||||||
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
|
-- >>> prop $ verify checkBashisms "while read a; do :; done < <(a)"
|
||||||
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
|
-- >>> prop $ verify checkBashisms "[ foo -nt bar ]"
|
||||||
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
|
-- >>> prop $ verify checkBashisms "echo $((i++))"
|
||||||
prop_checkBashisms5 = verify checkBashisms "source file"
|
-- >>> prop $ verify checkBashisms "rm !(*.hs)"
|
||||||
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
-- >>> prop $ verify checkBashisms "source file"
|
||||||
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
-- >>> prop $ verify checkBashisms "[ \"$a\" == 42 ]"
|
||||||
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
-- >>> prop $ verify checkBashisms "echo ${var[1]}"
|
||||||
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
-- >>> prop $ verify checkBashisms "echo ${!var[@]}"
|
||||||
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
|
-- >>> prop $ verify checkBashisms "echo ${!var*}"
|
||||||
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
|
-- >>> prop $ verify checkBashisms "echo ${var:4:12}"
|
||||||
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
|
-- >>> prop $ verifyNot checkBashisms "echo ${var:-4}"
|
||||||
prop_checkBashisms13= verify checkBashisms "exec -c env"
|
-- >>> prop $ verify checkBashisms "echo ${var//foo/bar}"
|
||||||
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
|
-- >>> prop $ verify checkBashisms "exec -c env"
|
||||||
prop_checkBashisms15= verify checkBashisms "let n++"
|
-- >>> prop $ verify checkBashisms "echo -n \"Foo: \""
|
||||||
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
|
-- >>> prop $ verify checkBashisms "let n++"
|
||||||
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
|
-- >>> prop $ verify checkBashisms "echo $RANDOM"
|
||||||
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
|
-- >>> prop $ verify checkBashisms "echo $((RANDOM%6+1))"
|
||||||
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
|
-- >>> prop $ verify checkBashisms "foo &> /dev/null"
|
||||||
prop_checkBashisms20= verify checkBashisms "read -ra foo"
|
-- >>> prop $ verify checkBashisms "foo > file*.txt"
|
||||||
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
|
-- >>> prop $ verify checkBashisms "read -ra foo"
|
||||||
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
|
-- >>> prop $ verify checkBashisms "[ -a foo ]"
|
||||||
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
|
-- >>> prop $ verifyNot checkBashisms "[ foo -a bar ]"
|
||||||
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
|
-- >>> prop $ verify checkBashisms "trap mything ERR INT"
|
||||||
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
|
-- >>> prop $ verifyNot checkBashisms "trap mything INT TERM"
|
||||||
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
|
-- >>> prop $ verify checkBashisms "cat < /dev/tcp/host/123"
|
||||||
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
|
-- >>> prop $ verify checkBashisms "trap mything ERR SIGTERM"
|
||||||
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
|
-- >>> prop $ verify checkBashisms "echo *[^0-9]*"
|
||||||
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
|
-- >>> prop $ verify checkBashisms "exec {n}>&2"
|
||||||
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
|
-- >>> prop $ verify checkBashisms "echo ${!var}"
|
||||||
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
|
-- >>> prop $ verify checkBashisms "printf -v '%s' \"$1\""
|
||||||
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
-- >>> prop $ verify checkBashisms "printf '%q' \"$1\""
|
||||||
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||||
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
-- >>> prop $ verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||||
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||||
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||||
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||||
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
|
-- >>> prop $ verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||||
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
|
-- >>> prop $ verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||||
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
|
-- >>> prop $ verify checkBashisms "foo-bar() { true; }"
|
||||||
prop_checkBashisms41= verify checkBashisms "echo `<file`"
|
-- >>> prop $ verify checkBashisms "echo $(<file)"
|
||||||
prop_checkBashisms42= verify checkBashisms "trap foo int"
|
-- >>> prop $ verify checkBashisms "echo `<file`"
|
||||||
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
|
-- >>> prop $ verify checkBashisms "trap foo int"
|
||||||
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
-- >>> prop $ verify checkBashisms "trap foo sigint"
|
||||||
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||||
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
-- >>> prop $ verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
|
-- >>> prop $ verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||||
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
||||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
-- >>> prop $ verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
-- >>> prop $ verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||||
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||||
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
-- >>> prop $ verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||||
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
-- >>> prop $ verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||||
|
-- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
||||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
params <- ask
|
params <- ask
|
||||||
kludge params t
|
kludge params t
|
||||||
@@ -302,11 +301,11 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
||||||
]
|
]
|
||||||
bashVars = [
|
bashVars = [
|
||||||
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||||
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
||||||
]
|
]
|
||||||
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||||
dashVars = [ "LINENO" ]
|
dashVars = [ ]
|
||||||
isBashVariable var =
|
isBashVariable var =
|
||||||
(var `elem` bashDynamicVars
|
(var `elem` bashDynamicVars
|
||||||
|| var `elem` bashVars && not (isAssigned var))
|
|| var `elem` bashVars && not (isAssigned var))
|
||||||
@@ -317,8 +316,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
Assignment (_, _, name, _) -> name == var
|
Assignment (_, _, name, _) -> name == var
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
-- |
|
||||||
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
-- >>> prop $ verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
||||||
|
-- >>> prop $ verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
||||||
checkEchoSed = ForShell [Bash, Ksh] f
|
checkEchoSed = ForShell [Bash, Ksh] f
|
||||||
where
|
where
|
||||||
f (T_Pipeline id _ [a, b]) =
|
f (T_Pipeline id _ [a, b]) =
|
||||||
@@ -344,10 +344,11 @@ checkEchoSed = ForShell [Bash, Ksh] f
|
|||||||
f _ = return ()
|
f _ = return ()
|
||||||
|
|
||||||
|
|
||||||
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
|
-- |
|
||||||
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
|
-- >>> prop $ verify checkBraceExpansionVars "echo {1..$n}"
|
||||||
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
|
-- >>> prop $ verifyNot checkBraceExpansionVars "echo {1,3,$n}"
|
||||||
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
|
-- >>> prop $ verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
|
||||||
|
-- >>> prop $ verify checkBraceExpansionVars "echo {$i..100}"
|
||||||
checkBraceExpansionVars = ForShell [Bash] f
|
checkBraceExpansionVars = ForShell [Bash] f
|
||||||
where
|
where
|
||||||
f t@(T_BraceExpansion id list) = mapM_ check list
|
f t@(T_BraceExpansion id list) = mapM_ check list
|
||||||
@@ -372,12 +373,13 @@ checkBraceExpansionVars = ForShell [Bash] f
|
|||||||
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
|
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
|
||||||
|
|
||||||
|
|
||||||
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
|
-- |
|
||||||
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
|
-- >>> prop $ verify checkMultiDimensionalArrays "foo[a][b]=3"
|
||||||
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
|
-- >>> prop $ verifyNot checkMultiDimensionalArrays "foo[a]=3"
|
||||||
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
|
-- >>> prop $ verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
|
||||||
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
|
-- >>> prop $ verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
|
||||||
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
|
-- >>> prop $ verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
|
||||||
|
-- >>> prop $ verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
|
||||||
checkMultiDimensionalArrays = ForShell [Bash] f
|
checkMultiDimensionalArrays = ForShell [Bash] f
|
||||||
where
|
where
|
||||||
f token =
|
f token =
|
||||||
@@ -392,16 +394,17 @@ checkMultiDimensionalArrays = ForShell [Bash] f
|
|||||||
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
||||||
isMultiDim t = getBracedModifier (bracedString t) `matches` re
|
isMultiDim t = getBracedModifier (bracedString t) `matches` re
|
||||||
|
|
||||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
-- |
|
||||||
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
-- >>> prop $ verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||||
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
-- >>> prop $ verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||||
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
|
-- >>> prop $ verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
||||||
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
|
-- >>> prop $ verify checkPS1Assignments "PS1=$'\\x1b[c '"
|
||||||
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
|
-- >>> prop $ verify checkPS1Assignments "PS1=$'\\e[3m; '"
|
||||||
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
|
-- >>> prop $ verify checkPS1Assignments "export PS1=$'\\e[3m; '"
|
||||||
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
|
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
|
||||||
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
|
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
|
||||||
prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
|
-- >>> prop $ verifyNot checkPS1Assignments "PS1='e033x1B'"
|
||||||
|
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
|
||||||
checkPS1Assignments = ForShell [Bash] f
|
checkPS1Assignments = ForShell [Bash] f
|
||||||
where
|
where
|
||||||
f token = case token of
|
f token = case token of
|
||||||
@@ -417,7 +420,3 @@ checkPS1Assignments = ForShell [Bash] f
|
|||||||
isJust $ matchRegex escapeRegex unenclosed
|
isJust $ matchRegex escapeRegex unenclosed
|
||||||
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
|
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
|
||||||
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
||||||
|
|
||||||
|
|
||||||
return []
|
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
|
||||||
|
@@ -39,7 +39,7 @@ internalVariables = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
variablesWithoutSpaces = [
|
variablesWithoutSpaces = [
|
||||||
"$", "-", "?", "!",
|
"$", "-", "?", "!", "#",
|
||||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||||
|
@@ -30,17 +30,17 @@ data Formatter = Formatter {
|
|||||||
footer :: IO ()
|
footer :: IO ()
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceFile (PositionedComment pos _ _) = posFile pos
|
sourceFile = posFile . pcStartPos
|
||||||
lineNo (PositionedComment pos _ _) = posLine pos
|
lineNo = posLine . pcStartPos
|
||||||
endLineNo (PositionedComment _ end _) = posLine end
|
endLineNo = posLine . pcEndPos
|
||||||
colNo (PositionedComment pos _ _) = posColumn pos
|
colNo = posColumn . pcStartPos
|
||||||
endColNo (PositionedComment _ end _) = posColumn end
|
endColNo = posColumn . pcEndPos
|
||||||
codeNo (PositionedComment _ _ (Comment _ code _)) = code
|
codeNo = cCode . pcComment
|
||||||
messageText (PositionedComment _ _ (Comment _ _ t)) = t
|
messageText = cMessage . pcComment
|
||||||
|
|
||||||
severityText :: PositionedComment -> String
|
severityText :: PositionedComment -> String
|
||||||
severityText (PositionedComment _ _ (Comment c _ _)) =
|
severityText pc =
|
||||||
case c of
|
case cSeverity (pcComment pc) of
|
||||||
ErrorC -> "error"
|
ErrorC -> "error"
|
||||||
WarningC -> "warning"
|
WarningC -> "warning"
|
||||||
InfoC -> "info"
|
InfoC -> "info"
|
||||||
@@ -51,11 +51,14 @@ makeNonVirtual comments contents =
|
|||||||
map fix comments
|
map fix comments
|
||||||
where
|
where
|
||||||
ls = lines contents
|
ls = lines contents
|
||||||
fix c@(PositionedComment start end comment) = PositionedComment start {
|
fix c = c {
|
||||||
|
pcStartPos = (pcStartPos c) {
|
||||||
posColumn = realignColumn lineNo colNo c
|
posColumn = realignColumn lineNo colNo c
|
||||||
} end {
|
}
|
||||||
|
, pcEndPos = (pcEndPos c) {
|
||||||
posColumn = realignColumn endLineNo endColNo c
|
posColumn = realignColumn endLineNo endColNo c
|
||||||
} comment
|
}
|
||||||
|
}
|
||||||
realignColumn lineNo colNo c =
|
realignColumn lineNo colNo c =
|
||||||
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||||
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
||||||
|
@@ -39,8 +39,24 @@ format = do
|
|||||||
footer = finish ref
|
footer = finish ref
|
||||||
}
|
}
|
||||||
|
|
||||||
instance ToJSON (PositionedComment) where
|
instance ToJSON Replacement where
|
||||||
toJSON comment@(PositionedComment start end (Comment level code string)) =
|
toJSON replacement =
|
||||||
|
let start = repStartPos replacement
|
||||||
|
end = repEndPos replacement
|
||||||
|
str = repString replacement in
|
||||||
|
object [
|
||||||
|
"line" .= posLine start,
|
||||||
|
"endLine" .= posLine end,
|
||||||
|
"column" .= posColumn start,
|
||||||
|
"endColumn" .= posColumn end,
|
||||||
|
"replaceWith" .= str
|
||||||
|
]
|
||||||
|
|
||||||
|
instance ToJSON PositionedComment where
|
||||||
|
toJSON comment =
|
||||||
|
let start = pcStartPos comment
|
||||||
|
end = pcEndPos comment
|
||||||
|
c = pcComment comment in
|
||||||
object [
|
object [
|
||||||
"file" .= posFile start,
|
"file" .= posFile start,
|
||||||
"line" .= posLine start,
|
"line" .= posLine start,
|
||||||
@@ -48,11 +64,15 @@ instance ToJSON (PositionedComment) where
|
|||||||
"column" .= posColumn start,
|
"column" .= posColumn start,
|
||||||
"endColumn" .= posColumn end,
|
"endColumn" .= posColumn end,
|
||||||
"level" .= severityText comment,
|
"level" .= severityText comment,
|
||||||
"code" .= code,
|
"code" .= cCode c,
|
||||||
"message" .= string
|
"message" .= cMessage c,
|
||||||
|
"fix" .= pcFix comment
|
||||||
]
|
]
|
||||||
|
|
||||||
toEncoding comment@(PositionedComment start end (Comment level code string)) =
|
toEncoding comment =
|
||||||
|
let start = pcStartPos comment
|
||||||
|
end = pcEndPos comment
|
||||||
|
c = pcComment comment in
|
||||||
pairs (
|
pairs (
|
||||||
"file" .= posFile start
|
"file" .= posFile start
|
||||||
<> "line" .= posLine start
|
<> "line" .= posLine start
|
||||||
@@ -60,10 +80,16 @@ instance ToJSON (PositionedComment) where
|
|||||||
<> "column" .= posColumn start
|
<> "column" .= posColumn start
|
||||||
<> "endColumn" .= posColumn end
|
<> "endColumn" .= posColumn end
|
||||||
<> "level" .= severityText comment
|
<> "level" .= severityText comment
|
||||||
<> "code" .= code
|
<> "code" .= cCode c
|
||||||
<> "message" .= string
|
<> "message" .= cMessage c
|
||||||
|
<> "fix" .= pcFix comment
|
||||||
)
|
)
|
||||||
|
|
||||||
|
instance ToJSON Fix where
|
||||||
|
toJSON fix = object [
|
||||||
|
"replacements" .= fixReplacements fix
|
||||||
|
]
|
||||||
|
|
||||||
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||||
collectResult ref result _ =
|
collectResult ref result _ =
|
||||||
modifyIORef ref (\x -> crComments result ++ x)
|
modifyIORef ref (\x -> crComments result ++ x)
|
||||||
@@ -71,4 +97,3 @@ collectResult ref result _ =
|
|||||||
finish ref = do
|
finish ref = do
|
||||||
list <- readIORef ref
|
list <- readIORef ref
|
||||||
BL.putStrLn $ encode list
|
BL.putStrLn $ encode list
|
||||||
|
|
||||||
|
@@ -22,17 +22,27 @@ module ShellCheck.Formatter.TTY (format) where
|
|||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.IORef
|
||||||
import Data.List
|
import Data.List
|
||||||
|
import Data.Maybe
|
||||||
import GHC.Exts
|
import GHC.Exts
|
||||||
import System.Info
|
|
||||||
import System.IO
|
import System.IO
|
||||||
|
import System.Info
|
||||||
|
|
||||||
|
wikiLink = "https://www.shellcheck.net/wiki/"
|
||||||
|
|
||||||
|
-- An arbitrary Ord thing to order warnings
|
||||||
|
type Ranking = (Char, Severity, Integer)
|
||||||
|
|
||||||
format :: FormatterOptions -> IO Formatter
|
format :: FormatterOptions -> IO Formatter
|
||||||
format options = return Formatter {
|
format options = do
|
||||||
|
topErrorRef <- newIORef []
|
||||||
|
return Formatter {
|
||||||
header = return (),
|
header = return (),
|
||||||
footer = return (),
|
footer = outputWiki topErrorRef,
|
||||||
onFailure = outputError options,
|
onFailure = outputError options,
|
||||||
onResult = outputResult options
|
onResult = outputResult options topErrorRef
|
||||||
}
|
}
|
||||||
|
|
||||||
colorForLevel level =
|
colorForLevel level =
|
||||||
@@ -45,13 +55,60 @@ colorForLevel level =
|
|||||||
"source" -> 0 -- none
|
"source" -> 0 -- none
|
||||||
_ -> 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 . 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
|
outputError options file error = do
|
||||||
color <- getColorFunc $ foColorOption options
|
color <- getColorFunc $ foColorOption options
|
||||||
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
|
||||||
|
|
||||||
outputResult options result sys = do
|
outputResult options ref result sys = do
|
||||||
color <- getColorFunc $ foColorOption options
|
color <- getColorFunc $ foColorOption options
|
||||||
let comments = crComments result
|
let comments = crComments result
|
||||||
|
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
|
||||||
let fileGroups = groupWith sourceFile comments
|
let fileGroups = groupWith sourceFile comments
|
||||||
mapM_ (outputForFile color sys) fileGroups
|
mapM_ (outputForFile color sys) fileGroups
|
||||||
|
|
||||||
@@ -62,8 +119,8 @@ outputForFile color sys comments = do
|
|||||||
let fileLines = lines contents
|
let fileLines = lines contents
|
||||||
let lineCount = fromIntegral $ length fileLines
|
let lineCount = fromIntegral $ length fileLines
|
||||||
let groups = groupWith lineNo comments
|
let groups = groupWith lineNo comments
|
||||||
mapM_ (\x -> do
|
mapM_ (\commentsForLine -> do
|
||||||
let lineNum = lineNo (head x)
|
let lineNum = lineNo (head commentsForLine)
|
||||||
let line = if lineNum < 1 || lineNum > lineCount
|
let line = if lineNum < 1 || lineNum > lineCount
|
||||||
then ""
|
then ""
|
||||||
else fileLines !! fromIntegral (lineNum - 1)
|
else fileLines !! fromIntegral (lineNum - 1)
|
||||||
@@ -71,16 +128,74 @@ outputForFile color sys comments = do
|
|||||||
putStrLn $ color "message" $
|
putStrLn $ color "message" $
|
||||||
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
"In " ++ fileName ++" line " ++ show lineNum ++ ":"
|
||||||
putStrLn (color "source" line)
|
putStrLn (color "source" line)
|
||||||
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x
|
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
|
showFixedString color comments lineNum line
|
||||||
) groups
|
) groups
|
||||||
|
|
||||||
|
hasApplicableFix lineNum comment = fromMaybe False $ do
|
||||||
|
replacements <- fixReplacements <$> pcFix comment
|
||||||
|
guard $ all (\c -> onSameLine (repStartPos c) && onSameLine (repEndPos c)) replacements
|
||||||
|
return True
|
||||||
|
where
|
||||||
|
onSameLine pos = posLine pos == lineNum
|
||||||
|
|
||||||
|
-- FIXME: Work correctly with multiple replacements
|
||||||
|
showFixedString color comments lineNum line =
|
||||||
|
case filter (hasApplicableFix lineNum) comments of
|
||||||
|
(first:_) -> do
|
||||||
|
-- in the spirit of error prone
|
||||||
|
putStrLn $ color "message" "Did you mean: "
|
||||||
|
putStrLn $ fixedString first line
|
||||||
|
putStrLn ""
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
-- need to do something smart about sorting by end index
|
||||||
|
fixedString :: PositionedComment -> String -> String
|
||||||
|
fixedString comment line =
|
||||||
|
case (pcFix comment) of
|
||||||
|
Nothing -> ""
|
||||||
|
Just rs ->
|
||||||
|
applyReplacement (fixReplacements rs) line 0
|
||||||
|
where
|
||||||
|
applyReplacement [] s _ = s
|
||||||
|
applyReplacement (rep:xs) s offset =
|
||||||
|
let replacementString = repString rep
|
||||||
|
start = (posColumn . repStartPos) rep
|
||||||
|
end = (posColumn . repEndPos) rep
|
||||||
|
z = doReplace start end s replacementString
|
||||||
|
len_r = (fromIntegral . length) replacementString in
|
||||||
|
applyReplacement xs z (offset + (end - start) + len_r)
|
||||||
|
|
||||||
|
-- FIXME: Work correctly with tabs
|
||||||
|
-- start and end comes from pos, which is 1 based
|
||||||
|
-- doReplace 0 0 "1234" "A" -> "A1234" -- technically not valid
|
||||||
|
-- doReplace 1 1 "1234" "A" -> "A1234"
|
||||||
|
-- doReplace 1 2 "1234" "A" -> "A234"
|
||||||
|
-- doReplace 3 3 "1234" "A" -> "12A34"
|
||||||
|
-- doReplace 4 4 "1234" "A" -> "123A4"
|
||||||
|
-- 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
|
||||||
|
(y, z) = splitAt (ei - si) xs
|
||||||
|
in
|
||||||
|
x ++ r ++ z
|
||||||
|
|
||||||
cuteIndent :: PositionedComment -> String
|
cuteIndent :: PositionedComment -> String
|
||||||
cuteIndent comment =
|
cuteIndent comment =
|
||||||
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||||
"^-- " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
makeArrow ++ " " ++ code (codeNo 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 code = "SC" ++ show code
|
code num = "SC" ++ show num
|
||||||
|
|
||||||
getColorFunc colorOption = do
|
getColorFunc colorOption = do
|
||||||
term <- hIsTerminalDevice stdout
|
term <- hIsTerminalDevice stdout
|
||||||
|
@@ -17,10 +17,51 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Interface where
|
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
|
||||||
|
module ShellCheck.Interface
|
||||||
|
(
|
||||||
|
SystemInterface(..)
|
||||||
|
, CheckSpec(csFilename, csScript, csCheckSourced, csExcludedWarnings, csShellTypeOverride, csMinSeverity)
|
||||||
|
, CheckResult(crFilename, crComments)
|
||||||
|
, ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
|
||||||
|
, ParseResult(prComments, prTokenPositions, prRoot)
|
||||||
|
, AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced, asTokenPositions)
|
||||||
|
, AnalysisResult(arComments)
|
||||||
|
, FormatterOptions(foColorOption, foWikiLinkCount)
|
||||||
|
, Shell(Ksh, Sh, Bash, Dash)
|
||||||
|
, 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
|
||||||
|
, newParseResult
|
||||||
|
, newAnalysisSpec
|
||||||
|
, newAnalysisResult
|
||||||
|
, newFormatterOptions
|
||||||
|
, newPosition
|
||||||
|
, newTokenComment
|
||||||
|
, mockedSystemInterface
|
||||||
|
, newParseSpec
|
||||||
|
, emptyCheckSpec
|
||||||
|
, newPositionedComment
|
||||||
|
, newComment
|
||||||
|
, Fix(fixReplacements)
|
||||||
|
, newFix
|
||||||
|
, Replacement(repStartPos, repEndPos, repString)
|
||||||
|
, newReplacement
|
||||||
|
) where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
|
|
||||||
|
import Control.DeepSeq
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
|
import Data.Monoid
|
||||||
|
import GHC.Generics (Generic)
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +76,8 @@ data CheckSpec = CheckSpec {
|
|||||||
csScript :: String,
|
csScript :: String,
|
||||||
csCheckSourced :: Bool,
|
csCheckSourced :: Bool,
|
||||||
csExcludedWarnings :: [Integer],
|
csExcludedWarnings :: [Integer],
|
||||||
csShellTypeOverride :: Maybe Shell
|
csShellTypeOverride :: Maybe Shell,
|
||||||
|
csMinSeverity :: Severity
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
data CheckResult = CheckResult {
|
data CheckResult = CheckResult {
|
||||||
@@ -43,44 +85,85 @@ data CheckResult = CheckResult {
|
|||||||
crComments :: [PositionedComment]
|
crComments :: [PositionedComment]
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
emptyCheckResult :: CheckResult
|
||||||
|
emptyCheckResult = CheckResult {
|
||||||
|
crFilename = "",
|
||||||
|
crComments = []
|
||||||
|
}
|
||||||
|
|
||||||
emptyCheckSpec :: CheckSpec
|
emptyCheckSpec :: CheckSpec
|
||||||
emptyCheckSpec = CheckSpec {
|
emptyCheckSpec = CheckSpec {
|
||||||
csFilename = "",
|
csFilename = "",
|
||||||
csScript = "",
|
csScript = "",
|
||||||
csCheckSourced = False,
|
csCheckSourced = False,
|
||||||
csExcludedWarnings = [],
|
csExcludedWarnings = [],
|
||||||
csShellTypeOverride = Nothing
|
csShellTypeOverride = Nothing,
|
||||||
|
csMinSeverity = StyleC
|
||||||
|
}
|
||||||
|
|
||||||
|
newParseSpec :: ParseSpec
|
||||||
|
newParseSpec = ParseSpec {
|
||||||
|
psFilename = "",
|
||||||
|
psScript = "",
|
||||||
|
psCheckSourced = False,
|
||||||
|
psShellTypeOverride = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Parser input and output
|
-- Parser input and output
|
||||||
data ParseSpec = ParseSpec {
|
data ParseSpec = ParseSpec {
|
||||||
psFilename :: String,
|
psFilename :: String,
|
||||||
psScript :: String,
|
psScript :: String,
|
||||||
psCheckSourced :: Bool
|
psCheckSourced :: Bool,
|
||||||
|
psShellTypeOverride :: Maybe Shell
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
data ParseResult = ParseResult {
|
data ParseResult = ParseResult {
|
||||||
prComments :: [PositionedComment],
|
prComments :: [PositionedComment],
|
||||||
prTokenPositions :: Map.Map Id Position,
|
prTokenPositions :: Map.Map Id (Position, Position),
|
||||||
prRoot :: Maybe Token
|
prRoot :: Maybe Token
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
newParseResult :: ParseResult
|
||||||
|
newParseResult = ParseResult {
|
||||||
|
prComments = [],
|
||||||
|
prTokenPositions = Map.empty,
|
||||||
|
prRoot = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
-- Analyzer input and output
|
-- Analyzer input and output
|
||||||
data AnalysisSpec = AnalysisSpec {
|
data AnalysisSpec = AnalysisSpec {
|
||||||
asScript :: Token,
|
asScript :: Token,
|
||||||
asShellType :: Maybe Shell,
|
asShellType :: Maybe Shell,
|
||||||
asExecutionMode :: ExecutionMode,
|
asExecutionMode :: ExecutionMode,
|
||||||
asCheckSourced :: Bool
|
asCheckSourced :: Bool,
|
||||||
|
asTokenPositions :: Map.Map Id (Position, Position)
|
||||||
|
}
|
||||||
|
|
||||||
|
newAnalysisSpec token = AnalysisSpec {
|
||||||
|
asScript = token,
|
||||||
|
asShellType = Nothing,
|
||||||
|
asExecutionMode = Executed,
|
||||||
|
asCheckSourced = False,
|
||||||
|
asTokenPositions = Map.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
newtype AnalysisResult = AnalysisResult {
|
newtype AnalysisResult = AnalysisResult {
|
||||||
arComments :: [TokenComment]
|
arComments :: [TokenComment]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newAnalysisResult = AnalysisResult {
|
||||||
|
arComments = []
|
||||||
|
}
|
||||||
|
|
||||||
-- Formatter options
|
-- Formatter options
|
||||||
newtype FormatterOptions = FormatterOptions {
|
data FormatterOptions = FormatterOptions {
|
||||||
foColorOption :: ColorOption
|
foColorOption :: ColorOption,
|
||||||
|
foWikiLinkCount :: Integer
|
||||||
|
}
|
||||||
|
|
||||||
|
newFormatterOptions = FormatterOptions {
|
||||||
|
foColorOption = ColorAuto,
|
||||||
|
foWikiLinkCount = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -91,16 +174,81 @@ data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
|||||||
type ErrorMessage = String
|
type ErrorMessage = String
|
||||||
type Code = Integer
|
type Code = Integer
|
||||||
|
|
||||||
data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
|
data Severity = ErrorC | WarningC | InfoC | StyleC
|
||||||
|
deriving (Show, Eq, Ord, Generic, NFData)
|
||||||
data Position = Position {
|
data Position = Position {
|
||||||
posFile :: String, -- Filename
|
posFile :: String, -- Filename
|
||||||
posLine :: Integer, -- 1 based source line
|
posLine :: Integer, -- 1 based source line
|
||||||
posColumn :: Integer -- 1 based source column, where tabs are 8
|
posColumn :: Integer -- 1 based source column, where tabs are 8
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq, Generic, NFData)
|
||||||
|
|
||||||
data Comment = Comment Severity Code String deriving (Show, Eq)
|
newPosition :: Position
|
||||||
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq)
|
newPosition = Position {
|
||||||
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
|
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
|
||||||
|
} deriving (Show, Eq, Generic, NFData)
|
||||||
|
|
||||||
|
newReplacement = Replacement {
|
||||||
|
repStartPos = newPosition,
|
||||||
|
repEndPos = newPosition,
|
||||||
|
repString = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =
|
data ColorOption =
|
||||||
ColorAuto
|
ColorAuto
|
||||||
|
File diff suppressed because it is too large
Load Diff
34
stack.yaml
34
stack.yaml
@@ -1,35 +1,3 @@
|
|||||||
# This file was automatically generated by stack init
|
resolver: lts-12.9
|
||||||
# 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-8.5
|
|
||||||
|
|
||||||
# Local packages, usually specified by relative directory name
|
|
||||||
packages:
|
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
|
|
||||||
|
77
striptests
77
striptests
@@ -1,77 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# This file strips all unit tests from ShellCheck, removing
|
# This file was deprecated by the doctest build.
|
||||||
# the dependency on QuickCheck and Template Haskell and
|
|
||||||
# reduces the binary size considerably.
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
sponge() {
|
|
||||||
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() {
|
|
||||||
echo "-- AUTOGENERATED from ShellCheck by striptests. Do not modify."
|
|
||||||
awk '
|
|
||||||
BEGIN {
|
|
||||||
state = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/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
|
|
||||||
|
|
||||||
|
35
test/buildtest
Executable file
35
test/buildtest
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/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"
|
||||||
|
cabal install --dependencies-only --enable-tests ||
|
||||||
|
die "can't install dependencies"
|
||||||
|
cabal configure --enable-tests ||
|
||||||
|
die "configure failed"
|
||||||
|
cabal build ||
|
||||||
|
die "build failed"
|
||||||
|
cabal test ||
|
||||||
|
die "test failed"
|
||||||
|
|
||||||
|
dist/build/shellcheck/shellcheck - << 'EOF' || die "execution failed"
|
||||||
|
#!/bin/sh
|
||||||
|
echo "Hello World"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
dist/build/shellcheck/shellcheck - << 'EOF' && die "negative execution failed"
|
||||||
|
#!/bin/sh
|
||||||
|
echo $1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
|
||||||
|
echo "Success"
|
||||||
|
exit 0
|
80
test/distrotest
Executable file
80
test/distrotest
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
[ "$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' will be deleted.
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Deleting 'dist'..."
|
||||||
|
rm -rf dist
|
||||||
|
|
||||||
|
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
|
||||||
|
opensuse:latest zypper install -y cabal-install ghc
|
||||||
|
|
||||||
|
# Older Ubuntu versions we want to support
|
||||||
|
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
|
||||||
|
ubuntu:17.10 apt-get update && apt-get install -y cabal-install
|
||||||
|
|
||||||
|
# Misc Haskell including current and latest Stack build
|
||||||
|
ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
|
||||||
|
haskell:latest true
|
||||||
|
|
||||||
|
# Known to currently fail
|
||||||
|
centos:latest yum install -y epel-release && yum install -y cabal-install
|
||||||
|
fedora:latest dnf install -y cabal-install
|
||||||
|
base/archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||||
|
EOF
|
||||||
|
|
||||||
|
exit "$final"
|
12
test/doctests.hs
Normal file
12
test/doctests.hs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Main where
|
||||||
|
|
||||||
|
import Build_doctests (flags, pkgs, module_sources)
|
||||||
|
import Data.Foldable (traverse_)
|
||||||
|
import Test.DocTest (doctest)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
traverse_ putStrLn args
|
||||||
|
doctest args
|
||||||
|
where
|
||||||
|
args = flags ++ pkgs ++ module_sources
|
@@ -1,24 +0,0 @@
|
|||||||
module Main where
|
|
||||||
|
|
||||||
import Control.Monad
|
|
||||||
import System.Exit
|
|
||||||
import qualified ShellCheck.Checker
|
|
||||||
import qualified ShellCheck.Analytics
|
|
||||||
import qualified ShellCheck.AnalyzerLib
|
|
||||||
import qualified ShellCheck.Parser
|
|
||||||
import qualified ShellCheck.Checks.Commands
|
|
||||||
import qualified ShellCheck.Checks.ShellSupport
|
|
||||||
|
|
||||||
main = do
|
|
||||||
putStrLn "Running ShellCheck tests..."
|
|
||||||
results <- sequence [
|
|
||||||
ShellCheck.Checker.runTests,
|
|
||||||
ShellCheck.Checks.Commands.runTests,
|
|
||||||
ShellCheck.Checks.ShellSupport.runTests,
|
|
||||||
ShellCheck.Analytics.runTests,
|
|
||||||
ShellCheck.AnalyzerLib.runTests,
|
|
||||||
ShellCheck.Parser.runTests
|
|
||||||
]
|
|
||||||
if and results
|
|
||||||
then exitSuccess
|
|
||||||
else exitFailure
|
|
27
test/stacktest
Executable file
27
test/stacktest
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/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 || die "Failed to setup with default resolver"
|
||||||
|
stack build --test || die "Failed to build/test with default resolver"
|
||||||
|
|
||||||
|
for resolver in "${resolvers[@]}"
|
||||||
|
do
|
||||||
|
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
|
||||||
|
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Success"
|
Reference in New Issue
Block a user