mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-10-01 01:09:18 +08:00
Compare commits
104 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cb57b4a74f | ||
|
e3f0243c0e | ||
|
66b5f13c6f | ||
|
a7a404a5a8 | ||
|
0761f5c923 | ||
|
b55149b22d | ||
|
4097bb5154 | ||
|
1b207b3d43 | ||
|
135b4aa485 | ||
|
cb76951ad2 | ||
|
705e476e4c | ||
|
e705552c97 | ||
|
198aa4fc3d | ||
|
f4044fbcc7 | ||
|
2827b35696 | ||
|
de95c376ea | ||
|
5e1b1e010a | ||
|
620c9c2023 | ||
|
359b1467a2 | ||
|
df0a0d41fa | ||
|
b815242506 | ||
|
07b5aa2971 | ||
|
f7b82658f4 | ||
|
e0e46e979a | ||
|
79319558a5 | ||
|
8d13add1ed | ||
|
8940e60300 | ||
|
5f1c969546 | ||
|
dadfdfde97 | ||
|
3e2cb26119 | ||
|
1a6ae4f19e | ||
|
95a376aad1 | ||
|
a06d7c1841 | ||
|
5202072a34 | ||
|
72af1cfd59 | ||
|
228af7df54 | ||
|
6db392511b | ||
|
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"):
|
||||||
|
- [ ] I read the issue's wiki page, e.g. https://github.com/koalaman/shellcheck/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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,3 +1,27 @@
|
|||||||
|
## 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
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -9,13 +9,13 @@ RUN apt-get update && apt-get install -y ghc cabal-install
|
|||||||
# 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)
|
||||||
COPY ShellCheck.cabal ./
|
COPY ShellCheck.cabal ./
|
||||||
RUN cabal update && cabal install --dependencies-only
|
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections"
|
||||||
|
|
||||||
# Copy source and build it
|
# Copy source and build it
|
||||||
COPY LICENSE Setup.hs shellcheck.hs ./
|
COPY LICENSE Setup.hs shellcheck.hs ./
|
||||||
COPY src src
|
COPY src src
|
||||||
RUN cabal build Paths_ShellCheck && \
|
RUN cabal build Paths_ShellCheck && \
|
||||||
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck && \
|
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \
|
||||||
strip --strip-all shellcheck
|
strip --strip-all shellcheck
|
||||||
|
|
||||||
RUN mkdir -p /out/bin && \
|
RUN mkdir -p /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)!
|
||||||
|
2
Setup.hs
2
Setup.hs
@@ -33,4 +33,4 @@ myPreSDist _ _ = do
|
|||||||
putStrLn $ "pandoc exited with " ++ show result
|
putStrLn $ "pandoc exited with " ++ show result
|
||||||
return emptyHookedBuildInfo
|
return emptyHookedBuildInfo
|
||||||
where
|
where
|
||||||
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
pandoc_cmd = "pandoc -s -f markdown-smart -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,6 +28,8 @@ 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
|
||||||
|
-- convenience script for stripping tests
|
||||||
|
striptests
|
||||||
-- tests
|
-- tests
|
||||||
test/shellcheck.hs
|
test/shellcheck.hs
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ custom-setup
|
|||||||
setup-depends:
|
setup-depends:
|
||||||
base >= 4 && <5,
|
base >= 4 && <5,
|
||||||
process >= 1.0 && <1.7,
|
process >= 1.0 && <1.7,
|
||||||
Cabal >= 1.10 && <2.3
|
Cabal >= 1.10 && <2.5
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
|
2
quickrun
2
quickrun
@@ -1,4 +1,4 @@
|
|||||||
#!/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.
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# quicktest runs the ShellCheck unit tests in an interpreted mode.
|
# quicktest runs the ShellCheck unit tests in an interpreted mode.
|
||||||
# This allows running tests without compiling, which can be faster.
|
# This allows running tests without compiling, which can be faster.
|
||||||
# 'cabal test' remains the source of truth.
|
# 'cabal test' remains the source of truth.
|
||||||
|
@@ -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
|
||||||
|
@@ -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"
|
||||||
|
@@ -168,6 +168,8 @@ nodeChecks = [
|
|||||||
,checkPipeToNowhere
|
,checkPipeToNowhere
|
||||||
,checkForLoopGlobVariables
|
,checkForLoopGlobVariables
|
||||||
,checkSubshelledTests
|
,checkSubshelledTests
|
||||||
|
,checkInvertedStringTest
|
||||||
|
,checkRedirectionToCommand
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -396,7 +398,7 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
|
|||||||
mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices
|
mapM_ (f . (\ n -> take (length l) $ drop n commands)) indices
|
||||||
return . not . null $ indices
|
return . not . null $ indices
|
||||||
for' l f = for l (first f)
|
for' l f = for l (first f)
|
||||||
first func (x:_) = func (getId x)
|
first func (x:_) = func (getId $ getCommandTokenOrThis x)
|
||||||
first _ _ = return ()
|
first _ _ = return ()
|
||||||
hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
|
hasShortParameter char = any (\x -> "-" `isPrefixOf` x && char `elem` x)
|
||||||
hasParameter string =
|
hasParameter string =
|
||||||
@@ -430,17 +432,22 @@ prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"
|
|||||||
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
|
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
|
||||||
prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
|
prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
|
||||||
prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"
|
prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"
|
||||||
|
prop_checkShebang8 = verifyTree checkShebang "#!bin/sh\ntrue"
|
||||||
|
prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue"
|
||||||
|
prop_checkShebang10= verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"
|
||||||
checkShebang params (T_Annotation _ list t) =
|
checkShebang params (T_Annotation _ list t) =
|
||||||
if any isOverride list then [] else checkShebang params t
|
if any isOverride list then [] else checkShebang params t
|
||||||
where
|
where
|
||||||
isOverride (ShellOverride _) = True
|
isOverride (ShellOverride _) = True
|
||||||
isOverride _ = False
|
isOverride _ = False
|
||||||
checkShebang params (T_Script id sb _) = execWriter $
|
checkShebang params (T_Script id sb _) = execWriter $ do
|
||||||
unless (shellTypeSpecified params) $ do
|
unless (shellTypeSpecified params) $ do
|
||||||
when (sb == "") $
|
when (sb == "") $
|
||||||
err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
|
err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
|
||||||
when (executableFromShebang sb == "ash") $
|
when (executableFromShebang sb == "ash") $
|
||||||
warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
|
warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
|
||||||
|
unless (null sb || "/" `isPrefixOf` sb) $
|
||||||
|
err id 2239 "Ensure the shebang uses an absolute path to the interpreter."
|
||||||
|
|
||||||
|
|
||||||
prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
|
prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
|
||||||
@@ -709,6 +716,7 @@ prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo
|
|||||||
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
|
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
|
||||||
prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
|
prop_checkArrayWithoutIndex7 = verifyTree checkArrayWithoutIndex "a=(a b); a+=c"
|
||||||
prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
|
prop_checkArrayWithoutIndex8 = verifyTree checkArrayWithoutIndex "declare -a foo; foo=bar;"
|
||||||
|
prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr <<< 'foo bar'; echo \"$arr\""
|
||||||
checkArrayWithoutIndex params _ =
|
checkArrayWithoutIndex params _ =
|
||||||
doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
|
doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
|
||||||
where
|
where
|
||||||
@@ -1019,7 +1027,7 @@ checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
|
|||||||
error t =
|
error t =
|
||||||
unless (isConstantNonRe t) $
|
unless (isConstantNonRe t) $
|
||||||
err (getId t) 2076
|
err (getId t) 2076
|
||||||
"Don't quote rhs of =~, it'll match literally rather than as a regex."
|
"Don't quote right-hand side of =~, it'll match literally rather than as a regex."
|
||||||
re = mkRegex "[][*.+()|]"
|
re = mkRegex "[][*.+()|]"
|
||||||
hasMetachars s = s `matches` re
|
hasMetachars s = s `matches` re
|
||||||
isConstantNonRe t = fromMaybe False $ do
|
isConstantNonRe t = fromMaybe False $ do
|
||||||
@@ -1029,13 +1037,16 @@ checkQuotedCondRegex _ _ = return ()
|
|||||||
|
|
||||||
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
|
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
|
||||||
prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
|
prop_checkGlobbedRegex2 = verify checkGlobbedRegex "[[ $foo =~ f* ]]"
|
||||||
prop_checkGlobbedRegex2a = verify checkGlobbedRegex "[[ $foo =~ \\#* ]]"
|
|
||||||
prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
|
prop_checkGlobbedRegex3 = verifyNot checkGlobbedRegex "[[ $foo =~ $foo ]]"
|
||||||
prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
|
prop_checkGlobbedRegex4 = verifyNot checkGlobbedRegex "[[ $foo =~ ^c.* ]]"
|
||||||
|
prop_checkGlobbedRegex5 = verifyNot checkGlobbedRegex "[[ $foo =~ \\* ]]"
|
||||||
|
prop_checkGlobbedRegex6 = verifyNot checkGlobbedRegex "[[ $foo =~ (o*) ]]"
|
||||||
|
prop_checkGlobbedRegex7 = verifyNot checkGlobbedRegex "[[ $foo =~ \\*foo ]]"
|
||||||
|
prop_checkGlobbedRegex8 = verifyNot checkGlobbedRegex "[[ $foo =~ x\\* ]]"
|
||||||
checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
|
checkGlobbedRegex _ (TC_Binary _ DoubleBracket "=~" _ rhs) =
|
||||||
let s = concat $ oversimplify rhs in
|
let s = concat $ oversimplify rhs in
|
||||||
when (isConfusedGlobRegex s) $
|
when (isConfusedGlobRegex s) $
|
||||||
warn (getId rhs) 2049 "=~ is for regex. Use == for globs."
|
warn (getId rhs) 2049 "=~ is for regex, but this looks like a glob. Use = instead."
|
||||||
checkGlobbedRegex _ _ = return ()
|
checkGlobbedRegex _ _ = return ()
|
||||||
|
|
||||||
|
|
||||||
@@ -1186,11 +1197,12 @@ prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow
|
|||||||
prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
|
prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
|
||||||
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
|
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
|
||||||
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
|
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
|
||||||
|
prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]"
|
||||||
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
|
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
|
||||||
| op `elem` ["=", "==", "!="] =
|
| op `elem` ["=", "==", "!="] =
|
||||||
warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
|
warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching."
|
||||||
checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
|
checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
|
||||||
| (op == "=" || op == "==") && isGlob word =
|
| op `elem` ["=", "==", "!="] && isGlob word =
|
||||||
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
|
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
|
||||||
checkComparisonAgainstGlob _ _ = return ()
|
checkComparisonAgainstGlob _ _ = return ()
|
||||||
|
|
||||||
@@ -1324,7 +1336,7 @@ prop_checkBackticks1 = verify checkBackticks "echo `foo`"
|
|||||||
prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
|
prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
|
||||||
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
|
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
|
||||||
checkBackticks _ (T_Backticked id list) | not (null list) =
|
checkBackticks _ (T_Backticked id list) | not (null list) =
|
||||||
style id 2006 "Use $(..) instead of legacy `..`."
|
style id 2006 "Use $(...) notation instead of legacy backticked `...`."
|
||||||
checkBackticks _ _ = return ()
|
checkBackticks _ _ = return ()
|
||||||
|
|
||||||
prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
|
prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
|
||||||
@@ -1598,6 +1610,7 @@ prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
|
|||||||
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
|
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
|
||||||
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
||||||
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
|
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
|
||||||
|
prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"
|
||||||
|
|
||||||
checkSpacefulness params t =
|
checkSpacefulness params t =
|
||||||
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
||||||
@@ -1879,6 +1892,7 @@ prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[
|
|||||||
prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"
|
prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"
|
||||||
prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"
|
prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"
|
||||||
prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"
|
prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"
|
||||||
|
prop_checkUnassignedReferences36= verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\""
|
||||||
checkUnassignedReferences params t = warnings
|
checkUnassignedReferences params t = warnings
|
||||||
where
|
where
|
||||||
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
||||||
@@ -2069,6 +2083,7 @@ prop_checkCdAndBack1 = verify checkCdAndBack "for f in *; do cd $f; git pull; cd
|
|||||||
prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
|
prop_checkCdAndBack2 = verifyNot checkCdAndBack "for f in *; do cd $f || continue; git pull; cd ..; done"
|
||||||
prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
|
prop_checkCdAndBack3 = verifyNot checkCdAndBack "while [[ $PWD != / ]]; do cd ..; done"
|
||||||
prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
|
prop_checkCdAndBack4 = verify checkCdAndBack "cd $tmp; foo; cd -"
|
||||||
|
prop_checkCdAndBack5 = verifyNot checkCdAndBack "cd ..; foo; cd .."
|
||||||
checkCdAndBack params = doLists
|
checkCdAndBack params = doLists
|
||||||
where
|
where
|
||||||
shell = shellType params
|
shell = shellType params
|
||||||
@@ -2091,10 +2106,20 @@ checkCdAndBack params = doLists
|
|||||||
getCmd (T_Pipeline id _ [x]) = getCommandName x
|
getCmd (T_Pipeline id _ [x]) = getCommandName x
|
||||||
getCmd _ = Nothing
|
getCmd _ = Nothing
|
||||||
|
|
||||||
|
findCdPair list =
|
||||||
|
case list of
|
||||||
|
(a:b:rest) ->
|
||||||
|
if isCdRevert b && not (isCdRevert a)
|
||||||
|
then return $ getId b
|
||||||
|
else findCdPair (b:rest)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
doList list =
|
doList list =
|
||||||
let cds = filter ((== Just "cd") . getCmd) list in
|
let cds = filter ((== Just "cd") . getCmd) list in
|
||||||
when (length cds >= 2 && isCdRevert (last cds)) $
|
potentially $ do
|
||||||
info (getId $ last cds) 2103 message
|
cd <- findCdPair cds
|
||||||
|
return $ info cd 2103 message
|
||||||
|
|
||||||
message = "Use a ( subshell ) to avoid having to cd back."
|
message = "Use a ( subshell ) to avoid having to cd back."
|
||||||
|
|
||||||
@@ -2472,7 +2497,7 @@ prop_checkReadWithoutR1 = verify checkReadWithoutR "read -a foo"
|
|||||||
prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
|
prop_checkReadWithoutR2 = verifyNot checkReadWithoutR "read -ar foo"
|
||||||
checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
|
checkReadWithoutR _ t@T_SimpleCommand {} | t `isUnqualifiedCommand` "read" =
|
||||||
unless ("r" `elem` map snd (getAllFlags t)) $
|
unless ("r" `elem` map snd (getAllFlags t)) $
|
||||||
info (getId t) 2162 "read without -r will mangle backslashes."
|
info (getId $ getCommandTokenOrThis t) 2162 "read without -r will mangle backslashes."
|
||||||
checkReadWithoutR _ _ = return ()
|
checkReadWithoutR _ _ = return ()
|
||||||
|
|
||||||
prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"
|
prop_checkUncheckedCd1 = verifyTree checkUncheckedCdPushdPopd "cd ~/src; rm -r foo"
|
||||||
@@ -2501,6 +2526,7 @@ prop_checkUncheckedPopd5 = verifyTree checkUncheckedCdPushdPopd "if true; then p
|
|||||||
prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd"
|
prop_checkUncheckedPopd6 = verifyTree checkUncheckedCdPushdPopd "popd"
|
||||||
prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"
|
prop_checkUncheckedPopd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\npopd\nrm bar"
|
||||||
prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"
|
prop_checkUncheckedPopd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; popd; rm bar"
|
||||||
|
prop_checkUncheckedPopd9 = verifyNotTree checkUncheckedCdPushdPopd "popd -n foo"
|
||||||
|
|
||||||
checkUncheckedCdPushdPopd params root =
|
checkUncheckedCdPushdPopd params root =
|
||||||
if hasSetE params then
|
if hasSetE params then
|
||||||
@@ -2510,7 +2536,7 @@ checkUncheckedCdPushdPopd params root =
|
|||||||
checkElement t@T_SimpleCommand {} =
|
checkElement t@T_SimpleCommand {} =
|
||||||
when(name t `elem` ["cd", "pushd", "popd"]
|
when(name t `elem` ["cd", "pushd", "popd"]
|
||||||
&& not (isSafeDir t)
|
&& not (isSafeDir t)
|
||||||
&& not (name t == "pushd" && ("n" `elem` map snd (getAllFlags t)))
|
&& not (name t `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))
|
||||||
&& not (isCondition $ getPath (parentMap params) t)) $
|
&& not (isCondition $ getPath (parentMap params) t)) $
|
||||||
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
|
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
|
||||||
checkElement _ = return ()
|
checkElement _ = return ()
|
||||||
@@ -2685,26 +2711,31 @@ prop_checkUnmatchableCases5 = verify checkUnmatchableCases "case $f in *.txt) tr
|
|||||||
prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac"
|
prop_checkUnmatchableCases6 = verifyNot checkUnmatchableCases "case $f in ?*) true;; *) false;; esac"
|
||||||
prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac"
|
prop_checkUnmatchableCases7 = verifyNot checkUnmatchableCases "case $f in $(x)) true;; asdf) false;; esac"
|
||||||
prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac"
|
prop_checkUnmatchableCases8 = verify checkUnmatchableCases "case $f in cow) true;; bar|cow) false;; esac"
|
||||||
|
prop_checkUnmatchableCases9 = verifyNot checkUnmatchableCases "case $f in x) true;;& x) false;; esac"
|
||||||
checkUnmatchableCases _ t =
|
checkUnmatchableCases _ t =
|
||||||
case t of
|
case t of
|
||||||
T_CaseExpression _ word list -> do
|
T_CaseExpression _ word list -> do
|
||||||
let patterns = concatMap snd3 list
|
-- Check all patterns for whether they can ever match
|
||||||
|
let allpatterns = concatMap snd3 list
|
||||||
|
-- Check only the non-fallthrough branches for shadowing
|
||||||
|
let breakpatterns = concatMap snd3 $ filter (\x -> fst3 x == CaseBreak) list
|
||||||
|
|
||||||
if isConstant word
|
if isConstant word
|
||||||
then warn (getId word) 2194
|
then warn (getId word) 2194
|
||||||
"This word is constant. Did you forget the $ on a variable?"
|
"This word is constant. Did you forget the $ on a variable?"
|
||||||
else potentially $ do
|
else potentially $ do
|
||||||
pg <- wordToPseudoGlob word
|
pg <- wordToPseudoGlob word
|
||||||
return $ mapM_ (check pg) patterns
|
return $ mapM_ (check pg) allpatterns
|
||||||
|
|
||||||
let exactGlobs = tupMap wordToExactPseudoGlob patterns
|
let exactGlobs = tupMap wordToExactPseudoGlob breakpatterns
|
||||||
let fuzzyGlobs = tupMap wordToPseudoGlob patterns
|
let fuzzyGlobs = tupMap wordToPseudoGlob breakpatterns
|
||||||
let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)
|
let dominators = zip exactGlobs (tails $ drop 1 fuzzyGlobs)
|
||||||
|
|
||||||
mapM_ checkDoms dominators
|
mapM_ checkDoms dominators
|
||||||
|
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
where
|
where
|
||||||
|
fst3 (x,_,_) = x
|
||||||
snd3 (_,x,_) = x
|
snd3 (_,x,_) = x
|
||||||
check target candidate = potentially $ do
|
check target candidate = potentially $ do
|
||||||
candidateGlob <- wordToPseudoGlob candidate
|
candidateGlob <- wordToPseudoGlob candidate
|
||||||
@@ -2837,6 +2868,7 @@ prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"
|
|||||||
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
|
prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
|
||||||
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
|
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
|
||||||
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
|
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
|
||||||
|
prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin"
|
||||||
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
||||||
checkPipeToNowhere _ t =
|
checkPipeToNowhere _ t =
|
||||||
case t of
|
case t of
|
||||||
@@ -2849,15 +2881,25 @@ checkPipeToNowhere _ t =
|
|||||||
name <- getCommandBasename cmd
|
name <- getCommandBasename cmd
|
||||||
guard $ name `elem` nonReadingCommands
|
guard $ name `elem` nonReadingCommands
|
||||||
guard . not $ hasAdditionalConsumers cmd
|
guard . not $ hasAdditionalConsumers cmd
|
||||||
|
-- Confusing echo for cat is so common that it's worth a special case
|
||||||
|
let suggestion =
|
||||||
|
if name == "echo"
|
||||||
|
then "Did you want 'cat' instead?"
|
||||||
|
else "Wrong command or missing xargs?"
|
||||||
return $ warn (getId cmd) 2216 $
|
return $ warn (getId cmd) 2216 $
|
||||||
"Piping to '" ++ name ++ "', a command that doesn't read stdin. Wrong command or missing xargs?"
|
"Piping to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
|
||||||
|
|
||||||
checkRedir cmd = potentially $ do
|
checkRedir cmd = potentially $ do
|
||||||
name <- getCommandBasename cmd
|
name <- getCommandBasename cmd
|
||||||
guard $ name `elem` nonReadingCommands
|
guard $ name `elem` nonReadingCommands
|
||||||
guard . not $ hasAdditionalConsumers cmd
|
guard . not $ hasAdditionalConsumers cmd
|
||||||
|
guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i"
|
||||||
|
let suggestion =
|
||||||
|
if name == "echo"
|
||||||
|
then "Did you want 'cat' instead?"
|
||||||
|
else "Bad quoting, wrong command or missing xargs?"
|
||||||
return $ warn (getId cmd) 2217 $
|
return $ warn (getId cmd) 2217 $
|
||||||
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. Bad quoting or missing xargs?"
|
"Redirecting to '" ++ name ++ "', a command that doesn't read stdin. " ++ suggestion
|
||||||
|
|
||||||
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
|
-- Could any words in a SimpleCommand consume stdin (e.g. echo "$(cat)")?
|
||||||
hasAdditionalConsumers t = fromMaybe True $ do
|
hasAdditionalConsumers t = fromMaybe True $ do
|
||||||
@@ -2926,9 +2968,10 @@ checkForLoopGlobVariables _ t =
|
|||||||
prop_checkSubshelledTests1 = verify checkSubshelledTests "a && ( [ b ] || ! [ c ] )"
|
prop_checkSubshelledTests1 = verify checkSubshelledTests "a && ( [ b ] || ! [ c ] )"
|
||||||
prop_checkSubshelledTests2 = verify checkSubshelledTests "( [ a ] )"
|
prop_checkSubshelledTests2 = verify checkSubshelledTests "( [ a ] )"
|
||||||
prop_checkSubshelledTests3 = verify checkSubshelledTests "( [ a ] && [ b ] || test c )"
|
prop_checkSubshelledTests3 = verify checkSubshelledTests "( [ a ] && [ b ] || test c )"
|
||||||
|
prop_checkSubshelledTests4 = verify checkSubshelledTests "( [ a ] && { [ b ] && [ c ]; } )"
|
||||||
checkSubshelledTests params t =
|
checkSubshelledTests params t =
|
||||||
case t of
|
case t of
|
||||||
T_Subshell id list | isSubshelledTest t ->
|
T_Subshell id list | all isTestStructure list ->
|
||||||
case () of
|
case () of
|
||||||
-- Special case for if (test) and while (test)
|
-- Special case for if (test) and while (test)
|
||||||
_ | isCompoundCondition (getPath (parentMap params) t) ->
|
_ | isCompoundCondition (getPath (parentMap params) t) ->
|
||||||
@@ -2948,19 +2991,24 @@ checkSubshelledTests params t =
|
|||||||
[c] | isTestCommand c -> True
|
[c] | isTestCommand c -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
isSubshelledTest t =
|
isTestStructure t =
|
||||||
case t of
|
case t of
|
||||||
T_Subshell _ list -> all isSubshelledTest list
|
T_Banged _ t -> isTestStructure t
|
||||||
T_AndIf _ a b -> isSubshelledTest a && isSubshelledTest b
|
T_AndIf _ a b -> isTestStructure a && isTestStructure b
|
||||||
T_OrIf _ a b -> isSubshelledTest a && isSubshelledTest b
|
T_OrIf _ a b -> isTestStructure a && isTestStructure b
|
||||||
T_Annotation _ _ t -> isSubshelledTest t
|
T_Pipeline _ [] [T_Redirecting _ _ cmd] ->
|
||||||
|
case cmd of
|
||||||
|
T_BraceGroup _ ts -> all isTestStructure ts
|
||||||
|
T_Subshell _ ts -> all isTestStructure ts
|
||||||
|
_ -> isTestCommand t
|
||||||
_ -> isTestCommand t
|
_ -> isTestCommand t
|
||||||
|
|
||||||
isTestCommand t =
|
isTestCommand t =
|
||||||
case t of
|
case t of
|
||||||
T_Banged _ t -> isTestCommand t
|
T_Pipeline _ [] [T_Redirecting _ _ cmd] ->
|
||||||
T_Pipeline _ [] [T_Redirecting _ _ T_Condition {}] -> True
|
case cmd of
|
||||||
T_Pipeline _ [] [T_Redirecting _ _ cmd] -> cmd `isCommand` "test"
|
T_Condition {} -> True
|
||||||
|
_ -> cmd `isCommand` "test"
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
-- Check if a T_Subshell is used as a condition, e.g. if ( test )
|
-- Check if a T_Subshell is used as a condition, e.g. if ( test )
|
||||||
@@ -2981,5 +3029,35 @@ checkSubshelledTests params t =
|
|||||||
T_Annotation {} -> True
|
T_Annotation {} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
prop_checkInvertedStringTest1 = verify checkInvertedStringTest "[ ! -z $var ]"
|
||||||
|
prop_checkInvertedStringTest2 = verify checkInvertedStringTest "! [[ -n $var ]]"
|
||||||
|
prop_checkInvertedStringTest3 = verifyNot checkInvertedStringTest "! [ -x $var ]"
|
||||||
|
prop_checkInvertedStringTest4 = verifyNot checkInvertedStringTest "[[ ! -w $var ]]"
|
||||||
|
prop_checkInvertedStringTest5 = verifyNot checkInvertedStringTest "[ -z $var ]"
|
||||||
|
checkInvertedStringTest _ t =
|
||||||
|
case t of
|
||||||
|
TC_Unary _ _ "!" (TC_Unary _ _ op _) ->
|
||||||
|
case op of
|
||||||
|
"-n" -> style (getId t) 2236 "Use -z instead of ! -n."
|
||||||
|
"-z" -> style (getId t) 2236 "Use -n instead of ! -z."
|
||||||
|
_ -> return ()
|
||||||
|
T_Banged _ (T_Pipeline _ _
|
||||||
|
[T_Redirecting _ _ (T_Condition _ _ (TC_Unary _ _ op _))]) ->
|
||||||
|
case op of
|
||||||
|
"-n" -> style (getId t) 2237 "Use [ -z .. ] instead of ! [ -n .. ]."
|
||||||
|
"-z" -> style (getId t) 2237 "Use [ -n .. ] instead of ! [ -z .. ]."
|
||||||
|
_ -> return ()
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
prop_checkRedirectionToCommand1 = verify checkRedirectionToCommand "ls > rm"
|
||||||
|
prop_checkRedirectionToCommand2 = verifyNot checkRedirectionToCommand "ls > 'rm'"
|
||||||
|
prop_checkRedirectionToCommand3 = verifyNot checkRedirectionToCommand "ls > myfile"
|
||||||
|
checkRedirectionToCommand _ t =
|
||||||
|
case t of
|
||||||
|
T_IoFile _ _ (T_NormalWord id [T_Literal _ str]) | str `elem` commonCommands ->
|
||||||
|
unless (str == "file") $ -- This would be confusing
|
||||||
|
warn id 2238 "Redirecting to/from command name instead of file. Did you want pipes/xargs (or quote to ignore)?"
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
@@ -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
|
||||||
|
@@ -109,19 +109,17 @@ data DataSource =
|
|||||||
|
|
||||||
data VariableState = Dead Token String | Alive deriving (Show)
|
data VariableState = Dead Token String | Alive deriving (Show)
|
||||||
|
|
||||||
defaultSpec root = AnalysisSpec {
|
defaultSpec root = spec {
|
||||||
asScript = root,
|
|
||||||
asShellType = Nothing,
|
asShellType = Nothing,
|
||||||
asCheckSourced = False,
|
asCheckSourced = False,
|
||||||
asExecutionMode = Executed
|
asExecutionMode = Executed
|
||||||
}
|
} where spec = newAnalysisSpec root
|
||||||
|
|
||||||
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 prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||||
|
|
||||||
@@ -135,7 +133,14 @@ producesComments c s = do
|
|||||||
|
|
||||||
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 = tell [note]
|
||||||
|
|
||||||
@@ -235,9 +240,10 @@ 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
|
||||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
_:rest -> case rest of [] -> put (rest, map)
|
||||||
|
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||||
|
|
||||||
-- Given a root node, make a map from Id to Token
|
-- Given a root node, make a map from Id to Token
|
||||||
getTokenMap :: Token -> Map.Map Id Token
|
getTokenMap :: Token -> Map.Map Id Token
|
||||||
@@ -520,12 +526,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 +584,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 +633,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
|
||||||
@@ -812,10 +837,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 +848,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) =
|
||||||
|
@@ -37,25 +37,30 @@ import Control.Monad
|
|||||||
|
|
||||||
import Test.QuickCheck.All
|
import Test.QuickCheck.All
|
||||||
|
|
||||||
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
tokenToPosition startMap t = fromMaybe fail $ do
|
||||||
position <- Map.lookup id map
|
span <- Map.lookup (tcId t) startMap
|
||||||
return $ PositionedComment position position c
|
return $ newPositionedComment {
|
||||||
|
pcStartPos = fst span,
|
||||||
|
pcEndPos = snd span,
|
||||||
|
pcComment = tcComment 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 analysisMessages =
|
let analysisMessages =
|
||||||
@@ -66,27 +71,38 @@ checkScript sys spec = do
|
|||||||
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 root =
|
||||||
AnalysisSpec {
|
as {
|
||||||
asScript = root,
|
asScript = root,
|
||||||
asShellType = csShellTypeOverride spec,
|
asShellType = csShellTypeOverride spec,
|
||||||
asCheckSourced = csCheckSourced spec,
|
asCheckSourced = csCheckSourced spec,
|
||||||
asExecutionMode = Executed
|
asExecutionMode = Executed
|
||||||
}
|
} where as = newAnalysisSpec root
|
||||||
|
|
||||||
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 []
|
||||||
|
|
||||||
@@ -136,6 +152,21 @@ prop_optionDisablesIssue2 =
|
|||||||
csExcludedWarnings = [2148, 1037]
|
csExcludedWarnings = [2148, 1037]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prop_wontParseBadShell =
|
||||||
|
[1071] == check "#!/usr/bin/python\ntrue $1\n"
|
||||||
|
|
||||||
|
prop_optionDisablesBadShebang =
|
||||||
|
null $ getErrors
|
||||||
|
(mockedSystemInterface [])
|
||||||
|
emptyCheckSpec {
|
||||||
|
csScript = "#!/usr/bin/python\ntrue\n",
|
||||||
|
csShellTypeOverride = Just Sh
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_annotationDisablesBadShebang =
|
||||||
|
[] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
|
||||||
|
|
||||||
|
|
||||||
prop_canParseDevNull =
|
prop_canParseDevNull =
|
||||||
[] == check "source /dev/null"
|
[] == check "source /dev/null"
|
||||||
|
|
||||||
@@ -180,7 +211,7 @@ prop_filewideAnnotation1 = null $
|
|||||||
prop_filewideAnnotation2 = null $
|
prop_filewideAnnotation2 = null $
|
||||||
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
||||||
prop_filewideAnnotation3 = null $
|
prop_filewideAnnotation3 = null $
|
||||||
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"
|
check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
|
||||||
prop_filewideAnnotation4 = null $
|
prop_filewideAnnotation4 = null $
|
||||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||||
prop_filewideAnnotation5 = null $
|
prop_filewideAnnotation5 = null $
|
||||||
@@ -197,6 +228,5 @@ prop_filewideAnnotation8 = null $
|
|||||||
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
||||||
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
||||||
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
||||||
|
@@ -61,6 +61,7 @@ commandChecks = [
|
|||||||
,checkGrepRe
|
,checkGrepRe
|
||||||
,checkTrapQuotes
|
,checkTrapQuotes
|
||||||
,checkReturn
|
,checkReturn
|
||||||
|
,checkExit
|
||||||
,checkFindExecWithSingleArgument
|
,checkFindExecWithSingleArgument
|
||||||
,checkUnusedEchoEscapes
|
,checkUnusedEchoEscapes
|
||||||
,checkInjectableFindSh
|
,checkInjectableFindSh
|
||||||
@@ -92,6 +93,7 @@ commandChecks = [
|
|||||||
,checkWhich
|
,checkWhich
|
||||||
,checkSudoRedirect
|
,checkSudoRedirect
|
||||||
,checkSudoArgs
|
,checkSudoArgs
|
||||||
|
,checkSourceArgs
|
||||||
]
|
]
|
||||||
|
|
||||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||||
@@ -141,6 +143,7 @@ prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
|||||||
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||||
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||||
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
||||||
|
prop_checkTr12= 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 +155,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 ()
|
||||||
@@ -183,7 +186,7 @@ prop_checkNeedlessExpr4 = 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 = [ ":", "<", ">", "<=", ">=" ]
|
||||||
@@ -279,15 +282,28 @@ prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
|
|||||||
prop_checkReturn5 = verify checkReturn "return -1"
|
prop_checkReturn5 = verify checkReturn "return -1"
|
||||||
prop_checkReturn6 = verify checkReturn "return 1000"
|
prop_checkReturn6 = verify checkReturn "return 1000"
|
||||||
prop_checkReturn7 = verify checkReturn "return 'hello world'"
|
prop_checkReturn7 = verify checkReturn "return 'hello world'"
|
||||||
checkReturn = CommandCheck (Exactly "return") (f . arguments)
|
checkReturn = CommandCheck (Exactly "return") (returnOrExit
|
||||||
|
(\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")
|
||||||
|
(\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))
|
||||||
|
|
||||||
|
prop_checkExit1 = verifyNot checkExit "exit"
|
||||||
|
prop_checkExit2 = verifyNot checkExit "exit 1"
|
||||||
|
prop_checkExit3 = verifyNot checkExit "exit $var"
|
||||||
|
prop_checkExit4 = verifyNot checkExit "exit $((a|b))"
|
||||||
|
prop_checkExit5 = verify checkExit "exit -1"
|
||||||
|
prop_checkExit6 = verify checkExit "exit 1000"
|
||||||
|
prop_checkExit7 = verify checkExit "exit 'hello world'"
|
||||||
|
checkExit = CommandCheck (Exactly "exit") (returnOrExit
|
||||||
|
(\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")
|
||||||
|
(\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))
|
||||||
|
|
||||||
|
returnOrExit multi invalid = (f . arguments)
|
||||||
where
|
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
|
||||||
@@ -324,26 +340,18 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
|
|||||||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
|
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_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
|
||||||
@@ -515,6 +523,9 @@ prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
|||||||
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
||||||
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
|
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
|
||||||
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
||||||
|
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
|
||||||
|
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
|
||||||
|
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
||||||
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 +536,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
|
||||||
@@ -678,20 +698,24 @@ prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
|
|||||||
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
|
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
|
||||||
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
|
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ."
|
||||||
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
|
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ."
|
||||||
|
prop_checkFindWithoutPath7 = verifyNot checkFindWithoutPath "find --help"
|
||||||
|
prop_checkFindWithoutPath8 = verifyNot checkFindWithoutPath "find -Hx . -print"
|
||||||
checkFindWithoutPath = CommandCheck (Basename "find") f
|
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_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
|
||||||
@@ -740,20 +764,20 @@ 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_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
prop_checkDeprecatedTempfile2 = 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_checkDeprecatedEgrep = 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_checkDeprecatedFgrep = 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_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_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
|
||||||
@@ -947,7 +971,7 @@ checkFindRedirections = CommandCheck (Basename "find") f
|
|||||||
|
|
||||||
prop_checkWhich = verify checkWhich "which '.+'"
|
prop_checkWhich = 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_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
|
||||||
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
||||||
@@ -999,5 +1023,16 @@ 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:"
|
||||||
|
|
||||||
|
prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg"
|
||||||
|
prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script"
|
||||||
|
prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg"
|
||||||
|
checkSourceArgs = CommandCheck (Exactly ".") f
|
||||||
|
where
|
||||||
|
f t = whenShell [Sh, Dash] $
|
||||||
|
case arguments t of
|
||||||
|
(file:arg1:_) -> warn (getId arg1) 2240 $
|
||||||
|
"The dot command does not support arguments in sh/dash. Set them as variables."
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
@@ -127,7 +127,7 @@ prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
|||||||
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||||
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
|
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
||||||
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||||
@@ -302,11 +302,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))
|
||||||
|
@@ -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 {
|
||||||
posColumn = realignColumn lineNo colNo c
|
pcStartPos = (pcStartPos c) {
|
||||||
} end {
|
posColumn = realignColumn lineNo colNo c
|
||||||
posColumn = realignColumn endLineNo endColNo c
|
}
|
||||||
} comment
|
, pcEndPos = (pcEndPos c) {
|
||||||
|
posColumn = realignColumn endLineNo endColNo c
|
||||||
|
}
|
||||||
|
}
|
||||||
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)
|
||||||
|
@@ -40,7 +40,10 @@ format = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
instance ToJSON (PositionedComment) where
|
instance ToJSON (PositionedComment) where
|
||||||
toJSON comment@(PositionedComment start end (Comment level code string)) =
|
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 +51,14 @@ 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
|
||||||
]
|
]
|
||||||
|
|
||||||
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,8 +66,8 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||||
|
@@ -22,18 +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 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
|
||||||
header = return (),
|
topErrorRef <- newIORef []
|
||||||
footer = return (),
|
return Formatter {
|
||||||
onFailure = outputError options,
|
header = return (),
|
||||||
onResult = outputResult options
|
footer = outputWiki topErrorRef,
|
||||||
}
|
onFailure = outputError options,
|
||||||
|
onResult = outputResult options topErrorRef
|
||||||
|
}
|
||||||
|
|
||||||
colorForLevel level =
|
colorForLevel level =
|
||||||
case level of
|
case level of
|
||||||
@@ -45,13 +54,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
|
||||||
|
|
||||||
@@ -78,9 +134,16 @@ outputForFile color sys comments = do
|
|||||||
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,7 +17,39 @@
|
|||||||
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
|
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)
|
||||||
|
, 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)
|
||||||
|
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
||||||
|
, TokenComment(tcId, tcComment)
|
||||||
|
, emptyCheckResult
|
||||||
|
, newParseResult
|
||||||
|
, newAnalysisSpec
|
||||||
|
, newAnalysisResult
|
||||||
|
, newFormatterOptions
|
||||||
|
, newPosition
|
||||||
|
, newTokenComment
|
||||||
|
, mockedSystemInterface
|
||||||
|
, newParseSpec
|
||||||
|
, emptyCheckSpec
|
||||||
|
, newPositionedComment
|
||||||
|
, newComment
|
||||||
|
) where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
@@ -35,7 +67,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,28 +76,51 @@ 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,
|
||||||
@@ -73,14 +129,30 @@ data AnalysisSpec = AnalysisSpec {
|
|||||||
asCheckSourced :: Bool
|
asCheckSourced :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newAnalysisSpec token = AnalysisSpec {
|
||||||
|
asScript = token,
|
||||||
|
asShellType = Nothing,
|
||||||
|
asExecutionMode = Executed,
|
||||||
|
asCheckSourced = False
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -98,9 +170,48 @@ data Position = Position {
|
|||||||
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)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
newComment :: Comment
|
||||||
|
newComment = Comment {
|
||||||
|
cSeverity = StyleC,
|
||||||
|
cCode = 0,
|
||||||
|
cMessage = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
data PositionedComment = PositionedComment {
|
||||||
|
pcStartPos :: Position,
|
||||||
|
pcEndPos :: Position,
|
||||||
|
pcComment :: Comment
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
newPositionedComment :: PositionedComment
|
||||||
|
newPositionedComment = PositionedComment {
|
||||||
|
pcStartPos = newPosition,
|
||||||
|
pcEndPos = newPosition,
|
||||||
|
pcComment = newComment
|
||||||
|
}
|
||||||
|
|
||||||
|
data TokenComment = TokenComment {
|
||||||
|
tcId :: Id,
|
||||||
|
tcComment :: Comment
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
newTokenComment = TokenComment {
|
||||||
|
tcId = Id 0,
|
||||||
|
tcComment = newComment
|
||||||
|
}
|
||||||
|
|
||||||
data ColorOption =
|
data ColorOption =
|
||||||
ColorAuto
|
ColorAuto
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
|||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
sponge() {
|
sponge() {
|
||||||
|
local data
|
||||||
data="$(cat)"
|
data="$(cat)"
|
||||||
printf '%s\n' "$data" > "$1"
|
printf '%s\n' "$data" > "$1"
|
||||||
}
|
}
|
||||||
@@ -22,7 +23,7 @@ modify() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detestify() {
|
detestify() {
|
||||||
echo "-- AUTOGENERATED from ShellCheck by striptests. Do not modify."
|
printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
|
||||||
awk '
|
awk '
|
||||||
BEGIN {
|
BEGIN {
|
||||||
state = 0;
|
state = 0;
|
||||||
@@ -52,7 +53,7 @@ detestify() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
if [[ ! -e ShellCheck.cabal ]]
|
if [[ ! -e 'ShellCheck.cabal' ]]
|
||||||
then
|
then
|
||||||
echo "Run me from the ShellCheck directory." >&2
|
echo "Run me from the ShellCheck directory." >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -64,7 +65,7 @@ then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
modify ShellCheck.cabal sed -e '
|
modify 'ShellCheck.cabal' sed -e '
|
||||||
/QuickCheck/d
|
/QuickCheck/d
|
||||||
/^test-suite/{ s/.*//; q; }
|
/^test-suite/{ s/.*//; q; }
|
||||||
'
|
'
|
||||||
|
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"
|
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