mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 16:59:20 +08:00
Compare commits
130 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a3be776f80 | ||
|
b5e5d249c4 | ||
|
efffc6150b | ||
|
cb4a0c0250 | ||
|
135cf5932f | ||
|
467dfe07b6 | ||
|
d140388bea | ||
|
884eff0c36 | ||
|
1d8047cce1 | ||
|
77546fba2f | ||
|
4a5ee06ce4 | ||
|
4dceecb1ed | ||
|
5b226e733b | ||
|
46c10c1571 | ||
|
1de8ba0210 | ||
|
407f6a63b9 | ||
|
7ee7448a70 | ||
|
48ebd41e22 | ||
|
0c88fbc76d | ||
|
b3362f1dc3 | ||
|
2c6bc43614 | ||
|
235bf6605f | ||
|
7029a713c7 | ||
|
cf608dc2f6 | ||
|
aa3b3fdc56 | ||
|
bca2ad4e18 | ||
|
719e1854e5 | ||
|
20ad7dc8de | ||
|
f84859ab90 | ||
|
08235a1cb2 | ||
|
728922d2b8 | ||
|
a953dd3454 | ||
|
ef6a5b97b9 | ||
|
8873a1732b | ||
|
5adfce72e1 | ||
|
12b3fdf661 | ||
|
bb4ce86fab | ||
|
7ec2fa2d3e | ||
|
5481ccd7f7 | ||
|
a1d8947297 | ||
|
683a30abde | ||
|
573936f353 | ||
|
ce7658ed86 | ||
|
0136d9ccce | ||
|
a4c6cea5e6 | ||
|
32af2783f0 | ||
|
08aab3c161 | ||
|
cf39adff75 | ||
|
da4072a118 | ||
|
08d2eef411 | ||
|
de257a6cf3 | ||
|
68c24925bc | ||
|
366dc5d3f8 | ||
|
1ed743e410 | ||
|
9a2aad16ad | ||
|
177cb10daa | ||
|
4aca1ff128 | ||
|
ffed7caff4 | ||
|
ef28200199 | ||
|
55216792c9 | ||
|
51e115cf47 | ||
|
764b242f1b | ||
|
c3b606c68a | ||
|
9f53109dfa | ||
|
795a881219 | ||
|
6dd5350e3b | ||
|
a5b359591c | ||
|
966194e387 | ||
|
71bcc80c2f | ||
|
48616225b3 | ||
|
99276cb9f5 | ||
|
f769d4e92c | ||
|
71df01c00f | ||
|
5364701914 | ||
|
fb97aca5a6 | ||
|
6b81a9924c | ||
|
cd7c077ecc | ||
|
b33607b048 | ||
|
969230f171 | ||
|
a98d69f4ff | ||
|
f71c142a44 | ||
|
9dfcf54f10 | ||
|
c8cd9dd09c | ||
|
8b8aeb4409 | ||
|
ee354ffce8 | ||
|
9fc3ddf849 | ||
|
ecb9d07f52 | ||
|
d16bf41c3d | ||
|
8d5e3a80ae | ||
|
34e0fa53c8 | ||
|
7fb27310e1 | ||
|
00d3c09ddb | ||
|
e8fc09414a | ||
|
b7a8b090d2 | ||
|
72044a79c6 | ||
|
6511dc0246 | ||
|
740441f2c4 | ||
|
b311563421 | ||
|
6d257bfa17 | ||
|
d8717c7046 | ||
|
7aa3a7ffc3 | ||
|
017af8333f | ||
|
f73d6f2332 | ||
|
a840f4e464 | ||
|
0f5e40c076 | ||
|
ccaacb108a | ||
|
56751413b4 | ||
|
ba5f20deda | ||
|
c86885427c | ||
|
7b3c4025fb | ||
|
3b004275cf | ||
|
72971fa52b | ||
|
dbdab5705f | ||
|
46a3019ed7 | ||
|
81978d15bd | ||
|
1d0db9267d | ||
|
a6fb9d1ef8 | ||
|
dc1e7c1bd4 | ||
|
5b14dba489 | ||
|
ee997fdec4 | ||
|
1badeff383 | ||
|
2d5ed23ca1 | ||
|
ec581cee90 | ||
|
bb32289ee3 | ||
|
31d6b063d9 | ||
|
3c5c74ff04 | ||
|
9657e8dda3 | ||
|
6ed60b403f | ||
|
8fa8823981 | ||
|
161801a86e |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
# Created by http://www.gitignore.io
|
# Created by https://www.gitignore.io
|
||||||
|
|
||||||
### Haskell ###
|
### Haskell ###
|
||||||
dist
|
dist
|
||||||
@@ -13,3 +13,10 @@ cabal-dev
|
|||||||
cabal.sandbox.config
|
cabal.sandbox.config
|
||||||
cabal.config
|
cabal.config
|
||||||
.stack-work
|
.stack-work
|
||||||
|
|
||||||
|
### Snap ###
|
||||||
|
/snap/.snapcraft/
|
||||||
|
/stage/
|
||||||
|
/parts/
|
||||||
|
/prime/
|
||||||
|
*.snap
|
||||||
|
@@ -7,7 +7,7 @@ cd deploy
|
|||||||
cp ../LICENSE LICENSE.txt
|
cp ../LICENSE LICENSE.txt
|
||||||
sed -e $'s/$/\r/' > README.txt << END
|
sed -e $'s/$/\r/' > README.txt << END
|
||||||
This is a precompiled ShellCheck binary.
|
This is a precompiled ShellCheck binary.
|
||||||
http://www.shellcheck.net/
|
https://www.shellcheck.net/
|
||||||
|
|
||||||
ShellCheck is a static analysis tool for shell scripts.
|
ShellCheck is a static analysis tool for shell scripts.
|
||||||
It's licensed under the GNU General Public License v3.0.
|
It's licensed under the GNU General Public License v3.0.
|
||||||
|
40
.travis.yml
40
.travis.yml
@@ -10,45 +10,49 @@ before_install:
|
|||||||
- DOCKER_BUILDS=""
|
- DOCKER_BUILDS=""
|
||||||
- TAGS=""
|
- TAGS=""
|
||||||
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
||||||
- test -n "$TRAVIS_TAG" && TAGS="$TAGS $TRAVIS_TAG" || true
|
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
||||||
- test "$TRAVIS_BRANCH" = master && test -n "$TRAVIS_TAG" && TAGS="$TAGS stable" || true
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- mkdir deploy
|
- mkdir deploy
|
||||||
# Windows .exe
|
# Remove all tests to reduce binary size
|
||||||
- docker pull koalaman/winghc
|
- ./striptests
|
||||||
- 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
|
|
||||||
- rm -rf dist || true
|
|
||||||
# Linux static executable
|
|
||||||
- docker pull koalaman/scbuilder
|
|
||||||
- docker run --user="$UID" --rm -v "$PWD:/mnt" koalaman/scbuilder
|
|
||||||
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux"; done
|
|
||||||
- ./shellcheck --version
|
|
||||||
- rm -rf dist || true
|
|
||||||
# Linux Docker image
|
# Linux Docker image
|
||||||
- name="$DOCKER_BASE"
|
- name="$DOCKER_BASE"
|
||||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||||
- docker build -t "$name:current" .
|
- docker build -t "$name:current" .
|
||||||
- docker run "$name:current" --version
|
- docker run "$name:current" --version
|
||||||
|
- printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
|
||||||
|
- docker run -v "$PWD:/mnt" "$name:current" myscript
|
||||||
|
# Copy static executable from docker image
|
||||||
|
- id=$(docker create "$name:current")
|
||||||
|
- docker cp "$id:/bin/shellcheck" "shellcheck"
|
||||||
|
- docker rm "$id"
|
||||||
|
- ls -l shellcheck
|
||||||
|
- ./shellcheck myscript
|
||||||
|
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux"; 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 's/^FROM .*/FROM alpine:latest/' 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" --version
|
- docker run "$name:current" sh -c 'shellcheck --version'
|
||||||
|
# Windows .exe
|
||||||
|
- docker pull koalaman/winghc
|
||||||
|
- 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
|
||||||
|
- rm -rf dist || true
|
||||||
# Misc packaging
|
# Misc packaging
|
||||||
- ./.prepare_deploy
|
- ./.prepare_deploy
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||||
- for repo in $DOCKER_BUILDS;
|
- for repo in $DOCKER_BUILDS;
|
||||||
do
|
do
|
||||||
for tag in $TAGS;
|
for tag in $TAGS;
|
||||||
do
|
do
|
||||||
echo "Deploying $repo:current as $repo:$tag...";
|
echo "Deploying $repo:current as $repo:$tag...";
|
||||||
docker tag "$repo:current" "$repo:$tag";
|
docker tag "$repo:current" "$repo:$tag" || exit 1;
|
||||||
docker push "$repo:$tag";
|
docker push "$repo:$tag" || exit 1;
|
||||||
done;
|
done;
|
||||||
done;
|
done;
|
||||||
|
|
||||||
|
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,3 +1,30 @@
|
|||||||
|
## v0.5.0 - 2018-05-31
|
||||||
|
### Added
|
||||||
|
- SC2233/SC2234/SC2235: Suggest removing or replacing (..) around tests
|
||||||
|
- SC2232: Warn about invalid arguments to sudo
|
||||||
|
- SC2231: Suggest quoting expansions in for loop globs
|
||||||
|
- SC2229: Warn about 'read $var'
|
||||||
|
- SC2227: Warn about redirections in the middle of 'find' commands
|
||||||
|
- SC2224/SC2225/SC2226: Warn when using mv/cp/ln without a destination
|
||||||
|
- SC2223: Quote warning specific to `: ${var=value}`
|
||||||
|
- SC1131: Warn when using `elseif` or `elsif`
|
||||||
|
- SC1128: Warn about blanks/comments before shebang
|
||||||
|
- SC1127: Warn about C-style comments
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Annotations intended for a command's here documents now work
|
||||||
|
- Escaped characters inside groups in =~ regexes now parse
|
||||||
|
- Associative arrays are now respected in arithmetic contexts
|
||||||
|
- SC1087 about `$var[@]` now correctly triggers on any index
|
||||||
|
- Bad expansions in here documents are no longer ignored
|
||||||
|
- FD move operations like {fd}>1- now parse correctly
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Here docs are now terminated as per spec, rather than by presumed intent
|
||||||
|
- SC1073: 'else if' is now parsed correctly and not like 'elif'
|
||||||
|
- SC2163: 'export $name' can now be silenced with 'export ${name?}'
|
||||||
|
- SC2183: Now warns when printf arg count is not a multiple of format count
|
||||||
|
|
||||||
## v0.4.7 - 2017-12-08
|
## v0.4.7 - 2017-12-08
|
||||||
### Added
|
### Added
|
||||||
- Statically linked binaries for Linux and Windows (see README.md)!
|
- Statically linked binaries for Linux and Windows (see README.md)!
|
||||||
|
34
Dockerfile
34
Dockerfile
@@ -1,10 +1,36 @@
|
|||||||
FROM scratch
|
# Build-only image
|
||||||
|
FROM ubuntu:17.10 AS build
|
||||||
|
USER root
|
||||||
|
WORKDIR /opt/shellCheck
|
||||||
|
|
||||||
|
# Install OS deps
|
||||||
|
RUN apt-get update && apt-get install -y ghc cabal-install
|
||||||
|
|
||||||
|
# Install Haskell deps
|
||||||
|
# (This is a separate copy/run so that source changes don't require rebuilding)
|
||||||
|
COPY ShellCheck.cabal ./
|
||||||
|
RUN cabal update && cabal install --dependencies-only
|
||||||
|
|
||||||
|
# Copy source and build it
|
||||||
|
COPY LICENSE Setup.hs shellcheck.hs ./
|
||||||
|
COPY src src
|
||||||
|
RUN cabal build Paths_ShellCheck && \
|
||||||
|
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck && \
|
||||||
|
strip --strip-all shellcheck
|
||||||
|
|
||||||
|
RUN mkdir -p /out/bin && \
|
||||||
|
cp shellcheck /out/bin/
|
||||||
|
|
||||||
|
# Resulting Alpine image
|
||||||
|
FROM alpine:latest
|
||||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||||
|
COPY --from=build /out /
|
||||||
|
|
||||||
# This file assumes ShellCheck has already been built.
|
# DELETE-MARKER (Remove everything below to keep the alpine image)
|
||||||
# See https://github.com/koalaman/scbuilder
|
|
||||||
COPY shellcheck /bin/shellcheck
|
|
||||||
|
|
||||||
|
# Resulting ShellCheck image
|
||||||
|
FROM scratch
|
||||||
|
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||||
WORKDIR /mnt
|
WORKDIR /mnt
|
||||||
|
COPY --from=build /out /
|
||||||
ENTRYPOINT ["/bin/shellcheck"]
|
ENTRYPOINT ["/bin/shellcheck"]
|
||||||
|
8
LICENSE
8
LICENSE
@@ -1,7 +1,7 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
@@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
@@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
|
|||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
The GNU General Public License does not permit incorporating your program
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
may consider it more useful to permit linking proprietary applications with
|
may consider it more useful to permit linking proprietary applications with
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
|
83
README.md
83
README.md
@@ -1,3 +1,5 @@
|
|||||||
|
[](https://travis-ci.org/koalaman/shellcheck)
|
||||||
|
|
||||||
# ShellCheck - A shell script static analysis tool
|
# ShellCheck - A shell script static analysis tool
|
||||||
|
|
||||||
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:
|
||||||
@@ -25,7 +27,7 @@ See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for ex
|
|||||||
- [In your editor](#in-your-editor)
|
- [In your editor](#in-your-editor)
|
||||||
- [In your build or test suites](#in-your-build-or-test-suites)
|
- [In your build or test suites](#in-your-build-or-test-suites)
|
||||||
- [Installing](#installing)
|
- [Installing](#installing)
|
||||||
- [Travis CI Setup](#travis-ci-setup)
|
- [Travis CI](#travis-ci)
|
||||||
- [Compiling from source](#compiling-from-source)
|
- [Compiling from source](#compiling-from-source)
|
||||||
- [Installing Cabal](#installing-cabal)
|
- [Installing Cabal](#installing-cabal)
|
||||||
- [Compiling ShellCheck](#compiling-shellcheck)
|
- [Compiling ShellCheck](#compiling-shellcheck)
|
||||||
@@ -52,9 +54,9 @@ There are a number of ways to use ShellCheck!
|
|||||||
|
|
||||||
### On the web
|
### On the web
|
||||||
|
|
||||||
Paste a shell script on http://www.shellcheck.net for instant feedback.
|
Paste a shell script on https://www.shellcheck.net for instant feedback.
|
||||||
|
|
||||||
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
|
[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
|
||||||
|
|
||||||
### From your terminal
|
### From your terminal
|
||||||
|
|
||||||
@@ -108,6 +110,8 @@ On Arch Linux based distros:
|
|||||||
|
|
||||||
pacman -S shellcheck
|
pacman -S shellcheck
|
||||||
|
|
||||||
|
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
|
||||||
|
|
||||||
On Gentoo based distros:
|
On Gentoo based distros:
|
||||||
|
|
||||||
emerge --ask shellcheck
|
emerge --ask shellcheck
|
||||||
@@ -121,23 +125,16 @@ On Fedora based distros:
|
|||||||
|
|
||||||
dnf install ShellCheck
|
dnf install ShellCheck
|
||||||
|
|
||||||
|
On FreeBSD:
|
||||||
|
|
||||||
|
pkg install hs-ShellCheck
|
||||||
|
|
||||||
On OS X with homebrew:
|
On OS X with homebrew:
|
||||||
|
|
||||||
brew install shellcheck
|
brew install shellcheck
|
||||||
|
|
||||||
On OS X with MacPorts:
|
On openSUSE
|
||||||
|
|
||||||
port install shellcheck
|
|
||||||
|
|
||||||
On openSUSE:Tumbleweed:
|
|
||||||
|
|
||||||
zypper in ShellCheck
|
|
||||||
|
|
||||||
On other openSUSE distributions:
|
|
||||||
|
|
||||||
Add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
|
|
||||||
|
|
||||||
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
|
|
||||||
zypper in ShellCheck
|
zypper in ShellCheck
|
||||||
|
|
||||||
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||||
@@ -146,36 +143,47 @@ On Solus:
|
|||||||
|
|
||||||
eopkg install shellcheck
|
eopkg install shellcheck
|
||||||
|
|
||||||
|
On Windows (via [scoop](http://scoop.sh)):
|
||||||
|
|
||||||
|
scoop install shellcheck
|
||||||
|
|
||||||
|
From Snap Store:
|
||||||
|
|
||||||
|
snap install --channel=edge shellcheck
|
||||||
|
|
||||||
From Docker Hub:
|
From Docker Hub:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker pull koalaman/shellcheck:latest # Or :v0.4.6 for a release version
|
docker pull koalaman/shellcheck:stable # Or :v0.4.7 for that version, or :latest for daily builds
|
||||||
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
|
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
|
||||||
```
|
```
|
||||||
|
|
||||||
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend.
|
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
|
||||||
|
|
||||||
Alternatively, get freshly built binaries for the latest commit here:
|
Alternatively, you can download pre-compiled binaries for the latest release here:
|
||||||
|
|
||||||
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-latest.linux.x86_64.tar.xz) (statically linked)
|
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||||
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-latest.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 and release builds.
|
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
|
||||||
|
|
||||||
## Travis CI Setup
|
## Travis CI
|
||||||
|
|
||||||
If you want to use ShellCheck in Travis CI, you can most easily install it via `apt`:
|
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
|
||||||
|
|
||||||
```yml
|
If you still want to do so in order to upgrade at your leisure or ensure the latest release:
|
||||||
language: bash
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- debian-sid # Grab ShellCheck from the Debian repo
|
|
||||||
packages:
|
|
||||||
- shellcheck
|
|
||||||
```
|
|
||||||
|
|
||||||
|
install:
|
||||||
|
|
||||||
|
# Install a custom version of shellcheck instead of Travis CI's default
|
||||||
|
- 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"
|
||||||
|
- shellcheck() { "shellcheck-$scversion/shellcheck" "$@"; }
|
||||||
|
- shellcheck --version
|
||||||
|
|
||||||
|
script:
|
||||||
|
- shellcheck *.sh
|
||||||
|
|
||||||
## Compiling from source
|
## Compiling from source
|
||||||
|
|
||||||
@@ -293,6 +301,7 @@ alias archive='mv $1 /backup' # Defining aliases with arguments
|
|||||||
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
||||||
exec foo; echo "Done!" # Misused 'exec'
|
exec foo; echo "Done!" # Misused 'exec'
|
||||||
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
||||||
|
# find . -exec foo > bar \; # Redirections in find
|
||||||
f() { whoami; }; sudo f # External use of internal functions
|
f() { whoami; }; sudo f # External use of internal functions
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -308,9 +317,13 @@ var$n="Hello" # Wrong indirect assignment
|
|||||||
echo ${var$n} # Wrong indirect reference
|
echo ${var$n} # Wrong indirect reference
|
||||||
var=(1, 2, 3) # Comma separated arrays
|
var=(1, 2, 3) # Comma separated arrays
|
||||||
array=( [index] = value ) # Incorrect index initialization
|
array=( [index] = value ) # Incorrect index initialization
|
||||||
|
echo $var[14] # Missing {} in array references
|
||||||
echo "Argument 10 is $10" # Positional parameter misreference
|
echo "Argument 10 is $10" # Positional parameter misreference
|
||||||
if $(myfunction); then ..; fi # Wrapping commands in $()
|
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||||
else if othercondition; then .. # Using 'else if'
|
else if othercondition; then .. # Using 'else if'
|
||||||
|
f; f() { echo "hello world; } # Using function before definition
|
||||||
|
[ false ] # 'false' being true
|
||||||
|
if ( -f file ) # Using (..) instead of test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Style
|
### Style
|
||||||
@@ -341,6 +354,8 @@ printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
|
|||||||
var=World; echo "Hello " var # Unused lowercase variables
|
var=World; echo "Hello " var # Unused lowercase variables
|
||||||
echo "Hello $name" # Unassigned lowercase variables
|
echo "Hello $name" # Unassigned lowercase variables
|
||||||
cmd | read bar; echo $bar # Assignments in subshells
|
cmd | read bar; echo $bar # Assignments in subshells
|
||||||
|
cat foo | cp bar # Piping to commands that don't read
|
||||||
|
printf '%s: %s\n' foo # Mismatches in printf argument count
|
||||||
```
|
```
|
||||||
|
|
||||||
### Robustness
|
### Robustness
|
||||||
@@ -354,6 +369,7 @@ find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
|
|||||||
printf "Hello $name" # Variables in printf format
|
printf "Hello $name" # Variables in printf format
|
||||||
for f in $(ls *.txt); do # Iterating over ls output
|
for f in $(ls *.txt); do # Iterating over ls output
|
||||||
export MYVAR=$(cmd) # Masked exit codes
|
export MYVAR=$(cmd) # Masked exit codes
|
||||||
|
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
|
||||||
```
|
```
|
||||||
|
|
||||||
### Portability
|
### Portability
|
||||||
@@ -388,6 +404,7 @@ var=42 echo $var # Expansion of inlined environment
|
|||||||
echo $((n/180*100)) # Unnecessary loss of precision
|
echo $((n/180*100)) # Unnecessary loss of precision
|
||||||
ls *[:digit:].txt # Bad character class globs
|
ls *[:digit:].txt # Bad character class globs
|
||||||
sed 's/foo/bar/' file > file # Redirecting to input
|
sed 's/foo/bar/' file > file # Redirecting to input
|
||||||
|
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testimonials
|
## Testimonials
|
||||||
@@ -422,6 +439,6 @@ The contributor retains the copyright.
|
|||||||
|
|
||||||
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
|
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
|
||||||
|
|
||||||
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
|
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
|
||||||
|
|
||||||
Happy ShellChecking!
|
Happy ShellChecking!
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
Version: 0.4.7
|
Version: 0.5.0
|
||||||
Synopsis: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
License: GPL-3
|
License: GPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
Category: Static Analysis
|
Category: Static Analysis
|
||||||
Author: Vidar Holen
|
Author: Vidar Holen
|
||||||
Maintainer: vidar@vidarholen.net
|
Maintainer: vidar@vidarholen.net
|
||||||
Homepage: http://www.shellcheck.net/
|
Homepage: https://www.shellcheck.net/
|
||||||
Build-Type: Custom
|
Build-Type: Custom
|
||||||
Cabal-Version: >= 1.8
|
Cabal-Version: >= 1.8
|
||||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||||
@@ -31,16 +31,29 @@ Extra-Source-Files:
|
|||||||
-- tests
|
-- tests
|
||||||
test/shellcheck.hs
|
test/shellcheck.hs
|
||||||
|
|
||||||
|
custom-setup
|
||||||
|
setup-depends:
|
||||||
|
base >= 4 && <5,
|
||||||
|
process >= 1.0 && <1.7,
|
||||||
|
Cabal >= 1.10 && <2.3
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
location: git://github.com/koalaman/shellcheck.git
|
location: git://github.com/koalaman/shellcheck.git
|
||||||
|
|
||||||
library
|
library
|
||||||
|
hs-source-dirs: src
|
||||||
|
if impl(ghc < 8.0)
|
||||||
build-depends:
|
build-depends:
|
||||||
base >= 4 && < 5,
|
semigroups
|
||||||
containers,
|
build-depends:
|
||||||
|
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
|
||||||
|
-- Just disable that version entirely to fail fast.
|
||||||
|
aeson,
|
||||||
|
base > 4.6.0.1 && < 5,
|
||||||
|
bytestring,
|
||||||
|
containers >= 0.5,
|
||||||
directory,
|
directory,
|
||||||
json,
|
|
||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec,
|
||||||
regex-tdfa,
|
regex-tdfa,
|
||||||
@@ -69,27 +82,34 @@ library
|
|||||||
Paths_ShellCheck
|
Paths_ShellCheck
|
||||||
|
|
||||||
executable shellcheck
|
executable shellcheck
|
||||||
|
if impl(ghc < 8.0)
|
||||||
build-depends:
|
build-depends:
|
||||||
|
semigroups
|
||||||
|
build-depends:
|
||||||
|
aeson,
|
||||||
base >= 4 && < 5,
|
base >= 4 && < 5,
|
||||||
|
bytestring,
|
||||||
|
ShellCheck,
|
||||||
containers,
|
containers,
|
||||||
directory,
|
directory,
|
||||||
json,
|
|
||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec >= 3.0,
|
||||||
regex-tdfa,
|
QuickCheck >= 2.7.4,
|
||||||
QuickCheck >= 2.7.4
|
regex-tdfa
|
||||||
main-is: shellcheck.hs
|
main-is: shellcheck.hs
|
||||||
|
|
||||||
test-suite test-shellcheck
|
test-suite test-shellcheck
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
build-depends:
|
build-depends:
|
||||||
|
aeson,
|
||||||
base >= 4 && < 5,
|
base >= 4 && < 5,
|
||||||
|
bytestring,
|
||||||
|
ShellCheck,
|
||||||
containers,
|
containers,
|
||||||
directory,
|
directory,
|
||||||
json,
|
|
||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec,
|
||||||
regex-tdfa,
|
QuickCheck >= 2.7.4,
|
||||||
QuickCheck >= 2.7.4
|
regex-tdfa
|
||||||
main-is: test/shellcheck.hs
|
main-is: test/shellcheck.hs
|
||||||
|
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# TODO: Find a less trashy way to get the next available error code
|
# TODO: Find a less trashy way to get the next available error code
|
||||||
|
if ! shopt -s globstar
|
||||||
shopt -s globstar
|
then
|
||||||
|
echo "Error: This script depends on Bash 4." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
for i in 1 2
|
for i in 1 2
|
||||||
do
|
do
|
||||||
|
2
quickrun
2
quickrun
@@ -2,4 +2,4 @@
|
|||||||
# quickrun runs ShellCheck in an interpreted mode.
|
# quickrun runs ShellCheck in an interpreted mode.
|
||||||
# This allows testing changes without recompiling.
|
# This allows testing changes without recompiling.
|
||||||
|
|
||||||
runghc -idist/build/autogen shellcheck.hs "$@"
|
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
|
||||||
|
@@ -207,7 +207,7 @@ https://github.com/koalaman/shellcheck/issues
|
|||||||
# COPYRIGHT
|
# COPYRIGHT
|
||||||
Copyright 2012-2015, Vidar Holen.
|
Copyright 2012-2015, Vidar Holen.
|
||||||
Licensed under the GNU General Public License version 3 or later,
|
Licensed under the GNU General Public License version 3 or later,
|
||||||
see http://gnu.org/licenses/gpl.html
|
see https://gnu.org/licenses/gpl.html
|
||||||
|
|
||||||
|
|
||||||
# SEE ALSO
|
# SEE ALSO
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,15 +15,15 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
import ShellCheck.Data
|
|
||||||
import ShellCheck.Checker
|
import ShellCheck.Checker
|
||||||
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import ShellCheck.Formatter.Format
|
|
||||||
import qualified ShellCheck.Formatter.CheckStyle
|
import qualified ShellCheck.Formatter.CheckStyle
|
||||||
|
import ShellCheck.Formatter.Format
|
||||||
import qualified ShellCheck.Formatter.GCC
|
import qualified ShellCheck.Formatter.GCC
|
||||||
import qualified ShellCheck.Formatter.JSON
|
import qualified ShellCheck.Formatter.JSON
|
||||||
import qualified ShellCheck.Formatter.TTY
|
import qualified ShellCheck.Formatter.TTY
|
||||||
@@ -40,6 +40,7 @@ import Data.List
|
|||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
import Data.Semigroup (Semigroup (..))
|
||||||
import Prelude hiding (catch)
|
import Prelude hiding (catch)
|
||||||
import System.Console.GetOpt
|
import System.Console.GetOpt
|
||||||
import System.Directory
|
import System.Directory
|
||||||
@@ -56,9 +57,12 @@ data Status =
|
|||||||
| RuntimeException
|
| RuntimeException
|
||||||
deriving (Ord, Eq, Show)
|
deriving (Ord, Eq, Show)
|
||||||
|
|
||||||
|
instance Semigroup Status where
|
||||||
|
(<>) = max
|
||||||
|
|
||||||
instance Monoid Status where
|
instance Monoid Status where
|
||||||
mempty = NoProblems
|
mempty = NoProblems
|
||||||
mappend = max
|
mappend = (Data.Semigroup.<>)
|
||||||
|
|
||||||
data Options = Options {
|
data Options = Options {
|
||||||
checkSpec :: CheckSpec,
|
checkSpec :: CheckSpec,
|
||||||
@@ -203,7 +207,7 @@ runFormatter sys format options files = do
|
|||||||
|
|
||||||
process :: FilePath -> IO Status
|
process :: FilePath -> IO Status
|
||||||
process filename = do
|
process filename = do
|
||||||
input <- (siReadFile sys) filename
|
input <- siReadFile sys filename
|
||||||
either (reportFailure filename) check input
|
either (reportFailure filename) check input
|
||||||
where
|
where
|
||||||
check contents = do
|
check contents = do
|
||||||
@@ -378,4 +382,4 @@ printVersion = do
|
|||||||
putStrLn "ShellCheck - shell script analysis tool"
|
putStrLn "ShellCheck - shell script analysis tool"
|
||||||
putStrLn $ "version: " ++ shellcheckVersion
|
putStrLn $ "version: " ++ shellcheckVersion
|
||||||
putStrLn "license: GNU General Public License, version 3"
|
putStrLn "license: GNU General Public License, version 3"
|
||||||
putStrLn "website: http://www.shellcheck.net"
|
putStrLn "website: https://www.shellcheck.net"
|
||||||
|
46
snap/snapcraft.yaml
Normal file
46
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: shellcheck
|
||||||
|
summary: A shell script static analysis tool
|
||||||
|
description: |
|
||||||
|
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh
|
||||||
|
shell scripts.
|
||||||
|
|
||||||
|
The goals of ShellCheck are
|
||||||
|
|
||||||
|
- To point out and clarify typical beginner's syntax issues that cause a
|
||||||
|
shell to give cryptic error messages.
|
||||||
|
|
||||||
|
- To point out and clarify typical intermediate level semantic problems that
|
||||||
|
cause a shell to behave strangely and counter-intuitively.
|
||||||
|
|
||||||
|
- To point out subtle caveats, corner cases and pitfalls that may cause an
|
||||||
|
advanced user's otherwise working script to fail under future
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
By default ShellCheck can only check non-hidden files under /home, to make
|
||||||
|
ShellCheck be able to check files under /media and /run/media you must
|
||||||
|
connect it to the `removable-media` interface manually:
|
||||||
|
|
||||||
|
# snap connect shellcheck:removable-media
|
||||||
|
|
||||||
|
version: git
|
||||||
|
grade: devel
|
||||||
|
confinement: strict
|
||||||
|
|
||||||
|
apps:
|
||||||
|
shellcheck:
|
||||||
|
command: usr/bin/shellcheck
|
||||||
|
plugs: [home, removable-media]
|
||||||
|
|
||||||
|
parts:
|
||||||
|
shellcheck:
|
||||||
|
plugin: dump
|
||||||
|
source: ./
|
||||||
|
build-packages:
|
||||||
|
- cabal-install
|
||||||
|
build: |
|
||||||
|
cabal sandbox init
|
||||||
|
cabal update
|
||||||
|
cabal install -j
|
||||||
|
install: |
|
||||||
|
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
||||||
|
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.AST where
|
module ShellCheck.AST where
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ newtype Root = Root Token
|
|||||||
data Token =
|
data Token =
|
||||||
TA_Binary Id String Token Token
|
TA_Binary Id String Token Token
|
||||||
| TA_Assignment Id String Token Token
|
| TA_Assignment Id String Token Token
|
||||||
|
| TA_Variable Id String [Token]
|
||||||
| TA_Expansion Id [Token]
|
| TA_Expansion Id [Token]
|
||||||
| TA_Index Id Token
|
|
||||||
| TA_Sequence Id [Token]
|
| TA_Sequence Id [Token]
|
||||||
| TA_Trinary Id Token Token Token
|
| TA_Trinary Id Token Token Token
|
||||||
| TA_Unary Id String Token
|
| TA_Unary Id String Token
|
||||||
@@ -134,7 +134,8 @@ data Token =
|
|||||||
| T_Pipe Id String
|
| T_Pipe Id String
|
||||||
| T_CoProc Id (Maybe String) Token
|
| T_CoProc Id (Maybe String) Token
|
||||||
| T_CoProcBody Id Token
|
| T_CoProcBody Id Token
|
||||||
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
|
| T_Include Id Token
|
||||||
|
| T_SourceCommand Id Token Token
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data Annotation =
|
data Annotation =
|
||||||
@@ -266,11 +267,12 @@ analyze f g i =
|
|||||||
c <- round t3
|
c <- round t3
|
||||||
return $ TA_Trinary id a b c
|
return $ TA_Trinary id a b c
|
||||||
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
delve (TA_Expansion id t) = dl t $ TA_Expansion id
|
||||||
delve (TA_Index id t) = d1 t $ TA_Index id
|
delve (TA_Variable id str t) = dl t $ TA_Variable id str
|
||||||
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
delve (T_Annotation id anns t) = d1 t $ T_Annotation id anns
|
||||||
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
delve (T_CoProc id var body) = d1 body $ T_CoProc id var
|
||||||
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
|
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
|
||||||
delve (T_Include id includer script) = d2 includer script $ T_Include id
|
delve (T_Include id script) = d1 script $ T_Include id
|
||||||
|
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
|
||||||
delve t = return t
|
delve t = return t
|
||||||
|
|
||||||
getId :: Token -> Id
|
getId :: Token -> Id
|
||||||
@@ -360,7 +362,6 @@ getId t = case t of
|
|||||||
TA_Sequence id _ -> id
|
TA_Sequence id _ -> id
|
||||||
TA_Trinary id _ _ _ -> id
|
TA_Trinary id _ _ _ -> id
|
||||||
TA_Expansion id _ -> id
|
TA_Expansion id _ -> id
|
||||||
TA_Index id _ -> id
|
|
||||||
T_ProcSub id _ _ -> id
|
T_ProcSub id _ _ -> id
|
||||||
T_Glob id _ -> id
|
T_Glob id _ -> id
|
||||||
T_ForArithmetic id _ _ _ _ -> id
|
T_ForArithmetic id _ _ _ _ -> id
|
||||||
@@ -371,9 +372,11 @@ getId t = case t of
|
|||||||
T_Pipe id _ -> id
|
T_Pipe id _ -> id
|
||||||
T_CoProc id _ _ -> id
|
T_CoProc id _ _ -> id
|
||||||
T_CoProcBody id _ -> id
|
T_CoProcBody id _ -> id
|
||||||
T_Include id _ _ -> id
|
T_Include id _ -> id
|
||||||
|
T_SourceCommand id _ _ -> id
|
||||||
T_UnparsedIndex id _ _ -> id
|
T_UnparsedIndex id _ _ -> id
|
||||||
TC_Empty id _ -> id
|
TC_Empty id _ -> id
|
||||||
|
TA_Variable id _ _ -> id
|
||||||
|
|
||||||
blank :: Monad m => Token -> m ()
|
blank :: Monad m => Token -> m ()
|
||||||
blank = const $ return ()
|
blank = const $ return ()
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.ASTLib where
|
module ShellCheck.ASTLib where
|
||||||
|
|
||||||
@@ -112,6 +112,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
|||||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||||
|
|
||||||
-- Get all flags in a GNU way, up until --
|
-- Get all flags in a GNU way, up until --
|
||||||
|
getAllFlags :: Token -> [(Token, String)]
|
||||||
getAllFlags = getFlagsUntil (== "--")
|
getAllFlags = getFlagsUntil (== "--")
|
||||||
-- Get all flags in a BSD way, up until first non-flag argument or --
|
-- Get all flags in a BSD way, up until first non-flag argument or --
|
||||||
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
||||||
@@ -260,7 +261,7 @@ getCommand t =
|
|||||||
getCommandName t = do
|
getCommandName t = do
|
||||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||||
s <- getLiteralString w
|
s <- getLiteralString w
|
||||||
if "busybox" `isSuffixOf` s
|
if "busybox" `isSuffixOf` s || "builtin" == s
|
||||||
then
|
then
|
||||||
case rest of
|
case rest of
|
||||||
(applet:_) -> getLiteralString applet
|
(applet:_) -> getLiteralString applet
|
||||||
@@ -434,3 +435,12 @@ pseudoGlobIsSuperSetof = matchable
|
|||||||
|
|
||||||
wordsCanBeEqual x y = fromMaybe True $
|
wordsCanBeEqual x y = fromMaybe True $
|
||||||
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||||
|
|
||||||
|
-- Is this an expansion that can be quoted,
|
||||||
|
-- e.g. $(foo) `foo` $foo (but not {foo,})?
|
||||||
|
isQuoteableExpansion t = case t of
|
||||||
|
T_DollarExpansion {} -> True
|
||||||
|
T_DollarBraceCommandExpansion {} -> True
|
||||||
|
T_Backticked {} -> True
|
||||||
|
T_DollarBraced {} -> True
|
||||||
|
_ -> False
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
module ShellCheck.Analytics (runAnalytics, ShellCheck.Analytics.runTests) where
|
module ShellCheck.Analytics (runAnalytics, ShellCheck.Analytics.runTests) where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
@@ -119,7 +120,6 @@ nodeChecks = [
|
|||||||
,checkGlobbedRegex
|
,checkGlobbedRegex
|
||||||
,checkTestRedirects
|
,checkTestRedirects
|
||||||
,checkIndirectExpansion
|
,checkIndirectExpansion
|
||||||
,checkSudoRedirect
|
|
||||||
,checkPS1Assignments
|
,checkPS1Assignments
|
||||||
,checkBackticks
|
,checkBackticks
|
||||||
,checkInexplicablyUnquoted
|
,checkInexplicablyUnquoted
|
||||||
@@ -166,6 +166,8 @@ nodeChecks = [
|
|||||||
,checkFlagAsCommand
|
,checkFlagAsCommand
|
||||||
,checkEmptyCondition
|
,checkEmptyCondition
|
||||||
,checkPipeToNowhere
|
,checkPipeToNowhere
|
||||||
|
,checkForLoopGlobVariables
|
||||||
|
,checkSubshelledTests
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -572,6 +574,7 @@ prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/
|
|||||||
prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
|
prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
|
||||||
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
|
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
|
||||||
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
|
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
|
||||||
|
prop_checkRedirectToSame8 = verifyNot checkRedirectToSame "while read -r line; do _=\"$fname\"; done <\"$fname\""
|
||||||
checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||||
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
|
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
|
||||||
where
|
where
|
||||||
@@ -582,7 +585,8 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
|||||||
&& x == y
|
&& x == y
|
||||||
&& not (isOutput t && isOutput u)
|
&& not (isOutput t && isOutput u)
|
||||||
&& not (special t)
|
&& not (special t)
|
||||||
&& not (any isHarmlessCommand [t,u])) $ do
|
&& not (any isHarmlessCommand [t,u])
|
||||||
|
&& not (any containsAssignment [u])) $ do
|
||||||
addComment $ note newId
|
addComment $ note newId
|
||||||
addComment $ note exceptId
|
addComment $ note exceptId
|
||||||
checkOccurrences _ _ = return ()
|
checkOccurrences _ _ = return ()
|
||||||
@@ -609,6 +613,9 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
|||||||
cmd <- getClosestCommand (parentMap params) arg
|
cmd <- getClosestCommand (parentMap params) arg
|
||||||
name <- getCommandBasename cmd
|
name <- getCommandBasename cmd
|
||||||
return $ name `elem` ["echo", "printf", "sponge"]
|
return $ name `elem` ["echo", "printf", "sponge"]
|
||||||
|
containsAssignment arg = fromMaybe False $ do
|
||||||
|
cmd <- getClosestCommand (parentMap params) arg
|
||||||
|
return $ isAssignment cmd
|
||||||
|
|
||||||
checkRedirectToSame _ _ = return ()
|
checkRedirectToSame _ _ = return ()
|
||||||
|
|
||||||
@@ -742,6 +749,7 @@ prop_checkStderrRedirect3 = verifyNot checkStderrRedirect "test 2>&1 > file | gr
|
|||||||
prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)"
|
prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 > file)"
|
||||||
prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
|
prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
|
||||||
prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
|
prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
|
||||||
|
prop_checkStderrRedirect7 = verifyNot checkStderrRedirect "{ cmd > file; } 2>&1"
|
||||||
checkStderrRedirect params redir@(T_Redirecting _ [
|
checkStderrRedirect params redir@(T_Redirecting _ [
|
||||||
T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"),
|
T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"),
|
||||||
T_FdRedirect _ _ (T_IoFile _ op _)
|
T_FdRedirect _ _ (T_IoFile _ op _)
|
||||||
@@ -760,7 +768,7 @@ checkStderrRedirect params redir@(T_Redirecting _ [
|
|||||||
isCaptured = any usesOutput $ getPath (parentMap params) redir
|
isCaptured = any usesOutput $ getPath (parentMap params) redir
|
||||||
|
|
||||||
error = unless isCaptured $
|
error = unless isCaptured $
|
||||||
err id 2069 "The order of the 2>&1 and the redirect matters. The 2>&1 has to be last."
|
warn id 2069 "To redirect stdout+stderr, 2>&1 must be last (or use '{ cmd > file; } 2>&1' to clarify)."
|
||||||
|
|
||||||
checkStderrRedirect _ _ = return ()
|
checkStderrRedirect _ _ = return ()
|
||||||
|
|
||||||
@@ -786,6 +794,10 @@ prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${
|
|||||||
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
|
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
|
||||||
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
|
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
|
||||||
prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"
|
prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"
|
||||||
|
prop_checkSingleQuotedVariables15= verifyNot checkSingleQuotedVariables "git filter-branch 'test $GIT_COMMIT'"
|
||||||
|
prop_checkSingleQuotedVariables16= verify checkSingleQuotedVariables "git '$a'"
|
||||||
|
prop_checkSingleQuotedVariables17= verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *"
|
||||||
|
|
||||||
checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||||
when (s `matches` re) $
|
when (s `matches` re) $
|
||||||
if "sed" == commandName
|
if "sed" == commandName
|
||||||
@@ -798,7 +810,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
|||||||
commandName = fromMaybe "" $ do
|
commandName = fromMaybe "" $ do
|
||||||
cmd <- getClosestCommand parents t
|
cmd <- getClosestCommand parents t
|
||||||
name <- getCommandBasename cmd
|
name <- getCommandBasename cmd
|
||||||
return $ if name == "find" then getFindCommand cmd else name
|
return $ if name == "find" then getFindCommand cmd else if name == "git" then getGitCommand cmd else name
|
||||||
|
|
||||||
isProbablyOk =
|
isProbablyOk =
|
||||||
any isOkAssignment (take 3 $ getPath parents t)
|
any isOkAssignment (take 3 $ getPath parents t)
|
||||||
@@ -813,8 +825,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
|||||||
,"xprop"
|
,"xprop"
|
||||||
,"alias"
|
,"alias"
|
||||||
,"sudo" -- covering "sudo sh" and such
|
,"sudo" -- covering "sudo sh" and such
|
||||||
|
,"docker" -- like above
|
||||||
,"dpkg-query"
|
,"dpkg-query"
|
||||||
,"jq" -- could also check that user provides --arg
|
,"jq" -- could also check that user provides --arg
|
||||||
|
,"rename"
|
||||||
|
,"unset"
|
||||||
|
,"git filter-branch"
|
||||||
]
|
]
|
||||||
|| "awk" `isSuffixOf` commandName
|
|| "awk" `isSuffixOf` commandName
|
||||||
|| "perl" `isPrefixOf` commandName
|
|| "perl" `isPrefixOf` commandName
|
||||||
@@ -838,6 +854,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
|||||||
_ -> "find"
|
_ -> "find"
|
||||||
getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
|
getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
|
||||||
getFindCommand _ = "find"
|
getFindCommand _ = "find"
|
||||||
|
getGitCommand (T_SimpleCommand _ _ words) =
|
||||||
|
case map getLiteralString words of
|
||||||
|
Just "git":Just "filter-branch":_ -> "git filter-branch"
|
||||||
|
_ -> "git"
|
||||||
|
getGitCommand (T_Redirecting _ _ cmd) = getGitCommand cmd
|
||||||
|
getGitCommand _ = "git"
|
||||||
checkSingleQuotedVariables _ _ = return ()
|
checkSingleQuotedVariables _ _ = return ()
|
||||||
|
|
||||||
|
|
||||||
@@ -845,6 +867,7 @@ prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
|
|||||||
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
|
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
|
||||||
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
|
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
|
||||||
prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
|
prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
|
||||||
|
prop_checkUnquotedN5 = verifyNot checkUnquotedN "[ -n \"$@\" ]"
|
||||||
checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
|
checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
|
||||||
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
|
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
|
||||||
checkUnquotedN _ _ = return ()
|
checkUnquotedN _ _ = return ()
|
||||||
@@ -1140,12 +1163,10 @@ checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
|
|||||||
T_Arithmetic {} -> return normalWarning
|
T_Arithmetic {} -> return normalWarning
|
||||||
T_DollarArithmetic {} -> return normalWarning
|
T_DollarArithmetic {} -> return normalWarning
|
||||||
T_ForArithmetic {} -> return normalWarning
|
T_ForArithmetic {} -> return normalWarning
|
||||||
TA_Index {} -> return indexWarning
|
|
||||||
T_SimpleCommand {} -> return noWarning
|
T_SimpleCommand {} -> return noWarning
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
|
normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
|
||||||
indexWarning = style id 2149 "Remove $/${} for numeric index, or escape it for string."
|
|
||||||
noWarning = return ()
|
noWarning = return ()
|
||||||
checkArithmeticDeref _ _ = return ()
|
checkArithmeticDeref _ _ = return ()
|
||||||
|
|
||||||
@@ -1276,33 +1297,6 @@ checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
|
|||||||
_ -> False
|
_ -> False
|
||||||
checkTestRedirects _ _ = return ()
|
checkTestRedirects _ _ = return ()
|
||||||
|
|
||||||
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
|
|
||||||
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
|
||||||
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
|
|
||||||
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
|
|
||||||
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
|
|
||||||
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
|
|
||||||
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
|
|
||||||
checkSudoRedirect _ (T_Redirecting _ redirs cmd) | cmd `isCommand` "sudo" =
|
|
||||||
mapM_ warnAbout redirs
|
|
||||||
where
|
|
||||||
warnAbout (T_FdRedirect _ s (T_IoFile id op file))
|
|
||||||
| (s == "" || s == "&") && not (special file) =
|
|
||||||
case op of
|
|
||||||
T_Less _ ->
|
|
||||||
info (getId op) 2024
|
|
||||||
"sudo doesn't affect redirects. Use sudo cat file | .."
|
|
||||||
T_Greater _ ->
|
|
||||||
warn (getId op) 2024
|
|
||||||
"sudo doesn't affect redirects. Use ..| sudo tee file"
|
|
||||||
T_DGREAT _ ->
|
|
||||||
warn (getId op) 2024
|
|
||||||
"sudo doesn't affect redirects. Use .. | sudo tee -a file"
|
|
||||||
_ -> return ()
|
|
||||||
warnAbout _ = return ()
|
|
||||||
special file = concat (oversimplify file) == "/dev/null"
|
|
||||||
checkSudoRedirect _ _ = return ()
|
|
||||||
|
|
||||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||||
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||||
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
||||||
@@ -1426,6 +1420,7 @@ prop_checkSpuriousExec4 = verifyNot checkSpuriousExec "if a; then exec b; fi"
|
|||||||
prop_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd"
|
prop_checkSpuriousExec5 = verifyNot checkSpuriousExec "exec > file; cmd"
|
||||||
prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
|
prop_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
|
||||||
prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
|
prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
|
||||||
|
prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.log 2>&1; bar"
|
||||||
checkSpuriousExec _ = doLists
|
checkSpuriousExec _ = doLists
|
||||||
where
|
where
|
||||||
doLists (T_Script _ _ cmds) = doList cmds
|
doLists (T_Script _ _ cmds) = doList cmds
|
||||||
@@ -1617,16 +1612,23 @@ checkSpacefulness params t =
|
|||||||
modify $ Map.insert name bool
|
modify $ Map.insert name bool
|
||||||
|
|
||||||
readF _ token name = do
|
readF _ token name = do
|
||||||
spaced <- hasSpaces name
|
spaces <- hasSpaces name
|
||||||
return [makeComment InfoC (getId token) 2086 warning |
|
return [warning |
|
||||||
isExpansion token && spaced
|
isExpansion token && spaces
|
||||||
&& not (isArrayExpansion token) -- There's another warning for this
|
&& not (isArrayExpansion token) -- There's another warning for this
|
||||||
&& not (isCountingReference token)
|
&& not (isCountingReference token)
|
||||||
&& not (isQuoteFree parents token)
|
&& not (isQuoteFree parents token)
|
||||||
&& not (isQuotedAlternativeReference token)
|
&& not (isQuotedAlternativeReference token)
|
||||||
&& not (usedAsCommandName parents token)]
|
&& not (usedAsCommandName parents token)]
|
||||||
where
|
where
|
||||||
warning = "Double quote to prevent globbing and word splitting."
|
warning =
|
||||||
|
if isDefaultAssignment (parentMap params) token
|
||||||
|
then
|
||||||
|
makeComment InfoC (getId token) 2223
|
||||||
|
"This default assignment may cause DoS due to globbing. Quote it."
|
||||||
|
else
|
||||||
|
makeComment InfoC (getId token) 2086
|
||||||
|
"Double quote to prevent globbing and word splitting."
|
||||||
|
|
||||||
writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
|
writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
|
||||||
writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
|
writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
|
||||||
@@ -1665,6 +1667,12 @@ checkSpacefulness params t =
|
|||||||
globspace = "*?[] \t\n"
|
globspace = "*?[] \t\n"
|
||||||
containsAny s = any (`elem` s)
|
containsAny s = any (`elem` s)
|
||||||
|
|
||||||
|
isDefaultAssignment parents token =
|
||||||
|
let modifier = getBracedModifier $ bracedString token in
|
||||||
|
isExpansion token
|
||||||
|
&& any (`isPrefixOf` modifier) ["=", ":="]
|
||||||
|
&& isParamTo parents ":" token
|
||||||
|
|
||||||
prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
|
prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"
|
||||||
prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
|
prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"
|
||||||
prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\""
|
prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\""
|
||||||
@@ -1811,6 +1819,9 @@ prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo ));
|
|||||||
prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"
|
prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"
|
||||||
prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"
|
prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"
|
||||||
prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"
|
prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"
|
||||||
|
prop_checkUnused38= verifyTree checkUnusedAssignments "(( a=42 ))"
|
||||||
|
prop_checkUnused39= verifyNotTree checkUnusedAssignments "declare -x -f foo"
|
||||||
|
prop_checkUnused40= verifyNotTree checkUnusedAssignments "arr=(1 2); num=2; echo \"${arr[@]:num}\""
|
||||||
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||||
where
|
where
|
||||||
flow = variableFlow params
|
flow = variableFlow params
|
||||||
@@ -1828,7 +1839,7 @@ checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
|||||||
|
|
||||||
warnFor (name, token) =
|
warnFor (name, token) =
|
||||||
warn (getId token) 2034 $
|
warn (getId token) 2034 $
|
||||||
name ++ " appears unused. Verify it or export it."
|
name ++ " appears unused. Verify use (or export if used externally)."
|
||||||
|
|
||||||
stripSuffix = takeWhile isVariableChar
|
stripSuffix = takeWhile isVariableChar
|
||||||
defaultMap = Map.fromList $ zip internalVariables $ repeat ()
|
defaultMap = Map.fromList $ zip internalVariables $ repeat ()
|
||||||
@@ -1865,6 +1876,9 @@ prop_checkUnassignedReferences29= verifyNotTree checkUnassignedReferences "if [[
|
|||||||
prop_checkUnassignedReferences30= verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi"
|
prop_checkUnassignedReferences30= verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi"
|
||||||
prop_checkUnassignedReferences31= verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"
|
prop_checkUnassignedReferences31= verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"
|
||||||
prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi"
|
prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi"
|
||||||
|
prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"
|
||||||
|
prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"
|
||||||
|
prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"
|
||||||
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)
|
||||||
@@ -1924,11 +1938,13 @@ checkUnassignedReferences params t = warnings
|
|||||||
isArray _ = False
|
isArray _ = False
|
||||||
|
|
||||||
isGuarded (T_DollarBraced _ v) =
|
isGuarded (T_DollarBraced _ v) =
|
||||||
any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]
|
rest `matches` guardRegex
|
||||||
where
|
where
|
||||||
name = concat $ oversimplify v
|
name = concat $ oversimplify v
|
||||||
rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
|
rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
|
||||||
isGuarded _ = False
|
isGuarded _ = False
|
||||||
|
-- :? or :- with optional array index and colon
|
||||||
|
guardRegex = mkRegex "^(\\[.*\\])?:?[-?]"
|
||||||
|
|
||||||
match var candidate =
|
match var candidate =
|
||||||
if var /= candidate && map toLower var == map toLower candidate
|
if var /= candidate && map toLower var == map toLower candidate
|
||||||
@@ -2467,6 +2483,7 @@ prop_checkUncheckedCd5 = verifyTree checkUncheckedCdPushdPopd "if true; then cd
|
|||||||
prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .."
|
prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .."
|
||||||
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"
|
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"
|
||||||
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm bar"
|
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm bar"
|
||||||
|
prop_checkUncheckedCd9 = verifyTree checkUncheckedCdPushdPopd "builtin cd ~/src; rm -r foo"
|
||||||
prop_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo"
|
prop_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo"
|
||||||
prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"
|
prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"
|
||||||
prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; rm -r foo"
|
prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; rm -r foo"
|
||||||
@@ -2525,7 +2542,7 @@ checkLoopVariableReassignment params token =
|
|||||||
T_ForArithmetic _
|
T_ForArithmetic _
|
||||||
(TA_Sequence _
|
(TA_Sequence _
|
||||||
[TA_Assignment _ "="
|
[TA_Assignment _ "="
|
||||||
(TA_Expansion _ [T_Literal _ var]) _])
|
(TA_Variable _ var _ ) _])
|
||||||
_ _ _ -> return var
|
_ _ _ -> return var
|
||||||
_ -> fail "not loop"
|
_ -> fail "not loop"
|
||||||
|
|
||||||
@@ -2819,6 +2836,7 @@ prop_checkPipeToNowhere4 = verify checkPipeToNowhere "printf 'Lol' << eof\nlol\n
|
|||||||
prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du"
|
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"
|
||||||
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
||||||
checkPipeToNowhere _ t =
|
checkPipeToNowhere _ t =
|
||||||
case t of
|
case t of
|
||||||
@@ -2891,5 +2909,77 @@ checkUseBeforeDefinition _ t =
|
|||||||
then [x]
|
then [x]
|
||||||
else concatMap recursiveSequences list
|
else concatMap recursiveSequences list
|
||||||
|
|
||||||
|
prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done"
|
||||||
|
prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done"
|
||||||
|
prop_checkForLoopGlobVariables3 = verifyNot checkForLoopGlobVariables "for i in $var; do true; done"
|
||||||
|
checkForLoopGlobVariables _ t =
|
||||||
|
case t of
|
||||||
|
T_ForIn _ _ words _ -> mapM_ check words
|
||||||
|
_ -> return ()
|
||||||
|
where
|
||||||
|
check (T_NormalWord _ parts) =
|
||||||
|
when (any isGlob parts) $
|
||||||
|
mapM_ suggest $ filter isQuoteableExpansion parts
|
||||||
|
suggest t = info (getId t) 2231
|
||||||
|
"Quote expansions in this for loop glob to prevent wordsplitting, e.g. \"$dir\"/*.txt ."
|
||||||
|
|
||||||
|
prop_checkSubshelledTests1 = verify checkSubshelledTests "a && ( [ b ] || ! [ c ] )"
|
||||||
|
prop_checkSubshelledTests2 = verify checkSubshelledTests "( [ a ] )"
|
||||||
|
prop_checkSubshelledTests3 = verify checkSubshelledTests "( [ a ] && [ b ] || test c )"
|
||||||
|
checkSubshelledTests params t =
|
||||||
|
case t of
|
||||||
|
T_Subshell id list | isSubshelledTest t ->
|
||||||
|
case () of
|
||||||
|
-- Special case for if (test) and while (test)
|
||||||
|
_ | isCompoundCondition (getPath (parentMap params) t) ->
|
||||||
|
style id 2233 "Remove superfluous (..) around condition."
|
||||||
|
|
||||||
|
-- Special case for ([ x ])
|
||||||
|
_ | isSingleTest list ->
|
||||||
|
style id 2234 "Remove superfluous (..) around test command."
|
||||||
|
|
||||||
|
-- General case for ([ x ] || [ y ] && etc)
|
||||||
|
_ -> style id 2235 "Use { ..; } instead of (..) to avoid subshell overhead."
|
||||||
|
_ -> return ()
|
||||||
|
where
|
||||||
|
|
||||||
|
isSingleTest cmds =
|
||||||
|
case cmds of
|
||||||
|
[c] | isTestCommand c -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
isSubshelledTest t =
|
||||||
|
case t of
|
||||||
|
T_Subshell _ list -> all isSubshelledTest list
|
||||||
|
T_AndIf _ a b -> isSubshelledTest a && isSubshelledTest b
|
||||||
|
T_OrIf _ a b -> isSubshelledTest a && isSubshelledTest b
|
||||||
|
T_Annotation _ _ t -> isSubshelledTest t
|
||||||
|
_ -> isTestCommand t
|
||||||
|
|
||||||
|
isTestCommand t =
|
||||||
|
case t of
|
||||||
|
T_Banged _ t -> isTestCommand t
|
||||||
|
T_Pipeline _ [] [T_Redirecting _ _ T_Condition {}] -> True
|
||||||
|
T_Pipeline _ [] [T_Redirecting _ _ cmd] -> cmd `isCommand` "test"
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Check if a T_Subshell is used as a condition, e.g. if ( test )
|
||||||
|
-- This technically also triggers for `if true; then ( test ); fi`
|
||||||
|
-- but it's still a valid suggestion.
|
||||||
|
isCompoundCondition chain =
|
||||||
|
case dropWhile skippable (drop 1 chain) of
|
||||||
|
T_IfExpression {} : _ -> True
|
||||||
|
T_WhileExpression {} : _ -> True
|
||||||
|
T_UntilExpression {} : _ -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
-- Skip any parent of a T_Subshell until we reach something interesting
|
||||||
|
skippable t =
|
||||||
|
case t of
|
||||||
|
T_Redirecting _ [] _ -> True
|
||||||
|
T_Pipeline _ [] _ -> True
|
||||||
|
T_Annotation {} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Analyzer (analyzeScript) where
|
module ShellCheck.Analyzer (analyzeScript) where
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
module ShellCheck.AnalyzerLib where
|
module ShellCheck.AnalyzerLib where
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
@@ -34,11 +34,12 @@ import Control.Monad.State
|
|||||||
import Control.Monad.Writer
|
import Control.Monad.Writer
|
||||||
import Data.Char
|
import Data.Char
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
import Data.Maybe
|
||||||
|
import Data.Semigroup
|
||||||
|
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
|
||||||
|
|
||||||
type Analysis = AnalyzerM ()
|
type Analysis = AnalyzerM ()
|
||||||
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
||||||
@@ -57,16 +58,18 @@ runChecker params checker = notes
|
|||||||
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
|
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
|
||||||
notes = snd $ evalRWS (check $ Root root) params Cache
|
notes = snd $ evalRWS (check $ Root root) params Cache
|
||||||
|
|
||||||
|
instance Semigroup Checker where
|
||||||
|
(<>) x y = Checker {
|
||||||
|
perScript = perScript x `composeAnalyzers` perScript y,
|
||||||
|
perToken = perToken x `composeAnalyzers` perToken y
|
||||||
|
}
|
||||||
|
|
||||||
instance Monoid Checker where
|
instance Monoid Checker where
|
||||||
mempty = Checker {
|
mempty = Checker {
|
||||||
perScript = nullCheck,
|
perScript = nullCheck,
|
||||||
perToken = nullCheck
|
perToken = nullCheck
|
||||||
}
|
}
|
||||||
mappend x y = Checker {
|
mappend = (Data.Semigroup.<>)
|
||||||
perScript = perScript x `composeAnalyzers` perScript y,
|
|
||||||
perToken = perToken x `composeAnalyzers` perToken y
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
||||||
composeAnalyzers f g x = f x >> g x
|
composeAnalyzers f g x = f x >> g x
|
||||||
@@ -444,15 +447,12 @@ getModifiedVariables t =
|
|||||||
c@T_SimpleCommand {} ->
|
c@T_SimpleCommand {} ->
|
||||||
getModifiedVariableCommand c
|
getModifiedVariableCommand c
|
||||||
|
|
||||||
TA_Unary _ "++|" var -> maybeToList $ do
|
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||||
name <- getLiteralString var
|
[(t, v, name, DataString $ SourceFrom [v])]
|
||||||
return (t, t, name, DataString $ SourceFrom [t])
|
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||||
TA_Unary _ "|++" var -> maybeToList $ do
|
[(t, v, name, DataString $ SourceFrom [v])]
|
||||||
name <- getLiteralString var
|
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
|
||||||
return (t, t, name, DataString $ SourceFrom [t])
|
|
||||||
TA_Assignment _ op lhs rhs -> maybeToList $ do
|
|
||||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||||
name <- getLiteralString lhs
|
|
||||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||||
|
|
||||||
-- Count [[ -v foo ]] as an "assignment".
|
-- Count [[ -v foo ]] as an "assignment".
|
||||||
@@ -465,7 +465,7 @@ getModifiedVariables t =
|
|||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
guard . not . null $ str
|
guard . not . null $ str
|
||||||
return (t, token, str, DataString $ SourceChecked)
|
return (t, token, str, DataString SourceChecked)
|
||||||
|
|
||||||
T_DollarBraced _ l -> maybeToList $ do
|
T_DollarBraced _ l -> maybeToList $ do
|
||||||
let string = bracedString t
|
let string = bracedString t
|
||||||
@@ -498,7 +498,9 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
|||||||
"export" -> if "f" `elem` flags
|
"export" -> if "f" `elem` flags
|
||||||
then []
|
then []
|
||||||
else concatMap getReference rest
|
else concatMap getReference rest
|
||||||
"declare" -> if any (`elem` flags) ["x", "p"]
|
"declare" -> if
|
||||||
|
any (`elem` flags) ["x", "p"] &&
|
||||||
|
(not $ any (`elem` flags) ["f", "F"])
|
||||||
then concatMap getReference rest
|
then concatMap getReference rest
|
||||||
else []
|
else []
|
||||||
"readonly" ->
|
"readonly" ->
|
||||||
@@ -620,12 +622,17 @@ getIndexReferences s = fromMaybe [] $ do
|
|||||||
where
|
where
|
||||||
re = mkRegex "(\\[.*\\])"
|
re = mkRegex "(\\[.*\\])"
|
||||||
|
|
||||||
|
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
||||||
|
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||||
|
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
||||||
|
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||||
getOffsetReferences mods = fromMaybe [] $ do
|
getOffsetReferences mods = fromMaybe [] $ do
|
||||||
|
-- if mods start with [, then drop until ]
|
||||||
match <- matchRegex re mods
|
match <- matchRegex re mods
|
||||||
offsets <- match !!! 0
|
offsets <- match !!! 1
|
||||||
return $ matchAllStrings variableNameRegex offsets
|
return $ matchAllStrings variableNameRegex offsets
|
||||||
where
|
where
|
||||||
re = mkRegex "^ *:([^-=?+].*)"
|
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||||
|
|
||||||
getReferencedVariables parents t =
|
getReferencedVariables parents t =
|
||||||
case t of
|
case t of
|
||||||
@@ -634,10 +641,10 @@ getReferencedVariables parents t =
|
|||||||
map (\x -> (l, l, x)) (
|
map (\x -> (l, l, x)) (
|
||||||
getIndexReferences str
|
getIndexReferences str
|
||||||
++ getOffsetReferences (getBracedModifier str))
|
++ getOffsetReferences (getBracedModifier str))
|
||||||
TA_Expansion id _ ->
|
TA_Variable id name _ ->
|
||||||
if isArithmeticAssignment t
|
if isArithmeticAssignment t
|
||||||
then []
|
then []
|
||||||
else getIfReference t t
|
else [(t, t, name)]
|
||||||
T_Assignment id mode str _ word ->
|
T_Assignment id mode str _ word ->
|
||||||
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
||||||
|
|
||||||
@@ -664,7 +671,6 @@ getReferencedVariables parents t =
|
|||||||
else []
|
else []
|
||||||
|
|
||||||
literalizer t = case t of
|
literalizer t = case t of
|
||||||
TA_Index {} -> return "" -- x[0] becomes a reference of x
|
|
||||||
T_Glob _ s -> return s -- Also when parsed as globs
|
T_Glob _ s -> return s -- Also when parsed as globs
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
@@ -691,9 +697,8 @@ isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `
|
|||||||
-- Compare a command to a literal. Like above, but checks full path.
|
-- Compare a command to a literal. Like above, but checks full path.
|
||||||
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||||
|
|
||||||
isCommandMatch token matcher = fromMaybe False $ do
|
isCommandMatch token matcher = fromMaybe False $
|
||||||
cmd <- getCommandName token
|
fmap matcher (getCommandName token)
|
||||||
return $ matcher cmd
|
|
||||||
|
|
||||||
-- Does this regex look like it was intended as a glob?
|
-- Does this regex look like it was intended as a glob?
|
||||||
-- True: *foo*
|
-- True: *foo*
|
||||||
@@ -837,7 +842,38 @@ isQuotedAlternativeReference t =
|
|||||||
where
|
where
|
||||||
re = mkRegex "(^|\\]):?\\+"
|
re = mkRegex "(^|\\]):?\\+"
|
||||||
|
|
||||||
|
-- getGnuOpts "erd:u:" will parse a SimpleCommand like
|
||||||
|
-- read -re -d : -u 3 bar
|
||||||
|
-- into
|
||||||
|
-- Just [("r", -re), ("e", -re), ("d", :), ("u", 3), ("", bar)]
|
||||||
|
-- where flags with arguments map to arguments, while others map to themselves.
|
||||||
|
-- Any unrecognized flag will result in Nothing.
|
||||||
|
getGnuOpts = getOpts getAllFlags
|
||||||
|
getBsdOpts = getOpts getLeadingFlags
|
||||||
|
getOpts :: (Token -> [(Token, String)]) -> String -> Token -> Maybe [(String, Token)]
|
||||||
|
getOpts flagTokenizer string cmd = process flags
|
||||||
|
where
|
||||||
|
flags = flagTokenizer cmd
|
||||||
|
flagList (c:':':rest) = ([c], True) : flagList rest
|
||||||
|
flagList (c:rest) = ([c], False) : flagList rest
|
||||||
|
flagList [] = []
|
||||||
|
flagMap = Map.fromList $ ("", False) : flagList string
|
||||||
|
|
||||||
|
process [] = return []
|
||||||
|
process [(token, flag)] = do
|
||||||
|
takesArg <- Map.lookup flag flagMap
|
||||||
|
guard $ not takesArg
|
||||||
|
return [(flag, token)]
|
||||||
|
process ((token1, flag1):rest2@((token2, flag2):rest)) = do
|
||||||
|
takesArg <- Map.lookup flag1 flagMap
|
||||||
|
if takesArg
|
||||||
|
then do
|
||||||
|
guard $ flag2 == ""
|
||||||
|
more <- process rest
|
||||||
|
return $ (flag1, token2) : more
|
||||||
|
else do
|
||||||
|
more <- process rest2
|
||||||
|
return $ (flag1, token1) : more
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||||
@@ -194,5 +194,9 @@ prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo
|
|||||||
prop_filewideAnnotation8 = null $
|
prop_filewideAnnotation8 = null $
|
||||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||||
|
|
||||||
|
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
||||||
|
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
||||||
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,15 +15,13 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
-- This module contains checks that examine specific commands by name.
|
-- This module contains checks that examine specific commands by name.
|
||||||
module ShellCheck.Checks.Commands (checker
|
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
|
||||||
, ShellCheck.Checks.Commands.runTests
|
|
||||||
) where
|
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
@@ -88,6 +86,12 @@ commandChecks = [
|
|||||||
,checkWhileGetoptsCase
|
,checkWhileGetoptsCase
|
||||||
,checkCatastrophicRm
|
,checkCatastrophicRm
|
||||||
,checkLetUsage
|
,checkLetUsage
|
||||||
|
,checkMvArguments, checkCpArguments, checkLnArguments
|
||||||
|
,checkFindRedirections
|
||||||
|
,checkReadExpansions
|
||||||
|
,checkWhich
|
||||||
|
,checkSudoRedirect
|
||||||
|
,checkSudoArgs
|
||||||
]
|
]
|
||||||
|
|
||||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||||
@@ -480,13 +484,13 @@ checkInteractiveSu = CommandCheck (Basename "su") f
|
|||||||
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
|
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
|
||||||
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
||||||
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
||||||
|
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\""
|
||||||
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||||
where
|
where
|
||||||
nonOptions =
|
isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
|
||||||
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
|
|
||||||
f args =
|
f args =
|
||||||
case nonOptions args of
|
case partition isOption args of
|
||||||
(hostport:r@(_:_)) -> checkArg $ last r
|
([], hostport:r@(_:_)) -> checkArg $ last r
|
||||||
_ -> return ()
|
_ -> return ()
|
||||||
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||||
case filter (not . isConstant) parts of
|
case filter (not . isConstant) parts of
|
||||||
@@ -507,6 +511,10 @@ prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"
|
|||||||
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||||
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||||
|
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
||||||
|
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
||||||
|
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
|
||||||
|
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
||||||
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
|
||||||
@@ -517,6 +525,7 @@ 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 -> 1 + countFormats rest
|
'%':rest -> 1 + countFormats rest
|
||||||
_:rest -> countFormats rest
|
_:rest -> countFormats rest
|
||||||
[] -> 0
|
[] -> 0
|
||||||
@@ -532,7 +541,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||||||
"This printf format string has no variables. Other arguments are ignored."
|
"This printf format string has no variables. Other arguments are ignored."
|
||||||
|
|
||||||
when (vars > 0
|
when (vars > 0
|
||||||
&& length more < vars
|
&& ((length more) `mod` vars /= 0 || null more)
|
||||||
&& all (not . mayBecomeMultipleArgs) more) $
|
&& all (not . mayBecomeMultipleArgs) more) $
|
||||||
warn (getId format) 2183 $
|
warn (getId format) 2183 $
|
||||||
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
|
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
|
||||||
@@ -585,15 +594,48 @@ checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
|||||||
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
|
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
|
||||||
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
||||||
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
|
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
|
||||||
checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
|
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}"
|
||||||
|
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
|
||||||
where
|
where
|
||||||
check = mapM_ checkForVariables
|
check t = potentially $ do
|
||||||
checkForVariables f =
|
var <- getSingleUnmodifiedVariable t
|
||||||
case getWordParts f of
|
let name = bracedString var
|
||||||
[t@(T_DollarBraced {})] ->
|
return . warn (getId t) 2163 $
|
||||||
warn (getId t) 2163 "Exporting an expansion rather than a variable."
|
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||||
_ -> return ()
|
|
||||||
|
|
||||||
|
prop_checkReadExpansions1 = verify checkReadExpansions "read $var"
|
||||||
|
prop_checkReadExpansions2 = verify checkReadExpansions "read -r $var"
|
||||||
|
prop_checkReadExpansions3 = verifyNot checkReadExpansions "read -p $var"
|
||||||
|
prop_checkReadExpansions4 = verifyNot checkReadExpansions "read -rd $delim name"
|
||||||
|
prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
|
||||||
|
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
|
||||||
|
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
|
||||||
|
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
|
||||||
|
checkReadExpansions = CommandCheck (Exactly "read") check
|
||||||
|
where
|
||||||
|
options = getGnuOpts "sreu:n:N:i:p:a:"
|
||||||
|
getVars cmd = fromMaybe [] $ do
|
||||||
|
opts <- options cmd
|
||||||
|
return . map snd $ filter (\(x,_) -> x == "" || x == "a") opts
|
||||||
|
|
||||||
|
check cmd = mapM_ warning $ getVars cmd
|
||||||
|
warning t = potentially $ do
|
||||||
|
var <- getSingleUnmodifiedVariable t
|
||||||
|
let name = bracedString var
|
||||||
|
guard $ isVariableName name -- e.g. not $1
|
||||||
|
return . warn (getId t) 2229 $
|
||||||
|
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||||
|
|
||||||
|
-- Return the single variable expansion that makes up this word, if any.
|
||||||
|
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
|
||||||
|
getSingleUnmodifiedVariable :: Token -> Maybe Token
|
||||||
|
getSingleUnmodifiedVariable word =
|
||||||
|
case getWordParts word of
|
||||||
|
[t@(T_DollarBraced {})] ->
|
||||||
|
let contents = bracedString t
|
||||||
|
name = getBracedReference contents
|
||||||
|
in guard (contents == name) >> return t
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||||
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
||||||
@@ -850,5 +892,112 @@ checkLetUsage = CommandCheck (Exactly "let") f
|
|||||||
f t = whenShell [Bash,Ksh] $ do
|
f t = whenShell [Bash,Ksh] $ do
|
||||||
style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ."
|
style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ."
|
||||||
|
|
||||||
|
|
||||||
|
missingDestination handler token = do
|
||||||
|
case params of
|
||||||
|
[single] -> do
|
||||||
|
unless (hasTarget || mayBecomeMultipleArgs single) $
|
||||||
|
handler token
|
||||||
|
_ -> return ()
|
||||||
|
where
|
||||||
|
args = getAllFlags token
|
||||||
|
params = map fst $ filter (\(_,x) -> x == "") args
|
||||||
|
hasTarget =
|
||||||
|
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
|
||||||
|
map snd args
|
||||||
|
|
||||||
|
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'"
|
||||||
|
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar"
|
||||||
|
prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}"
|
||||||
|
prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\""
|
||||||
|
prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar"
|
||||||
|
prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar"
|
||||||
|
prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar"
|
||||||
|
prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version"
|
||||||
|
prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\""
|
||||||
|
checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
|
||||||
|
where
|
||||||
|
f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
|
||||||
|
|
||||||
|
checkCpArguments = CommandCheck (Basename "cp") $ missingDestination f
|
||||||
|
where
|
||||||
|
f t = err (getId t) 2225 "This cp has no destination. Check the arguments."
|
||||||
|
|
||||||
|
checkLnArguments = CommandCheck (Basename "ln") $ missingDestination f
|
||||||
|
where
|
||||||
|
f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkFindRedirections1 = verify checkFindRedirections "find . -exec echo {} > file \\;"
|
||||||
|
prop_checkFindRedirections2 = verifyNot checkFindRedirections "find . -exec echo {} \\; > file"
|
||||||
|
prop_checkFindRedirections3 = verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
|
||||||
|
checkFindRedirections = CommandCheck (Basename "find") f
|
||||||
|
where
|
||||||
|
f t = do
|
||||||
|
redirecting <- getClosestCommandM t
|
||||||
|
case redirecting of
|
||||||
|
Just (T_Redirecting _ redirs@(_:_) (T_SimpleCommand _ _ args@(_:_:_))) -> do
|
||||||
|
-- This assumes IDs are sequential, which is mostly but not always true.
|
||||||
|
let minRedir = minimum $ map getId redirs
|
||||||
|
let maxArg = maximum $ map getId args
|
||||||
|
when (minRedir < maxArg) $
|
||||||
|
warn minRedir 2227
|
||||||
|
"Redirection applies to the find command itself. Rewrite to work per action (or move to end)."
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
prop_checkWhich = verify checkWhich "which '.+'"
|
||||||
|
checkWhich = CommandCheck (Basename "which") $
|
||||||
|
\t -> info (getId t) 2230 "which is non-standard. Use builtin 'command -v' instead."
|
||||||
|
|
||||||
|
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
|
||||||
|
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input"
|
||||||
|
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file"
|
||||||
|
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file"
|
||||||
|
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1"
|
||||||
|
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log"
|
||||||
|
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
|
||||||
|
checkSudoRedirect = CommandCheck (Basename "sudo") f
|
||||||
|
where
|
||||||
|
f t = do
|
||||||
|
t_redir <- getClosestCommandM t
|
||||||
|
case t_redir of
|
||||||
|
Just (T_Redirecting _ redirs _) ->
|
||||||
|
mapM_ warnAbout redirs
|
||||||
|
warnAbout (T_FdRedirect _ s (T_IoFile id op file))
|
||||||
|
| (s == "" || s == "&") && not (special file) =
|
||||||
|
case op of
|
||||||
|
T_Less _ ->
|
||||||
|
info (getId op) 2024
|
||||||
|
"sudo doesn't affect redirects. Use sudo cat file | .."
|
||||||
|
T_Greater _ ->
|
||||||
|
warn (getId op) 2024
|
||||||
|
"sudo doesn't affect redirects. Use ..| sudo tee file"
|
||||||
|
T_DGREAT _ ->
|
||||||
|
warn (getId op) 2024
|
||||||
|
"sudo doesn't affect redirects. Use .. | sudo tee -a file"
|
||||||
|
_ -> return ()
|
||||||
|
warnAbout _ = return ()
|
||||||
|
special file = concat (oversimplify file) == "/dev/null"
|
||||||
|
|
||||||
|
prop_checkSudoArgs1 = verify checkSudoArgs "sudo cd /root"
|
||||||
|
prop_checkSudoArgs2 = verify checkSudoArgs "sudo export x=3"
|
||||||
|
prop_checkSudoArgs3 = verifyNot checkSudoArgs "sudo ls /usr/local/protected"
|
||||||
|
prop_checkSudoArgs4 = verifyNot checkSudoArgs "sudo ls && export x=3"
|
||||||
|
prop_checkSudoArgs5 = verifyNot checkSudoArgs "sudo echo ls"
|
||||||
|
prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls"
|
||||||
|
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo"
|
||||||
|
checkSudoArgs = CommandCheck (Basename "sudo") f
|
||||||
|
where
|
||||||
|
f t = potentially $ do
|
||||||
|
opts <- parseOpts t
|
||||||
|
let nonFlags = map snd $ filter (\(flag, _) -> flag == "") opts
|
||||||
|
commandArg <- nonFlags !!! 0
|
||||||
|
command <- getLiteralString commandArg
|
||||||
|
guard $ command `elem` builtins
|
||||||
|
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
|
||||||
|
builtins = [ "cd", "eval", "export", "history", "read", "source", "wait" ]
|
||||||
|
-- This mess is why ShellCheck prefers not to know.
|
||||||
|
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2016 Vidar Holen
|
Copyright 2012-2016 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,13 +15,11 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
module ShellCheck.Checks.ShellSupport (checker
|
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
||||||
, ShellCheck.Checks.ShellSupport.runTests
|
|
||||||
) where
|
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
@@ -136,6 +134,8 @@ prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
|||||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||||
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||||
|
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||||
|
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
||||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
params <- ask
|
params <- ask
|
||||||
kludge params t
|
kludge params t
|
||||||
@@ -191,11 +191,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
warnMsg id "^ in place of ! in glob bracket expressions is"
|
||||||
|
|
||||||
bashism t@(TA_Expansion id _) | isBashism =
|
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||||
warnMsg id $ fromJust str ++ " is"
|
warnMsg id $ str ++ " is"
|
||||||
where
|
|
||||||
str = getLiteralString t
|
|
||||||
isBashism = isJust str && isBashVariable (fromJust str)
|
|
||||||
bashism t@(T_DollarBraced id token) = do
|
bashism t@(T_DollarBraced id token) = do
|
||||||
mapM_ check expansion
|
mapM_ check expansion
|
||||||
when (isBashVariable var) $
|
when (isBashVariable var) $
|
||||||
@@ -279,14 +277,18 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||||
"typeset"
|
"typeset"
|
||||||
] ++ if not isDash then ["local", "type"] else []
|
] ++ if not isDash then ["local"] else []
|
||||||
allowedFlags = Map.fromList [
|
allowedFlags = Map.fromList [
|
||||||
("read", if isDash then ["r", "p"] else ["r"]),
|
("exec", []),
|
||||||
("ulimit", ["f"]),
|
("export", ["-p"]),
|
||||||
("printf", []),
|
("printf", []),
|
||||||
("exec", [])
|
("read", if isDash then ["r", "p"] else ["r"]),
|
||||||
|
("ulimit", ["f"])
|
||||||
]
|
]
|
||||||
|
bashism t@(T_SourceCommand id src _) =
|
||||||
|
let name = fromMaybe "" $ getCommandName src
|
||||||
|
in do
|
||||||
|
when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
||||||
bashism _ = return ()
|
bashism _ = return ()
|
||||||
|
|
||||||
varChars="_0-9a-zA-Z"
|
varChars="_0-9a-zA-Z"
|
||||||
@@ -295,8 +297,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
||||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
||||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
||||||
(re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
|
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
|
||||||
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
|
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
|
||||||
|
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
||||||
]
|
]
|
||||||
bashVars = [
|
bashVars = [
|
||||||
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
@@ -79,10 +79,10 @@ commonCommands = [
|
|||||||
|
|
||||||
nonReadingCommands = [
|
nonReadingCommands = [
|
||||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||||
"cp", "du", "echo", "export", "fg", "fuser", "getconf", "getopt",
|
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
||||||
"getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", "locale", "mv",
|
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||||
"nice", "printf", "ps", "pwd", "renice", "rm", "rmdir", "set", "sleep",
|
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||||
"touch", "trap", "ulimit", "unalias", "uname"
|
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
||||||
]
|
]
|
||||||
|
|
||||||
sampleWords = [
|
sampleWords = [
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.CheckStyle (format) where
|
module ShellCheck.Formatter.CheckStyle (format) where
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.Format where
|
module ShellCheck.Formatter.Format where
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.GCC (format) where
|
module ShellCheck.Formatter.GCC (format) where
|
||||||
|
|
@@ -1,8 +1,9 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-
|
{-
|
||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,17 +16,19 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.JSON (format) where
|
module ShellCheck.Formatter.JSON (format) where
|
||||||
|
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Data.Aeson
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
|
import Data.Monoid
|
||||||
import GHC.Exts
|
import GHC.Exts
|
||||||
import System.IO
|
import System.IO
|
||||||
import Text.JSON
|
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||||
|
|
||||||
format = do
|
format = do
|
||||||
ref <- newIORef []
|
ref <- newIORef []
|
||||||
@@ -36,19 +39,30 @@ format = do
|
|||||||
footer = finish ref
|
footer = finish ref
|
||||||
}
|
}
|
||||||
|
|
||||||
instance JSON (PositionedComment) where
|
instance ToJSON (PositionedComment) where
|
||||||
showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [
|
toJSON comment@(PositionedComment start end (Comment level code string)) =
|
||||||
("file", showJSON $ posFile start),
|
object [
|
||||||
("line", showJSON $ posLine start),
|
"file" .= posFile start,
|
||||||
("endLine", showJSON $ posLine end),
|
"line" .= posLine start,
|
||||||
("column", showJSON $ posColumn start),
|
"endLine" .= posLine end,
|
||||||
("endColumn", showJSON $ posColumn end),
|
"column" .= posColumn start,
|
||||||
("level", showJSON $ severityText comment),
|
"endColumn" .= posColumn end,
|
||||||
("code", showJSON code),
|
"level" .= severityText comment,
|
||||||
("message", showJSON string)
|
"code" .= code,
|
||||||
|
"message" .= string
|
||||||
]
|
]
|
||||||
|
|
||||||
readJSON = undefined
|
toEncoding comment@(PositionedComment start end (Comment level code string)) =
|
||||||
|
pairs (
|
||||||
|
"file" .= posFile start
|
||||||
|
<> "line" .= posLine start
|
||||||
|
<> "endLine" .= posLine end
|
||||||
|
<> "column" .= posColumn start
|
||||||
|
<> "endColumn" .= posColumn end
|
||||||
|
<> "level" .= severityText comment
|
||||||
|
<> "code" .= code
|
||||||
|
<> "message" .= string
|
||||||
|
)
|
||||||
|
|
||||||
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
||||||
collectResult ref result _ =
|
collectResult ref result _ =
|
||||||
@@ -56,5 +70,5 @@ collectResult ref result _ =
|
|||||||
|
|
||||||
finish ref = do
|
finish ref = do
|
||||||
list <- readIORef ref
|
list <- readIORef ref
|
||||||
putStrLn $ encodeStrict list
|
BL.putStrLn $ encode list
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Formatter.TTY (format) where
|
module ShellCheck.Formatter.TTY (format) where
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
module ShellCheck.Interface where
|
module ShellCheck.Interface where
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,9 +15,12 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE NoMonomorphismRestriction, TemplateHaskell, FlexibleContexts #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE NoMonomorphismRestriction #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
module ShellCheck.Parser (parseScript, runTests) where
|
module ShellCheck.Parser (parseScript, runTests) where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
@@ -25,7 +28,7 @@ import ShellCheck.ASTLib
|
|||||||
import ShellCheck.Data
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
|
|
||||||
import Control.Applicative ((<*))
|
import Control.Applicative ((<*), (*>))
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
import Control.Monad.Trans
|
import Control.Monad.Trans
|
||||||
@@ -143,8 +146,7 @@ data Context =
|
|||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data HereDocContext =
|
data HereDocContext =
|
||||||
HereDocPending Token -- on linefeed, read this T_HereDoc
|
HereDocPending Token [Context] -- on linefeed, read this T_HereDoc
|
||||||
| HereDocBoundary -- but don't consider heredocs before this
|
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data UserState = UserState {
|
data UserState = UserState {
|
||||||
@@ -193,50 +195,21 @@ addToHereDocMap id list = do
|
|||||||
hereDocMap = Map.insert id list map
|
hereDocMap = Map.insert id list map
|
||||||
}
|
}
|
||||||
|
|
||||||
withHereDocBoundary p = do
|
|
||||||
pushBoundary
|
|
||||||
do
|
|
||||||
v <- p
|
|
||||||
popBoundary
|
|
||||||
return v
|
|
||||||
<|> do
|
|
||||||
popBoundary
|
|
||||||
fail ""
|
|
||||||
where
|
|
||||||
pushBoundary = do
|
|
||||||
state <- getState
|
|
||||||
let docs = pendingHereDocs state
|
|
||||||
putState $ state {
|
|
||||||
pendingHereDocs = HereDocBoundary : docs
|
|
||||||
}
|
|
||||||
popBoundary = do
|
|
||||||
state <- getState
|
|
||||||
let docs = tail $ dropWhile (not . isHereDocBoundary) $
|
|
||||||
pendingHereDocs state
|
|
||||||
putState $ state {
|
|
||||||
pendingHereDocs = docs
|
|
||||||
}
|
|
||||||
|
|
||||||
addPendingHereDoc t = do
|
addPendingHereDoc t = do
|
||||||
state <- getState
|
state <- getState
|
||||||
|
context <- getCurrentContexts
|
||||||
let docs = pendingHereDocs state
|
let docs = pendingHereDocs state
|
||||||
putState $ state {
|
putState $ state {
|
||||||
pendingHereDocs = HereDocPending t : docs
|
pendingHereDocs = HereDocPending t context : docs
|
||||||
}
|
}
|
||||||
|
|
||||||
popPendingHereDocs = do
|
popPendingHereDocs = do
|
||||||
state <- getState
|
state <- getState
|
||||||
let (pending, boundary) = break isHereDocBoundary $ pendingHereDocs state
|
let pending = pendingHereDocs state
|
||||||
putState $ state {
|
putState $ state {
|
||||||
pendingHereDocs = boundary
|
pendingHereDocs = []
|
||||||
}
|
}
|
||||||
return . map extract . reverse $ pendingHereDocs state
|
return . reverse $ pendingHereDocs state
|
||||||
where
|
|
||||||
extract (HereDocPending t) = t
|
|
||||||
|
|
||||||
isHereDocBoundary x = case x of
|
|
||||||
HereDocBoundary -> True
|
|
||||||
otherwise -> False
|
|
||||||
|
|
||||||
getMap = positionMap <$> getState
|
getMap = positionMap <$> getState
|
||||||
getParseNotes = parseNotes <$> getState
|
getParseNotes = parseNotes <$> getState
|
||||||
@@ -331,11 +304,14 @@ pushContext c = do
|
|||||||
parseProblemAtWithEnd start end level code msg = do
|
parseProblemAtWithEnd start end level code msg = do
|
||||||
irrelevant <- shouldIgnoreCode code
|
irrelevant <- shouldIgnoreCode code
|
||||||
unless irrelevant $
|
unless irrelevant $
|
||||||
|
addParseProblem note
|
||||||
|
where
|
||||||
|
note = ParseNote start end level code msg
|
||||||
|
|
||||||
|
addParseProblem note =
|
||||||
Ms.modify (\state -> state {
|
Ms.modify (\state -> state {
|
||||||
parseProblems = note:parseProblems state
|
parseProblems = note:parseProblems state
|
||||||
})
|
})
|
||||||
where
|
|
||||||
note = ParseNote start end level code msg
|
|
||||||
|
|
||||||
parseProblemAt pos = parseProblemAtWithEnd pos pos
|
parseProblemAt pos = parseProblemAtWithEnd pos pos
|
||||||
|
|
||||||
@@ -401,15 +377,16 @@ acceptButWarn parser level code note =
|
|||||||
parseProblemAt pos level code note
|
parseProblemAt pos level code note
|
||||||
)
|
)
|
||||||
|
|
||||||
withContext entry p = do
|
parsecBracket before after op = do
|
||||||
pushContext entry
|
val <- before
|
||||||
do
|
(op val <* optional (after val)) <|> (after val *> fail "")
|
||||||
v <- p
|
|
||||||
popContext
|
swapContext contexts p =
|
||||||
return v
|
parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
|
||||||
<|> do -- p failed without consuming input, abort context
|
setCurrentContexts
|
||||||
v <- popContext
|
(const p)
|
||||||
fail ""
|
|
||||||
|
withContext entry p = parsecBracket (pushContext entry) (const popContext) (const p)
|
||||||
|
|
||||||
called s p = do
|
called s p = do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
@@ -421,7 +398,7 @@ withAnnotations anns =
|
|||||||
readConditionContents single =
|
readConditionContents single =
|
||||||
readCondContents `attempting` lookAhead (do
|
readCondContents `attempting` lookAhead (do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
s <- many1 letter
|
s <- readVariableName
|
||||||
when (s `elem` commonCommands) $
|
when (s `elem` commonCommands) $
|
||||||
parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")
|
parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")
|
||||||
|
|
||||||
@@ -605,17 +582,19 @@ readConditionContents single =
|
|||||||
<|> return False
|
<|> return False
|
||||||
readRegex = called "regex" $ do
|
readRegex = called "regex" $ do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
parts <- many1 (
|
parts <- many1 readPart
|
||||||
readGroup <|>
|
|
||||||
readSingleQuoted <|>
|
|
||||||
readDoubleQuoted <|>
|
|
||||||
readDollarExpression <|>
|
|
||||||
readNormalLiteral "( " <|>
|
|
||||||
readPipeLiteral <|>
|
|
||||||
readGlobLiteral)
|
|
||||||
void spacing
|
void spacing
|
||||||
return $ T_NormalWord id parts
|
return $ T_NormalWord id parts
|
||||||
where
|
where
|
||||||
|
readPart = choice [
|
||||||
|
readGroup,
|
||||||
|
readSingleQuoted,
|
||||||
|
readDoubleQuoted,
|
||||||
|
readDollarExpression,
|
||||||
|
readNormalLiteral "( ",
|
||||||
|
readPipeLiteral,
|
||||||
|
readGlobLiteral
|
||||||
|
]
|
||||||
readGlobLiteral = do
|
readGlobLiteral = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
s <- extglobStart <|> oneOf "{}[]$"
|
s <- extglobStart <|> oneOf "{}[]$"
|
||||||
@@ -623,7 +602,7 @@ readConditionContents single =
|
|||||||
readGroup = called "regex grouping" $ do
|
readGroup = called "regex grouping" $ do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
char '('
|
char '('
|
||||||
parts <- many (readGroup <|> readSingleQuoted <|> readDoubleQuoted <|> readDollarExpression <|> readRegexLiteral <|> readGlobLiteral)
|
parts <- many (readPart <|> readRegexLiteral)
|
||||||
char ')'
|
char ')'
|
||||||
return $ T_NormalWord id parts
|
return $ T_NormalWord id parts
|
||||||
readRegexLiteral = do
|
readRegexLiteral = do
|
||||||
@@ -724,31 +703,35 @@ readArithmeticContents =
|
|||||||
spacing1
|
spacing1
|
||||||
return (str, alt)
|
return (str, alt)
|
||||||
|
|
||||||
|
|
||||||
readArrayIndex = do
|
readArrayIndex = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
char '['
|
char '['
|
||||||
middle <- readArithmeticContents
|
pos <- getPosition
|
||||||
|
middle <- readStringForParser readArithmeticContents
|
||||||
char ']'
|
char ']'
|
||||||
return $ TA_Index id middle
|
return $ T_UnparsedIndex id pos middle
|
||||||
|
|
||||||
literal s = do
|
literal s = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
string s
|
string s
|
||||||
return $ T_Literal id s
|
return $ T_Literal id s
|
||||||
|
|
||||||
readArithmeticLiteral =
|
readVariable = do
|
||||||
readArrayIndex <|> literal "#"
|
id <- getNextId
|
||||||
|
name <- readVariableName
|
||||||
|
indices <- many readArrayIndex
|
||||||
|
spacing
|
||||||
|
return $ TA_Variable id name indices
|
||||||
|
|
||||||
readExpansion = do
|
readExpansion = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
pieces <- many1 $ choice [
|
pieces <- many1 $ choice [
|
||||||
readArithmeticLiteral,
|
|
||||||
readSingleQuoted,
|
readSingleQuoted,
|
||||||
readDoubleQuoted,
|
readDoubleQuoted,
|
||||||
readNormalDollar,
|
readNormalDollar,
|
||||||
readBraced,
|
readBraced,
|
||||||
readUnquotedBackTicked,
|
readUnquotedBackTicked,
|
||||||
|
literal "#",
|
||||||
readNormalLiteral "+-*/=%^,]?:"
|
readNormalLiteral "+-*/=%^,]?:"
|
||||||
]
|
]
|
||||||
spacing
|
spacing
|
||||||
@@ -761,7 +744,7 @@ readArithmeticContents =
|
|||||||
spacing
|
spacing
|
||||||
return s
|
return s
|
||||||
|
|
||||||
readArithTerm = readGroup <|> readExpansion
|
readArithTerm = readGroup <|> readVariable <|> readExpansion
|
||||||
|
|
||||||
readSequence = do
|
readSequence = do
|
||||||
spacing
|
spacing
|
||||||
@@ -864,6 +847,9 @@ prop_readCondition16= isOk readCondition "[ foo \\< bar ]"
|
|||||||
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
|
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
|
||||||
prop_readCondition18= isOk readCondition "[ ]"
|
prop_readCondition18= isOk readCondition "[ ]"
|
||||||
prop_readCondition19= isOk readCondition "[ '(' x \")\" ]"
|
prop_readCondition19= isOk readCondition "[ '(' x \")\" ]"
|
||||||
|
prop_readCondition20= isOk readCondition "[[ echo_rc -eq 0 ]]"
|
||||||
|
prop_readCondition21= isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
|
||||||
|
prop_readCondition22= isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
|
||||||
readCondition = called "test expression" $ do
|
readCondition = called "test expression" $ do
|
||||||
opos <- getPosition
|
opos <- getPosition
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
@@ -1166,6 +1152,7 @@ readBackTicked quoted = called "backtick expansion" $ do
|
|||||||
parseProblemAt pos ErrorC 1077
|
parseProblemAt pos ErrorC 1077
|
||||||
"For command expansion, the tick should slant left (` vs ´). Use $(..) instead."
|
"For command expansion, the tick should slant left (` vs ´). Use $(..) instead."
|
||||||
|
|
||||||
|
-- Run a parser on a new input, such as for `..` or here documents.
|
||||||
subParse pos parser input = do
|
subParse pos parser input = do
|
||||||
lastPosition <- getPosition
|
lastPosition <- getPosition
|
||||||
lastInput <- getInput
|
lastInput <- getInput
|
||||||
@@ -1237,12 +1224,6 @@ doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBa
|
|||||||
"This is a unicode quote. Delete and retype it (or ignore/singlequote for literal)."
|
"This is a unicode quote. Delete and retype it (or ignore/singlequote for literal)."
|
||||||
return $ T_Literal id [c]
|
return $ T_Literal id [c]
|
||||||
|
|
||||||
readDoubleQuotedLiteral = do
|
|
||||||
doubleQuote
|
|
||||||
x <- readDoubleLiteral
|
|
||||||
doubleQuote
|
|
||||||
return x
|
|
||||||
|
|
||||||
readDoubleLiteral = do
|
readDoubleLiteral = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
s <- many1 readDoubleLiteralPart
|
s <- many1 readDoubleLiteralPart
|
||||||
@@ -1561,6 +1542,7 @@ prop_readDollarVariable = isOk readDollarVariable "$@"
|
|||||||
prop_readDollarVariable2 = isOk (readDollarVariable >> anyChar) "$?!"
|
prop_readDollarVariable2 = isOk (readDollarVariable >> anyChar) "$?!"
|
||||||
prop_readDollarVariable3 = isWarning (readDollarVariable >> anyChar) "$10"
|
prop_readDollarVariable3 = isWarning (readDollarVariable >> anyChar) "$10"
|
||||||
prop_readDollarVariable4 = isWarning (readDollarVariable >> string "[@]") "$arr[@]"
|
prop_readDollarVariable4 = isWarning (readDollarVariable >> string "[@]") "$arr[@]"
|
||||||
|
prop_readDollarVariable5 = isWarning (readDollarVariable >> string "[f") "$arr[f"
|
||||||
|
|
||||||
readDollarVariable = do
|
readDollarVariable = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
@@ -1583,8 +1565,8 @@ readDollarVariable = do
|
|||||||
name <- readVariableName
|
name <- readVariableName
|
||||||
value <- wrap name
|
value <- wrap name
|
||||||
return (T_DollarBraced id value) `attempting` do
|
return (T_DollarBraced id value) `attempting` do
|
||||||
lookAhead $ void (string "[@]") <|> void (string "[*]") <|> void readArrayIndex
|
lookAhead $ char '['
|
||||||
parseNoteAt pos ErrorC 1087 "Braces are required when expanding arrays, as in ${array[idx]}."
|
parseNoteAt pos ErrorC 1087 "Use braces when expanding arrays, e.g. ${array[idx]} (or ${var}[.. to quiet)."
|
||||||
|
|
||||||
try $ char '$' >> (positional <|> special <|> regular)
|
try $ char '$' >> (positional <|> special <|> regular)
|
||||||
|
|
||||||
@@ -1607,9 +1589,9 @@ readDollarLonely = do
|
|||||||
return $ T_Literal id "$"
|
return $ T_Literal id "$"
|
||||||
|
|
||||||
prop_readHereDoc = isOk readScript "cat << foo\nlol\ncow\nfoo"
|
prop_readHereDoc = isOk readScript "cat << foo\nlol\ncow\nfoo"
|
||||||
prop_readHereDoc2 = isWarning readScript "cat <<- EOF\n cow\n EOF"
|
prop_readHereDoc2 = isNotOk readScript "cat <<- EOF\n cow\n EOF"
|
||||||
prop_readHereDoc3 = isOk readScript "cat << foo\n$\"\nfoo"
|
prop_readHereDoc3 = isOk readScript "cat << foo\n$\"\nfoo"
|
||||||
prop_readHereDoc4 = isOk readScript "cat << foo\n`\nfoo"
|
prop_readHereDoc4 = isNotOk readScript "cat << foo\n`\nfoo"
|
||||||
prop_readHereDoc5 = isOk readScript "cat <<- !foo\nbar\n!foo"
|
prop_readHereDoc5 = isOk readScript "cat <<- !foo\nbar\n!foo"
|
||||||
prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
|
prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
|
||||||
prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
|
prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
|
||||||
@@ -1620,9 +1602,13 @@ prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
|
|||||||
prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"
|
prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"
|
||||||
prop_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
|
prop_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
|
||||||
prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"
|
prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"
|
||||||
prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\n"
|
prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
|
||||||
prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"
|
prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"
|
||||||
prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n"
|
prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
|
||||||
|
prop_readHereDoc18= isWarning readScript "cat << foo\nLoose \\t\nfoo"
|
||||||
|
prop_readHereDoc19= isOk readScript "# shellcheck disable=SC1117\ncat << foo\nLoose \\t\nfoo"
|
||||||
|
prop_readHereDoc20= isWarning readScript "cat << foo\n foo\n()\nfoo\n"
|
||||||
|
prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n"
|
||||||
readHereDoc = called "here document" $ do
|
readHereDoc = called "here document" $ do
|
||||||
fid <- getNextId
|
fid <- getNextId
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
@@ -1654,38 +1640,96 @@ readPendingHereDocs = do
|
|||||||
docs <- popPendingHereDocs
|
docs <- popPendingHereDocs
|
||||||
mapM_ readDoc docs
|
mapM_ readDoc docs
|
||||||
where
|
where
|
||||||
readDoc (T_HereDoc id dashed quoted endToken _) = do
|
readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =
|
||||||
pos <- getPosition
|
swapContext ctx $
|
||||||
hereData <- concat <$> rawLine `reluctantlyTill` do
|
|
||||||
linewhitespace `reluctantlyTill` string endToken
|
|
||||||
string endToken
|
|
||||||
void linewhitespace <|> void (oneOf "\n;&#)") <|> eof
|
|
||||||
do
|
do
|
||||||
spaces <- linewhitespace `reluctantlyTill` string endToken
|
docPos <- getPosition
|
||||||
verifyHereDoc dashed quoted spaces hereData
|
tokenPos <- Map.findWithDefault (error "Missing ID") id <$> getMap
|
||||||
string endToken
|
(terminated, wasWarned, lines) <- readDocLines dashed endToken
|
||||||
trailingPos <- getPosition
|
let hereData = unlines lines
|
||||||
trailers <- lookAhead $ many (noneOf "\n")
|
unless terminated $ do
|
||||||
let ppt = parseProblemAt trailingPos ErrorC
|
unless wasWarned $
|
||||||
unless (null trailers) $
|
debugHereDoc tokenPos endToken hereData
|
||||||
if all isSpace trailers
|
fail "Here document was not correctly terminated"
|
||||||
then ppt 1118 "Delete whitespace after the here-doc end token."
|
list <- parseHereData quoted docPos hereData
|
||||||
else case (head $ dropWhile isSpace trailers) of
|
|
||||||
')' -> ppt 1119 $ "Add a linefeed between end token and terminating ')'."
|
|
||||||
'#' -> ppt 1120 "No comments allowed after here-doc token. Comment the next line instead."
|
|
||||||
c | c `elem` ";&" ->
|
|
||||||
ppt 1121 "Add ;/& terminators (and other syntax) on the line with the <<, not here."
|
|
||||||
_ -> ppt 1122 "Nothing allowed after end token. To continue a command, put it on the line with the <<."
|
|
||||||
parsedData <- parseHereData quoted pos hereData
|
|
||||||
list <- parseHereData quoted pos hereData
|
|
||||||
addToHereDocMap id list
|
addToHereDocMap id list
|
||||||
|
|
||||||
`attempting` (eof >> debugHereDoc pos endToken hereData)
|
-- Read the lines making up the here doc. Returns (IsTerminated, Lines)
|
||||||
|
readDocLines :: Monad m => Dashed -> String -> SCParser m (Bool, Bool, [String])
|
||||||
|
readDocLines dashed endToken = do
|
||||||
|
pos <- getPosition
|
||||||
|
str <- rawLine
|
||||||
|
isEof <- option False (eof >> return True)
|
||||||
|
(isEnd, wasWarned) <- subParse pos checkEnd str
|
||||||
|
if
|
||||||
|
| isEnd -> return (True, wasWarned, [])
|
||||||
|
| isEof -> return (False, wasWarned, [str])
|
||||||
|
| True -> do
|
||||||
|
(ok, previousWarning, rest) <- readDocLines dashed endToken
|
||||||
|
return (ok, wasWarned || previousWarning, str:rest)
|
||||||
|
where
|
||||||
|
-- Check if this is the actual end, or a plausible false end
|
||||||
|
checkEnd = option (False, False) $ try $ do
|
||||||
|
-- Match what's basically '^( *)token( *)(.*)$'
|
||||||
|
leadingSpacePos <- getPosition
|
||||||
|
leadingSpace <- linewhitespace `reluctantlyTill` string endToken
|
||||||
|
string endToken
|
||||||
|
trailingSpacePos <- getPosition
|
||||||
|
trailingSpace <- many linewhitespace
|
||||||
|
trailerPos <- getPosition
|
||||||
|
trailer <- many anyChar
|
||||||
|
|
||||||
|
let leadingSpacesAreTabs = all (== '\t') leadingSpace
|
||||||
|
let thereIsNoTrailer = null trailingSpace && null trailer
|
||||||
|
let leaderIsOk = null leadingSpace
|
||||||
|
|| dashed == Dashed && leadingSpacesAreTabs
|
||||||
|
let trailerStart = if null trailer then '\0' else head trailer
|
||||||
|
let hasTrailingSpace = not $ null trailingSpace
|
||||||
|
let hasTrailer = not $ null trailer
|
||||||
|
let ppt = parseProblemAt trailerPos ErrorC
|
||||||
|
|
||||||
|
if leaderIsOk && thereIsNoTrailer
|
||||||
|
then return (True, False)
|
||||||
|
else do
|
||||||
|
let foundCause = return (False, True)
|
||||||
|
let skipLine = return (False, False)
|
||||||
|
-- This may be intended as an end token. Debug why it isn't.
|
||||||
|
if
|
||||||
|
| trailerStart == ')' -> do
|
||||||
|
ppt 1119 $ "Add a linefeed between end token and terminating ')'."
|
||||||
|
foundCause
|
||||||
|
| trailerStart == '#' -> do
|
||||||
|
ppt 1120 "No comments allowed after here-doc token. Comment the next line instead."
|
||||||
|
foundCause
|
||||||
|
| trailerStart `elem` ";>|&" -> do
|
||||||
|
ppt 1121 "Add ;/& terminators (and other syntax) on the line with the <<, not here."
|
||||||
|
foundCause
|
||||||
|
| hasTrailingSpace && hasTrailer -> do
|
||||||
|
ppt 1122 "Nothing allowed after end token. To continue a command, put it on the line with the <<."
|
||||||
|
foundCause
|
||||||
|
| leaderIsOk && hasTrailingSpace && not hasTrailer -> do
|
||||||
|
parseProblemAt trailingSpacePos ErrorC 1118 "Delete whitespace after the here-doc end token."
|
||||||
|
-- Parse as if it's the actual end token. Will koala_man regret this once again?
|
||||||
|
return (True, True)
|
||||||
|
| not hasTrailingSpace && hasTrailer ->
|
||||||
|
-- The end token is just a prefix
|
||||||
|
skipLine
|
||||||
|
| hasTrailer ->
|
||||||
|
error "ShellCheck bug, please report (here doc trailer)."
|
||||||
|
|
||||||
|
-- The following cases assume no trailing text:
|
||||||
|
| dashed == Undashed && (not $ null leadingSpace) -> do
|
||||||
|
parseProblemAt leadingSpacePos ErrorC 1039 "Remove indentation before end token (or use <<- and indent with tabs)."
|
||||||
|
foundCause
|
||||||
|
| dashed == Dashed && not leadingSpacesAreTabs -> do
|
||||||
|
parseProblemAt leadingSpacePos ErrorC 1040 "When using <<-, you can only indent with tabs."
|
||||||
|
foundCause
|
||||||
|
| True -> skipLine
|
||||||
|
|
||||||
rawLine = do
|
rawLine = do
|
||||||
c <- many $ noneOf "\n"
|
c <- many $ noneOf "\n"
|
||||||
void (char '\n') <|> eof
|
void (char '\n') <|> eof
|
||||||
return $ c ++ "\n"
|
return c
|
||||||
|
|
||||||
parseHereData Quoted startPos hereData = do
|
parseHereData Quoted startPos hereData = do
|
||||||
id <- getNextIdAt startPos
|
id <- getNextIdAt startPos
|
||||||
@@ -1694,20 +1738,13 @@ readPendingHereDocs = do
|
|||||||
parseHereData Unquoted startPos hereData =
|
parseHereData Unquoted startPos hereData =
|
||||||
subParse startPos readHereData hereData
|
subParse startPos readHereData hereData
|
||||||
|
|
||||||
readHereData = many $ try doubleQuotedPart <|> readHereLiteral
|
readHereData = many $ doubleQuotedPart <|> readHereLiteral
|
||||||
|
|
||||||
readHereLiteral = do
|
readHereLiteral = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
chars <- many1 $ noneOf "`$\\"
|
chars <- many1 $ noneOf "`$\\"
|
||||||
return $ T_Literal id chars
|
return $ T_Literal id chars
|
||||||
|
|
||||||
verifyHereDoc dashed quoted spacing hereInfo = do
|
|
||||||
when (dashed == Undashed && spacing /= "") $
|
|
||||||
parseNote ErrorC 1039 "Use <<- instead of << if you want to indent the end token."
|
|
||||||
when (dashed == Dashed && filter (/= '\t') spacing /= "" ) $
|
|
||||||
parseNote ErrorC 1040 "When using <<-, you can only indent with tabs."
|
|
||||||
return ()
|
|
||||||
|
|
||||||
debugHereDoc pos endToken doc
|
debugHereDoc pos endToken doc
|
||||||
| endToken `isInfixOf` doc =
|
| endToken `isInfixOf` doc =
|
||||||
let lookAt line = when (endToken `isInfixOf` line) $
|
let lookAt line = when (endToken `isInfixOf` line) $
|
||||||
@@ -1727,8 +1764,15 @@ readIoFileOp = choice [g_DGREAT, g_LESSGREAT, g_GREATAND, g_LESSAND, g_CLOBBER,
|
|||||||
readIoDuplicate = try $ do
|
readIoDuplicate = try $ do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
op <- g_GREATAND <|> g_LESSAND
|
op <- g_GREATAND <|> g_LESSAND
|
||||||
target <- readIoVariable <|> many1 digit <|> string "-"
|
target <- readIoVariable <|> digitsAndOrDash
|
||||||
return $ T_IoDuplicate id op target
|
return $ T_IoDuplicate id op target
|
||||||
|
where
|
||||||
|
-- either digits with optional dash, or a required dash
|
||||||
|
digitsAndOrDash = do
|
||||||
|
str <- many digit
|
||||||
|
dash <- (if null str then id else option "") $ string "-"
|
||||||
|
return $ str ++ dash
|
||||||
|
|
||||||
|
|
||||||
prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\""
|
prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\""
|
||||||
readIoFile = called "redirection" $ do
|
readIoFile = called "redirection" $ do
|
||||||
@@ -1755,6 +1799,7 @@ prop_readIoRedirect3 = isOk readIoRedirect "4>&-"
|
|||||||
prop_readIoRedirect4 = isOk readIoRedirect "&> lol"
|
prop_readIoRedirect4 = isOk readIoRedirect "&> lol"
|
||||||
prop_readIoRedirect5 = isOk readIoRedirect "{foo}>&2"
|
prop_readIoRedirect5 = isOk readIoRedirect "{foo}>&2"
|
||||||
prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-"
|
prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-"
|
||||||
|
prop_readIoRedirect7 = isOk readIoRedirect "{foo}>&1-"
|
||||||
readIoRedirect = do
|
readIoRedirect = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
n <- readIoSource
|
n <- readIoSource
|
||||||
@@ -1838,17 +1883,26 @@ prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)"
|
|||||||
prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi"
|
prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi"
|
||||||
prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"
|
prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"
|
||||||
prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls"
|
prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls"
|
||||||
|
prop_readSimpleCommand8 = isWarning readSimpleCommand "// Lol"
|
||||||
|
prop_readSimpleCommand9 = isWarning readSimpleCommand "/* Lolbert */"
|
||||||
|
prop_readSimpleCommand10 = isWarning readSimpleCommand "/**** Lolbert */"
|
||||||
|
prop_readSimpleCommand11 = isOk readSimpleCommand "/\\* foo"
|
||||||
|
prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo"
|
||||||
|
prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo"
|
||||||
|
prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]"
|
||||||
readSimpleCommand = called "simple command" $ do
|
readSimpleCommand = called "simple command" $ do
|
||||||
pos <- getPosition
|
|
||||||
id1 <- getNextId
|
id1 <- getNextId
|
||||||
id2 <- getNextId
|
id2 <- getNextId
|
||||||
prefix <- option [] readCmdPrefix
|
prefix <- option [] readCmdPrefix
|
||||||
skipAnnotationAndWarn
|
skipAnnotationAndWarn
|
||||||
cmd <- option Nothing $ do { f <- readCmdName; return $ Just f; }
|
pos <- getPosition
|
||||||
|
cmd <- option Nothing $ Just <$> readCmdName
|
||||||
when (null prefix && isNothing cmd) $ fail "Expected a command"
|
when (null prefix && isNothing cmd) $ fail "Expected a command"
|
||||||
|
|
||||||
case cmd of
|
case cmd of
|
||||||
Nothing -> return $ makeSimpleCommand id1 id2 prefix [] []
|
Nothing -> return $ makeSimpleCommand id1 id2 prefix [] []
|
||||||
Just cmd -> do
|
Just cmd -> do
|
||||||
|
validateCommand pos cmd
|
||||||
suffix <- option [] $ getParser readCmdSuffix cmd [
|
suffix <- option [] $ getParser readCmdSuffix cmd [
|
||||||
(["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),
|
(["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),
|
||||||
(["time"], readTimeSuffix),
|
(["time"], readTimeSuffix),
|
||||||
@@ -1869,6 +1923,22 @@ readSimpleCommand = called "simple command" $ do
|
|||||||
then action
|
then action
|
||||||
else getParser def cmd rest
|
else getParser def cmd rest
|
||||||
|
|
||||||
|
cStyleComment cmd =
|
||||||
|
case cmd of
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
validateCommand pos cmd =
|
||||||
|
case cmd of
|
||||||
|
(T_NormalWord _ [T_Literal _ "//"]) -> commentWarning pos
|
||||||
|
(T_NormalWord _ (T_Literal _ "/" : T_Glob _ "*" :_)) -> commentWarning pos
|
||||||
|
(T_NormalWord _ (T_Literal _ str:_)) -> do
|
||||||
|
let cmd = map toLower $ takeWhile isAlpha str
|
||||||
|
when (cmd `elem` ["elsif", "elseif"]) $
|
||||||
|
parseProblemAt pos ErrorC 1131 "Use 'elif' to start another branch."
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
commentWarning pos =
|
||||||
|
parseProblemAt pos ErrorC 1127 "Was this intended as a comment? Use # in sh."
|
||||||
|
|
||||||
readSource :: Monad m => SourcePos -> Token -> SCParser m Token
|
readSource :: Monad m => SourcePos -> Token -> SCParser m Token
|
||||||
readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do
|
readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do
|
||||||
@@ -1902,11 +1972,12 @@ readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do
|
|||||||
"Not following: " ++ err
|
"Not following: " ++ err
|
||||||
return t
|
return t
|
||||||
Right script -> do
|
Right script -> do
|
||||||
id <- getNextIdAt pos
|
id1 <- getNextIdAt pos
|
||||||
|
id2 <- getNextIdAt pos
|
||||||
|
|
||||||
let included = do
|
let included = do
|
||||||
src <- subRead filename script
|
src <- subRead filename script
|
||||||
return $ T_Include id t src
|
return $ T_SourceCommand id1 t (T_Include id2 src)
|
||||||
|
|
||||||
let failed = do
|
let failed = do
|
||||||
parseNoteAt pos WarningC 1094
|
parseNoteAt pos WarningC 1094
|
||||||
@@ -2035,7 +2106,7 @@ skipAnnotationAndWarn = optional $ do
|
|||||||
prop_readIfClause = isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi"
|
prop_readIfClause = isOk readIfClause "if false; then foo; elif true; then stuff; more stuff; else cows; fi"
|
||||||
prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi"
|
prop_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi"
|
||||||
prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi"
|
prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi"
|
||||||
prop_readIfClause4 = isWarning readIfClause "if false; then true; else if true; then echo lol; fi"
|
prop_readIfClause4 = isWarning readIfClause "if false; then true; else if true; then echo lol; fi; fi"
|
||||||
prop_readIfClause5 = isOk readIfClause "if false; then true; else\nif true; then echo lol; fi; fi"
|
prop_readIfClause5 = isOk readIfClause "if false; then true; else\nif true; then echo lol; fi; fi"
|
||||||
readIfClause = called "if expression" $ do
|
readIfClause = called "if expression" $ do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
@@ -2080,12 +2151,9 @@ readIfPart = do
|
|||||||
|
|
||||||
readElifPart = called "elif clause" $ do
|
readElifPart = called "elif clause" $ do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
correctElif <- elif
|
g_Elif
|
||||||
unless correctElif $
|
|
||||||
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)."
|
|
||||||
allspacing
|
allspacing
|
||||||
condition <- readTerm
|
condition <- readTerm
|
||||||
|
|
||||||
ifNextToken (g_Fi <|> g_Elif <|> g_Else) $
|
ifNextToken (g_Fi <|> g_Elif <|> g_Else) $
|
||||||
parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?"
|
parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?"
|
||||||
|
|
||||||
@@ -2095,13 +2163,14 @@ readElifPart = called "elif clause" $ do
|
|||||||
verifyNotEmptyIf "then"
|
verifyNotEmptyIf "then"
|
||||||
action <- readTerm
|
action <- readTerm
|
||||||
return (condition, action)
|
return (condition, action)
|
||||||
where
|
|
||||||
elif = (g_Elif >> return True) <|>
|
|
||||||
try (g_Else >> g_If >> return False)
|
|
||||||
|
|
||||||
readElsePart = called "else clause" $ do
|
readElsePart = called "else clause" $ do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
g_Else
|
g_Else
|
||||||
|
optional $ do
|
||||||
|
try . lookAhead $ g_If
|
||||||
|
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)."
|
||||||
|
|
||||||
acceptButWarn g_Semi ErrorC 1053 "Semicolons directly after 'else' are not allowed. Just remove it."
|
acceptButWarn g_Semi ErrorC 1053 "Semicolons directly after 'else' are not allowed. Just remove it."
|
||||||
allspacing
|
allspacing
|
||||||
verifyNotEmptyIf "else"
|
verifyNotEmptyIf "else"
|
||||||
@@ -2590,11 +2659,14 @@ tryParseWordToken keyword t = try $ do
|
|||||||
str <- anycaseString keyword
|
str <- anycaseString keyword
|
||||||
|
|
||||||
optional $ do
|
optional $ do
|
||||||
try . lookAhead $ char '['
|
c <- try . lookAhead $ anyChar
|
||||||
parseProblem ErrorC 1069 "You need a space before the [."
|
let warning code = parseProblem ErrorC code $ "You need a space before the " ++ [c] ++ "."
|
||||||
optional $ do
|
case c of
|
||||||
try . lookAhead $ char '#'
|
'[' -> warning 1069
|
||||||
parseProblem ErrorC 1099 "You need a space before the #."
|
'#' -> warning 1099
|
||||||
|
'!' -> warning 1129
|
||||||
|
':' -> warning 1130
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
lookAhead keywordSeparator
|
lookAhead keywordSeparator
|
||||||
when (str /= keyword) $
|
when (str /= keyword) $
|
||||||
@@ -2666,20 +2738,23 @@ prop_readShebang1 = isOk readShebang "#!/bin/sh\n"
|
|||||||
prop_readShebang2 = isWarning readShebang "!# /bin/sh\n"
|
prop_readShebang2 = isWarning readShebang "!# /bin/sh\n"
|
||||||
prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n"
|
prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n"
|
||||||
prop_readShebang4 = isWarning readShebang "! /bin/sh"
|
prop_readShebang4 = isWarning readShebang "! /bin/sh"
|
||||||
|
prop_readShebang5 = isWarning readShebang "\n#!/bin/sh"
|
||||||
|
prop_readShebang6 = isWarning readShebang " # Copyright \n!#/bin/bash"
|
||||||
|
prop_readShebang7 = isNotOk readShebang "# Copyright \nfoo\n#!/bin/bash"
|
||||||
readShebang = do
|
readShebang = do
|
||||||
choice $ map try [
|
anyShebang <|> try readMissingBang <|> withHeader
|
||||||
readCorrect,
|
|
||||||
readSwapped,
|
|
||||||
readTooManySpaces,
|
|
||||||
readMissingHash,
|
|
||||||
readMissingBang
|
|
||||||
]
|
|
||||||
many linewhitespace
|
many linewhitespace
|
||||||
str <- many $ noneOf "\r\n"
|
str <- many $ noneOf "\r\n"
|
||||||
optional carriageReturn
|
optional carriageReturn
|
||||||
optional linefeed
|
optional linefeed
|
||||||
return str
|
return str
|
||||||
where
|
where
|
||||||
|
anyShebang = choice $ map try [
|
||||||
|
readCorrect,
|
||||||
|
readSwapped,
|
||||||
|
readTooManySpaces,
|
||||||
|
readMissingHash
|
||||||
|
]
|
||||||
readCorrect = void $ string "#!"
|
readCorrect = void $ string "#!"
|
||||||
|
|
||||||
readSwapped = do
|
readSwapped = do
|
||||||
@@ -2688,7 +2763,7 @@ readShebang = do
|
|||||||
parseProblemAt pos ErrorC 1084
|
parseProblemAt pos ErrorC 1084
|
||||||
"Use #!, not !#, for the shebang."
|
"Use #!, not !#, for the shebang."
|
||||||
|
|
||||||
skipSpaces = liftM (not . null) $ many linewhitespace
|
skipSpaces = fmap (not . null) $ many linewhitespace
|
||||||
readTooManySpaces = do
|
readTooManySpaces = do
|
||||||
startPos <- getPosition
|
startPos <- getPosition
|
||||||
startSpaces <- skipSpaces
|
startSpaces <- skipSpaces
|
||||||
@@ -2717,11 +2792,22 @@ readShebang = do
|
|||||||
parseProblemAt pos ErrorC 1113
|
parseProblemAt pos ErrorC 1113
|
||||||
"Use #!, not just #, for the shebang."
|
"Use #!, not just #, for the shebang."
|
||||||
|
|
||||||
|
|
||||||
ensurePathAhead = lookAhead $ do
|
ensurePathAhead = lookAhead $ do
|
||||||
many linewhitespace
|
many linewhitespace
|
||||||
char '/'
|
char '/'
|
||||||
|
|
||||||
|
withHeader = try $ do
|
||||||
|
many1 headerLine
|
||||||
|
pos <- getPosition
|
||||||
|
anyShebang <*
|
||||||
|
parseProblemAt pos ErrorC 1128 "The shebang must be on the first line. Delete blanks and move comments."
|
||||||
|
|
||||||
|
headerLine = do
|
||||||
|
notFollowedBy2 anyShebang
|
||||||
|
many linewhitespace
|
||||||
|
optional readAnyComment
|
||||||
|
linefeed
|
||||||
|
|
||||||
verifyEof = eof <|> choice [
|
verifyEof = eof <|> choice [
|
||||||
ifParsable g_Lparen $
|
ifParsable g_Lparen $
|
||||||
parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",
|
parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",
|
||||||
@@ -2810,17 +2896,34 @@ readScriptFile = do
|
|||||||
|
|
||||||
readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF"
|
readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF"
|
||||||
|
|
||||||
readScript = do
|
readScript = readScriptFile
|
||||||
script <- readScriptFile
|
|
||||||
reparseIndices script
|
|
||||||
|
|
||||||
|
-- Interactively run a specific parser in ghci:
|
||||||
-- Interactively run a parser in ghci:
|
-- debugParse readSimpleCommand "echo 'hello world'"
|
||||||
-- debugParse readScript "echo 'hello world'"
|
|
||||||
debugParse p string = runIdentity $ do
|
debugParse p string = runIdentity $ do
|
||||||
(res, _) <- runParser testEnvironment p "-" string
|
(res, _) <- runParser testEnvironment p "-" string
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
-- Interactively run the complete parser in ghci:
|
||||||
|
-- debugParseScript "#!/bin/bash\necho 'Hello World'\n"
|
||||||
|
debugParseScript string =
|
||||||
|
result {
|
||||||
|
-- Remove the noisiest parts
|
||||||
|
prTokenPositions = Map.fromList [
|
||||||
|
(Id 0, Position {
|
||||||
|
posFile = "removed for clarity",
|
||||||
|
posLine = -1,
|
||||||
|
posColumn = -1
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
where
|
||||||
|
result = runIdentity $
|
||||||
|
parseScript (mockedSystemInterface []) $ ParseSpec {
|
||||||
|
psFilename = "debug",
|
||||||
|
psScript = string,
|
||||||
|
psCheckSourced = False
|
||||||
|
}
|
||||||
|
|
||||||
testEnvironment =
|
testEnvironment =
|
||||||
Environment {
|
Environment {
|
||||||
systemInterface = (mockedSystemInterface []),
|
systemInterface = (mockedSystemInterface []),
|
||||||
@@ -2912,14 +3015,15 @@ parseShell env name contents = do
|
|||||||
prTokenPositions = Map.empty,
|
prTokenPositions = Map.empty,
|
||||||
prRoot = Nothing
|
prRoot = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
||||||
where
|
where
|
||||||
isName (ContextName _ _) = True
|
isName (ContextName _ _) = True
|
||||||
isName _ = False
|
isName _ = False
|
||||||
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
|
||||||
first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
|
first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
|
||||||
"Couldn't parse this " ++ str ++ "."
|
"Couldn't parse this " ++ str ++ ". Fix to allow more checks."
|
||||||
second (ContextName pos str) = ParseNote pos pos InfoC 1009 $
|
second (ContextName pos str) = ParseNote pos pos InfoC 1009 $
|
||||||
"The mentioned parser error was in this " ++ str ++ "."
|
"The mentioned syntax error was in this " ++ str ++ "."
|
||||||
|
|
||||||
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
|
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
|
||||||
-- depending on declare -A statements.
|
-- depending on declare -A statements.
|
||||||
@@ -2936,6 +3040,9 @@ reparseIndices root =
|
|||||||
return $ T_Array id2 newWords
|
return $ T_Array id2 newWords
|
||||||
x -> return x
|
x -> return x
|
||||||
return $ T_Assignment id mode name newIndices newValue
|
return $ T_Assignment id mode name newIndices newValue
|
||||||
|
f (TA_Variable id name indices) = do
|
||||||
|
newIndices <- mapM (fixAssignmentIndex name) indices
|
||||||
|
return $ TA_Variable id name newIndices
|
||||||
f t = return t
|
f t = return t
|
||||||
|
|
||||||
fixIndexElement name word =
|
fixIndexElement name word =
|
||||||
@@ -2943,13 +3050,13 @@ reparseIndices root =
|
|||||||
T_IndexedElement id indices value -> do
|
T_IndexedElement id indices value -> do
|
||||||
new <- mapM (fixAssignmentIndex name) indices
|
new <- mapM (fixAssignmentIndex name) indices
|
||||||
return $ T_IndexedElement id new value
|
return $ T_IndexedElement id new value
|
||||||
otherwise -> return word
|
_ -> return word
|
||||||
|
|
||||||
fixAssignmentIndex name word =
|
fixAssignmentIndex name word =
|
||||||
case word of
|
case word of
|
||||||
T_UnparsedIndex id pos src -> do
|
T_UnparsedIndex id pos src ->
|
||||||
parsed name pos src
|
parsed name pos src
|
||||||
otherwise -> return word
|
_ -> return word
|
||||||
|
|
||||||
parsed name pos src =
|
parsed name pos src =
|
||||||
if isAssociative name
|
if isAssociative name
|
||||||
@@ -2986,6 +3093,36 @@ parseScript sys spec =
|
|||||||
checkSourced = psCheckSourced spec
|
checkSourced = psCheckSourced spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Same as 'try' but emit syntax errors if the parse fails.
|
||||||
|
tryWithErrors :: Monad m => SCParser m v -> SCParser m v
|
||||||
|
tryWithErrors parser = do
|
||||||
|
userstate <- getState
|
||||||
|
oldContext <- getCurrentContexts
|
||||||
|
input <- getInput
|
||||||
|
pos <- getPosition
|
||||||
|
result <- lift $ runParserT (setPosition pos >> getResult parser) userstate (sourceName pos) input
|
||||||
|
case result of
|
||||||
|
Right (result, endPos, endInput, endState) -> do
|
||||||
|
-- 'many' objects if we don't consume anything at all, so read a dummy value
|
||||||
|
void anyChar <|> eof
|
||||||
|
putState endState
|
||||||
|
setPosition endPos
|
||||||
|
setInput endInput
|
||||||
|
return result
|
||||||
|
|
||||||
|
Left err -> do
|
||||||
|
newContext <- getCurrentContexts
|
||||||
|
addParseProblem $ makeErrorFor err
|
||||||
|
mapM_ addParseProblem $ notesForContext newContext
|
||||||
|
setCurrentContexts oldContext
|
||||||
|
fail ""
|
||||||
|
where
|
||||||
|
getResult p = do
|
||||||
|
result <- p
|
||||||
|
endPos <- getPosition
|
||||||
|
endInput <- getInput
|
||||||
|
endState <- getState
|
||||||
|
return (result, endPos, endInput, endState)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2012-2015 Vidar Holen
|
Copyright 2012-2015 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
http://www.vidarholen.net/contents/shellcheck
|
https://www.shellcheck.net
|
||||||
|
|
||||||
ShellCheck is free software: you can redistribute it and/or modify
|
ShellCheck is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
@@ -1,5 +1,5 @@
|
|||||||
# This file was automatically generated by stack init
|
# This file was automatically generated by stack init
|
||||||
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
|
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||||
|
|
||||||
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
||||||
resolver: lts-8.5
|
resolver: lts-8.5
|
||||||
|
77
striptests
Executable file
77
striptests
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# This file strips all unit tests from ShellCheck, removing
|
||||||
|
# the dependency on QuickCheck and Template Haskell and
|
||||||
|
# reduces the binary size considerably.
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
sponge() {
|
||||||
|
data="$(cat)"
|
||||||
|
printf '%s\n' "$data" > "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
modify() {
|
||||||
|
if ! "${@:2}" < "$1" | sponge "$1"
|
||||||
|
then
|
||||||
|
{
|
||||||
|
printf 'Failed to modify %s: ' "$1"
|
||||||
|
printf '%q ' "${@:2}"
|
||||||
|
printf '\n'
|
||||||
|
} >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detestify() {
|
||||||
|
echo "-- AUTOGENERATED from ShellCheck by striptests. Do not modify."
|
||||||
|
awk '
|
||||||
|
BEGIN {
|
||||||
|
state = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/LANGUAGE TemplateHaskell/ { next; }
|
||||||
|
/^import.*Test\./ { next; }
|
||||||
|
|
||||||
|
/^module/ {
|
||||||
|
sub(/,[^,)]*runTests/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
# Delete tests
|
||||||
|
/^prop_/ { state = 1; next; }
|
||||||
|
|
||||||
|
# ..and any blank lines following them.
|
||||||
|
state == 1 && /^ / { next; }
|
||||||
|
|
||||||
|
# Template Haskell marker
|
||||||
|
/^return / {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
{ state = 0; print; }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if [[ ! -e ShellCheck.cabal ]]
|
||||||
|
then
|
||||||
|
echo "Run me from the ShellCheck directory." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1
|
||||||
|
then
|
||||||
|
echo "You have local changes! These may be overwritten." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
modify ShellCheck.cabal sed -e '
|
||||||
|
/QuickCheck/d
|
||||||
|
/^test-suite/{ s/.*//; q; }
|
||||||
|
'
|
||||||
|
|
||||||
|
find . -name '.git' -prune -o -type f -name '*.hs' -print |
|
||||||
|
while IFS= read -r file
|
||||||
|
do
|
||||||
|
modify "$file" detestify
|
||||||
|
done
|
||||||
|
|
Reference in New Issue
Block a user