mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +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 ###
|
||||
dist
|
||||
@@ -13,3 +13,10 @@ cabal-dev
|
||||
cabal.sandbox.config
|
||||
cabal.config
|
||||
.stack-work
|
||||
|
||||
### Snap ###
|
||||
/snap/.snapcraft/
|
||||
/stage/
|
||||
/parts/
|
||||
/prime/
|
||||
*.snap
|
||||
|
@@ -7,7 +7,7 @@ cd deploy
|
||||
cp ../LICENSE LICENSE.txt
|
||||
sed -e $'s/$/\r/' > README.txt << END
|
||||
This is a precompiled ShellCheck binary.
|
||||
http://www.shellcheck.net/
|
||||
https://www.shellcheck.net/
|
||||
|
||||
ShellCheck is a static analysis tool for shell scripts.
|
||||
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=""
|
||||
- TAGS=""
|
||||
- test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
|
||||
- test -n "$TRAVIS_TAG" && TAGS="$TAGS $TRAVIS_TAG" || true
|
||||
- test "$TRAVIS_BRANCH" = master && test -n "$TRAVIS_TAG" && TAGS="$TAGS stable" || true
|
||||
- test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
|
||||
|
||||
script:
|
||||
- mkdir deploy
|
||||
# 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
|
||||
# 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
|
||||
# Remove all tests to reduce binary size
|
||||
- ./striptests
|
||||
# Linux Docker image
|
||||
- name="$DOCKER_BASE"
|
||||
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
|
||||
- docker build -t "$name:current" .
|
||||
- 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
|
||||
- name="$DOCKER_BASE-alpine"
|
||||
- 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 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
|
||||
- ./.prepare_deploy
|
||||
|
||||
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;
|
||||
do
|
||||
for tag in $TAGS;
|
||||
do
|
||||
echo "Deploying $repo:current as $repo:$tag...";
|
||||
docker tag "$repo:current" "$repo:$tag";
|
||||
docker push "$repo:$tag";
|
||||
docker tag "$repo:current" "$repo:$tag" || exit 1;
|
||||
docker push "$repo:$tag" || exit 1;
|
||||
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
|
||||
### Added
|
||||
- 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>"
|
||||
COPY --from=build /out /
|
||||
|
||||
# This file assumes ShellCheck has already been built.
|
||||
# See https://github.com/koalaman/scbuilder
|
||||
COPY shellcheck /bin/shellcheck
|
||||
# DELETE-MARKER (Remove everything below to keep the alpine image)
|
||||
|
||||
# Resulting ShellCheck image
|
||||
FROM scratch
|
||||
LABEL maintainer="Vidar Holen <vidar@vidarholen.net>"
|
||||
WORKDIR /mnt
|
||||
COPY --from=build /out /
|
||||
ENTRYPOINT ["/bin/shellcheck"]
|
||||
|
8
LICENSE
8
LICENSE
@@ -1,7 +1,7 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
<http://www.gnu.org/licenses/>.
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
85
README.md
85
README.md
@@ -1,3 +1,5 @@
|
||||
[](https://travis-ci.org/koalaman/shellcheck)
|
||||
|
||||
# ShellCheck - A shell script static analysis tool
|
||||
|
||||
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 build or test suites](#in-your-build-or-test-suites)
|
||||
- [Installing](#installing)
|
||||
- [Travis CI Setup](#travis-ci-setup)
|
||||
- [Travis CI](#travis-ci)
|
||||
- [Compiling from source](#compiling-from-source)
|
||||
- [Installing Cabal](#installing-cabal)
|
||||
- [Compiling ShellCheck](#compiling-shellcheck)
|
||||
@@ -52,9 +54,9 @@ There are a number of ways to use ShellCheck!
|
||||
|
||||
### 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
|
||||
|
||||
@@ -99,7 +101,7 @@ On systems with Stack (installs to `~/.local/bin`):
|
||||
|
||||
stack update
|
||||
stack install ShellCheck
|
||||
|
||||
|
||||
On Debian based distros:
|
||||
|
||||
apt-get install shellcheck
|
||||
@@ -108,6 +110,8 @@ On Arch Linux based distros:
|
||||
|
||||
pacman -S shellcheck
|
||||
|
||||
or get the dependency free [shellcheck-static](https://aur.archlinux.org/packages/shellcheck-static/) from the AUR.
|
||||
|
||||
On Gentoo based distros:
|
||||
|
||||
emerge --ask shellcheck
|
||||
@@ -121,23 +125,16 @@ On Fedora based distros:
|
||||
|
||||
dnf install ShellCheck
|
||||
|
||||
On FreeBSD:
|
||||
|
||||
pkg install hs-ShellCheck
|
||||
|
||||
On OS X with homebrew:
|
||||
|
||||
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
|
||||
|
||||
Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||
@@ -145,37 +142,48 @@ Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||
On Solus:
|
||||
|
||||
eopkg install shellcheck
|
||||
|
||||
On Windows (via [scoop](http://scoop.sh)):
|
||||
|
||||
scoop install shellcheck
|
||||
|
||||
From Snap Store:
|
||||
|
||||
snap install --channel=edge shellcheck
|
||||
|
||||
From Docker Hub:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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)
|
||||
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-latest.zip)
|
||||
* [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-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
|
||||
language: bash
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- debian-sid # Grab ShellCheck from the Debian repo
|
||||
packages:
|
||||
- shellcheck
|
||||
```
|
||||
If you still want to do so in order to upgrade at your leisure or ensure the latest release:
|
||||
|
||||
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
|
||||
|
||||
@@ -293,6 +301,7 @@ alias archive='mv $1 /backup' # Defining aliases with arguments
|
||||
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
|
||||
exec foo; echo "Done!" # Misused 'exec'
|
||||
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
|
||||
# find . -exec foo > bar \; # Redirections in find
|
||||
f() { whoami; }; sudo f # External use of internal functions
|
||||
```
|
||||
|
||||
@@ -308,9 +317,13 @@ var$n="Hello" # Wrong indirect assignment
|
||||
echo ${var$n} # Wrong indirect reference
|
||||
var=(1, 2, 3) # Comma separated arrays
|
||||
array=( [index] = value ) # Incorrect index initialization
|
||||
echo $var[14] # Missing {} in array references
|
||||
echo "Argument 10 is $10" # Positional parameter misreference
|
||||
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||
else if othercondition; then .. # Using 'else if'
|
||||
f; f() { echo "hello world; } # Using function before definition
|
||||
[ false ] # 'false' being true
|
||||
if ( -f file ) # Using (..) instead of test
|
||||
```
|
||||
|
||||
### Style
|
||||
@@ -341,6 +354,8 @@ printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
|
||||
var=World; echo "Hello " var # Unused lowercase variables
|
||||
echo "Hello $name" # Unassigned lowercase variables
|
||||
cmd | read bar; echo $bar # Assignments in subshells
|
||||
cat foo | cp bar # Piping to commands that don't read
|
||||
printf '%s: %s\n' foo # Mismatches in printf argument count
|
||||
```
|
||||
|
||||
### Robustness
|
||||
@@ -354,6 +369,7 @@ find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
|
||||
printf "Hello $name" # Variables in printf format
|
||||
for f in $(ls *.txt); do # Iterating over ls output
|
||||
export MYVAR=$(cmd) # Masked exit codes
|
||||
case $version in 2.*) :;; 2.6.*) # Shadowed case branches
|
||||
```
|
||||
|
||||
### Portability
|
||||
@@ -388,6 +404,7 @@ var=42 echo $var # Expansion of inlined environment
|
||||
echo $((n/180*100)) # Unnecessary loss of precision
|
||||
ls *[:digit:].txt # Bad character class globs
|
||||
sed 's/foo/bar/' file > file # Redirecting to input
|
||||
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
|
||||
```
|
||||
|
||||
## 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).
|
||||
|
||||
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
|
||||
Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
|
||||
|
||||
Happy ShellChecking!
|
||||
|
@@ -1,12 +1,12 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.4.7
|
||||
Version: 0.5.0
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
Category: Static Analysis
|
||||
Author: Vidar Holen
|
||||
Maintainer: vidar@vidarholen.net
|
||||
Homepage: http://www.shellcheck.net/
|
||||
Homepage: https://www.shellcheck.net/
|
||||
Build-Type: Custom
|
||||
Cabal-Version: >= 1.8
|
||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||
@@ -31,16 +31,29 @@ Extra-Source-Files:
|
||||
-- tests
|
||||
test/shellcheck.hs
|
||||
|
||||
custom-setup
|
||||
setup-depends:
|
||||
base >= 4 && <5,
|
||||
process >= 1.0 && <1.7,
|
||||
Cabal >= 1.10 && <2.3
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/koalaman/shellcheck.git
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
if impl(ghc < 8.0)
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
base >= 4 && < 5,
|
||||
containers,
|
||||
-- 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,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
@@ -69,27 +82,34 @@ library
|
||||
Paths_ShellCheck
|
||||
|
||||
executable shellcheck
|
||||
if impl(ghc < 8.0)
|
||||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
aeson,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
ShellCheck,
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4
|
||||
parsec >= 3.0,
|
||||
QuickCheck >= 2.7.4,
|
||||
regex-tdfa
|
||||
main-is: shellcheck.hs
|
||||
|
||||
test-suite test-shellcheck
|
||||
type: exitcode-stdio-1.0
|
||||
build-depends:
|
||||
aeson,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
ShellCheck,
|
||||
containers,
|
||||
directory,
|
||||
json,
|
||||
mtl >= 2.2.1,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4
|
||||
QuickCheck >= 2.7.4,
|
||||
regex-tdfa
|
||||
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
|
||||
|
||||
shopt -s globstar
|
||||
if ! shopt -s globstar
|
||||
then
|
||||
echo "Error: This script depends on Bash 4." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in 1 2
|
||||
do
|
||||
|
2
quickrun
2
quickrun
@@ -2,4 +2,4 @@
|
||||
# quickrun runs ShellCheck in an interpreted mode.
|
||||
# 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 2012-2015, Vidar Holen.
|
||||
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
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,37 +15,38 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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.Interface
|
||||
import ShellCheck.Regex
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
import ShellCheck.Formatter.Format
|
||||
import qualified ShellCheck.Formatter.CheckStyle
|
||||
import ShellCheck.Formatter.Format
|
||||
import qualified ShellCheck.Formatter.GCC
|
||||
import qualified ShellCheck.Formatter.JSON
|
||||
import qualified ShellCheck.Formatter.TTY
|
||||
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Prelude hiding (catch)
|
||||
import System.Console.GetOpt
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.IO
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.IORef
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Data.Semigroup (Semigroup (..))
|
||||
import Prelude hiding (catch)
|
||||
import System.Console.GetOpt
|
||||
import System.Directory
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.IO
|
||||
|
||||
data Flag = Flag String String
|
||||
data Status =
|
||||
@@ -56,13 +57,16 @@ data Status =
|
||||
| RuntimeException
|
||||
deriving (Ord, Eq, Show)
|
||||
|
||||
instance Semigroup Status where
|
||||
(<>) = max
|
||||
|
||||
instance Monoid Status where
|
||||
mempty = NoProblems
|
||||
mappend = max
|
||||
mappend = (Data.Semigroup.<>)
|
||||
|
||||
data Options = Options {
|
||||
checkSpec :: CheckSpec,
|
||||
externalSources :: Bool,
|
||||
checkSpec :: CheckSpec,
|
||||
externalSources :: Bool,
|
||||
formatterOptions :: FormatterOptions
|
||||
}
|
||||
|
||||
@@ -117,9 +121,9 @@ formatList = intercalate ", " names
|
||||
where
|
||||
names = Map.keys $ formats (formatterOptions defaultOptions)
|
||||
|
||||
getOption [] _ = Nothing
|
||||
getOption [] _ = Nothing
|
||||
getOption (Flag var val:_) name | name == var = return val
|
||||
getOption (_:rest) flag = getOption rest flag
|
||||
getOption (_:rest) flag = getOption rest flag
|
||||
|
||||
getOptions options name =
|
||||
map (\(Flag _ val) -> val) . filter (\(Flag var _) -> var == name) $ options
|
||||
@@ -159,10 +163,10 @@ main = do
|
||||
|
||||
statusToCode status =
|
||||
case status of
|
||||
NoProblems -> ExitSuccess
|
||||
SomeProblems -> ExitFailure 1
|
||||
SyntaxFailure -> ExitFailure 3
|
||||
SupportFailure -> ExitFailure 4
|
||||
NoProblems -> ExitSuccess
|
||||
SomeProblems -> ExitFailure 1
|
||||
SyntaxFailure -> ExitFailure 3
|
||||
SupportFailure -> ExitFailure 4
|
||||
RuntimeException -> ExitFailure 2
|
||||
|
||||
process :: [Flag] -> [FilePath] -> ExceptT Status IO Status
|
||||
@@ -203,7 +207,7 @@ runFormatter sys format options files = do
|
||||
|
||||
process :: FilePath -> IO Status
|
||||
process filename = do
|
||||
input <- (siReadFile sys) filename
|
||||
input <- siReadFile sys filename
|
||||
either (reportFailure filename) check input
|
||||
where
|
||||
check contents = do
|
||||
@@ -220,10 +224,10 @@ runFormatter sys format options files = do
|
||||
|
||||
parseColorOption colorOption =
|
||||
case colorOption of
|
||||
"auto" -> ColorAuto
|
||||
"auto" -> ColorAuto
|
||||
"always" -> ColorAlways
|
||||
"never" -> ColorNever
|
||||
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
|
||||
"never" -> ColorNever
|
||||
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
|
||||
|
||||
parseOption flag options =
|
||||
case flag of
|
||||
@@ -292,7 +296,7 @@ ioInterface options files = do
|
||||
get cache inputs file = do
|
||||
map <- readIORef cache
|
||||
case Map.lookup file map of
|
||||
Just x -> return $ Right x
|
||||
Just x -> return $ Right x
|
||||
Nothing -> fetch cache inputs file
|
||||
|
||||
fetch cache inputs file = do
|
||||
@@ -355,7 +359,7 @@ decodeString = decode
|
||||
in
|
||||
case next of
|
||||
Just (n, remainder) -> chr n : decode remainder
|
||||
Nothing -> c : decode rest
|
||||
Nothing -> c : decode rest
|
||||
|
||||
construct x 0 rest = do
|
||||
guard $ x <= 0x10FFFF
|
||||
@@ -378,4 +382,4 @@ printVersion = do
|
||||
putStrLn "ShellCheck - shell script analysis tool"
|
||||
putStrLn $ "version: " ++ shellcheckVersion
|
||||
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
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.AST where
|
||||
|
||||
@@ -37,8 +37,8 @@ newtype Root = Root Token
|
||||
data Token =
|
||||
TA_Binary Id String Token Token
|
||||
| TA_Assignment Id String Token Token
|
||||
| TA_Variable Id String [Token]
|
||||
| TA_Expansion Id [Token]
|
||||
| TA_Index Id Token
|
||||
| TA_Sequence Id [Token]
|
||||
| TA_Trinary Id Token Token Token
|
||||
| TA_Unary Id String Token
|
||||
@@ -134,7 +134,8 @@ data Token =
|
||||
| T_Pipe Id String
|
||||
| T_CoProc Id (Maybe String) 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)
|
||||
|
||||
data Annotation =
|
||||
@@ -266,11 +267,12 @@ analyze f g i =
|
||||
c <- round t3
|
||||
return $ TA_Trinary id a b c
|
||||
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_CoProc id var body) = d1 body $ T_CoProc id var
|
||||
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
|
||||
|
||||
getId :: Token -> Id
|
||||
@@ -360,7 +362,6 @@ getId t = case t of
|
||||
TA_Sequence id _ -> id
|
||||
TA_Trinary id _ _ _ -> id
|
||||
TA_Expansion id _ -> id
|
||||
TA_Index id _ -> id
|
||||
T_ProcSub id _ _ -> id
|
||||
T_Glob id _ -> id
|
||||
T_ForArithmetic id _ _ _ _ -> id
|
||||
@@ -371,9 +372,11 @@ getId t = case t of
|
||||
T_Pipe id _ -> id
|
||||
T_CoProc id _ _ -> id
|
||||
T_CoProcBody id _ -> id
|
||||
T_Include id _ _ -> id
|
||||
T_Include id _ -> id
|
||||
T_SourceCommand id _ _ -> id
|
||||
T_UnparsedIndex id _ _ -> id
|
||||
TC_Empty id _ -> id
|
||||
TA_Variable id _ _ -> id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.ASTLib where
|
||||
|
||||
@@ -112,6 +112,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||
|
||||
-- Get all flags in a GNU way, up until --
|
||||
getAllFlags :: Token -> [(Token, String)]
|
||||
getAllFlags = getFlagsUntil (== "--")
|
||||
-- Get all flags in a BSD way, up until first non-flag argument or --
|
||||
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
||||
@@ -260,7 +261,7 @@ getCommand t =
|
||||
getCommandName t = do
|
||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||
s <- getLiteralString w
|
||||
if "busybox" `isSuffixOf` s
|
||||
if "busybox" `isSuffixOf` s || "builtin" == s
|
||||
then
|
||||
case rest of
|
||||
(applet:_) -> getLiteralString applet
|
||||
@@ -434,3 +435,12 @@ pseudoGlobIsSuperSetof = matchable
|
||||
|
||||
wordsCanBeEqual x y = fromMaybe True $
|
||||
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
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,9 +15,10 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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
|
||||
|
||||
import ShellCheck.AST
|
||||
@@ -119,7 +120,6 @@ nodeChecks = [
|
||||
,checkGlobbedRegex
|
||||
,checkTestRedirects
|
||||
,checkIndirectExpansion
|
||||
,checkSudoRedirect
|
||||
,checkPS1Assignments
|
||||
,checkBackticks
|
||||
,checkInexplicablyUnquoted
|
||||
@@ -166,6 +166,8 @@ nodeChecks = [
|
||||
,checkFlagAsCommand
|
||||
,checkEmptyCondition
|
||||
,checkPipeToNowhere
|
||||
,checkForLoopGlobVariables
|
||||
,checkSubshelledTests
|
||||
]
|
||||
|
||||
|
||||
@@ -572,6 +574,7 @@ prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/
|
||||
prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
|
||||
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
|
||||
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
|
||||
prop_checkRedirectToSame8 = verifyNot checkRedirectToSame "while read -r line; do _=\"$fname\"; done <\"$fname\""
|
||||
checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
|
||||
where
|
||||
@@ -582,7 +585,8 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||
&& x == y
|
||||
&& not (isOutput t && isOutput u)
|
||||
&& not (special t)
|
||||
&& not (any isHarmlessCommand [t,u])) $ do
|
||||
&& not (any isHarmlessCommand [t,u])
|
||||
&& not (any containsAssignment [u])) $ do
|
||||
addComment $ note newId
|
||||
addComment $ note exceptId
|
||||
checkOccurrences _ _ = return ()
|
||||
@@ -609,6 +613,9 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
|
||||
cmd <- getClosestCommand (parentMap params) arg
|
||||
name <- getCommandBasename cmd
|
||||
return $ name `elem` ["echo", "printf", "sponge"]
|
||||
containsAssignment arg = fromMaybe False $ do
|
||||
cmd <- getClosestCommand (parentMap params) arg
|
||||
return $ isAssignment cmd
|
||||
|
||||
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_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
|
||||
prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
|
||||
prop_checkStderrRedirect7 = verifyNot checkStderrRedirect "{ cmd > file; } 2>&1"
|
||||
checkStderrRedirect params redir@(T_Redirecting _ [
|
||||
T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"),
|
||||
T_FdRedirect _ _ (T_IoFile _ op _)
|
||||
@@ -760,7 +768,7 @@ checkStderrRedirect params redir@(T_Redirecting _ [
|
||||
isCaptured = any usesOutput $ getPath (parentMap params) redir
|
||||
|
||||
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 ()
|
||||
|
||||
@@ -786,6 +794,10 @@ prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${
|
||||
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
|
||||
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
|
||||
prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"
|
||||
prop_checkSingleQuotedVariables15= verifyNot checkSingleQuotedVariables "git filter-branch 'test $GIT_COMMIT'"
|
||||
prop_checkSingleQuotedVariables16= verify checkSingleQuotedVariables "git '$a'"
|
||||
prop_checkSingleQuotedVariables17= verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *"
|
||||
|
||||
checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
when (s `matches` re) $
|
||||
if "sed" == commandName
|
||||
@@ -798,7 +810,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
commandName = fromMaybe "" $ do
|
||||
cmd <- getClosestCommand parents t
|
||||
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 =
|
||||
any isOkAssignment (take 3 $ getPath parents t)
|
||||
@@ -813,8 +825,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
,"xprop"
|
||||
,"alias"
|
||||
,"sudo" -- covering "sudo sh" and such
|
||||
,"docker" -- like above
|
||||
,"dpkg-query"
|
||||
,"jq" -- could also check that user provides --arg
|
||||
,"rename"
|
||||
,"unset"
|
||||
,"git filter-branch"
|
||||
]
|
||||
|| "awk" `isSuffixOf` commandName
|
||||
|| "perl" `isPrefixOf` commandName
|
||||
@@ -838,6 +854,12 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
_ -> "find"
|
||||
getFindCommand (T_Redirecting _ _ cmd) = getFindCommand cmd
|
||||
getFindCommand _ = "find"
|
||||
getGitCommand (T_SimpleCommand _ _ words) =
|
||||
case map getLiteralString words of
|
||||
Just "git":Just "filter-branch":_ -> "git filter-branch"
|
||||
_ -> "git"
|
||||
getGitCommand (T_Redirecting _ _ cmd) = getGitCommand cmd
|
||||
getGitCommand _ = "git"
|
||||
checkSingleQuotedVariables _ _ = return ()
|
||||
|
||||
|
||||
@@ -845,6 +867,7 @@ prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
|
||||
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
|
||||
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
|
||||
prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
|
||||
prop_checkUnquotedN5 = verifyNot checkUnquotedN "[ -n \"$@\" ]"
|
||||
checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
|
||||
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
|
||||
checkUnquotedN _ _ = return ()
|
||||
@@ -1140,12 +1163,10 @@ checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
|
||||
T_Arithmetic {} -> return normalWarning
|
||||
T_DollarArithmetic {} -> return normalWarning
|
||||
T_ForArithmetic {} -> return normalWarning
|
||||
TA_Index {} -> return indexWarning
|
||||
T_SimpleCommand {} -> return noWarning
|
||||
_ -> Nothing
|
||||
|
||||
normalWarning = style id 2004 "$/${} is unnecessary on arithmetic variables."
|
||||
indexWarning = style id 2149 "Remove $/${} for numeric index, or escape it for string."
|
||||
noWarning = return ()
|
||||
checkArithmeticDeref _ _ = return ()
|
||||
|
||||
@@ -1276,33 +1297,6 @@ checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
|
||||
_ -> False
|
||||
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_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||
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_checkSpuriousExec6 = verify checkSpuriousExec "exec foo > file; cmd"
|
||||
prop_checkSpuriousExec7 = verifyNot checkSpuriousExec "exec file; echo failed; exit 3"
|
||||
prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.log 2>&1; bar"
|
||||
checkSpuriousExec _ = doLists
|
||||
where
|
||||
doLists (T_Script _ _ cmds) = doList cmds
|
||||
@@ -1617,16 +1612,23 @@ checkSpacefulness params t =
|
||||
modify $ Map.insert name bool
|
||||
|
||||
readF _ token name = do
|
||||
spaced <- hasSpaces name
|
||||
return [makeComment InfoC (getId token) 2086 warning |
|
||||
isExpansion token && spaced
|
||||
spaces <- hasSpaces name
|
||||
return [warning |
|
||||
isExpansion token && spaces
|
||||
&& not (isArrayExpansion token) -- There's another warning for this
|
||||
&& not (isCountingReference token)
|
||||
&& not (isQuoteFree parents token)
|
||||
&& not (isQuotedAlternativeReference token)
|
||||
&& not (usedAsCommandName parents token)]
|
||||
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 SourceInteger) = setSpaces name False >> return []
|
||||
@@ -1665,6 +1667,12 @@ checkSpacefulness params t =
|
||||
globspace = "*?[] \t\n"
|
||||
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_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; 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_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"
|
||||
prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"
|
||||
prop_checkUnused38= verifyTree checkUnusedAssignments "(( a=42 ))"
|
||||
prop_checkUnused39= verifyNotTree checkUnusedAssignments "declare -x -f foo"
|
||||
prop_checkUnused40= verifyNotTree checkUnusedAssignments "arr=(1 2); num=2; echo \"${arr[@]:num}\""
|
||||
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||
where
|
||||
flow = variableFlow params
|
||||
@@ -1828,7 +1839,7 @@ checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||
|
||||
warnFor (name, token) =
|
||||
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
|
||||
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_checkUnassignedReferences31= verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"
|
||||
prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi"
|
||||
prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"
|
||||
prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"
|
||||
prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"
|
||||
checkUnassignedReferences params t = warnings
|
||||
where
|
||||
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
||||
@@ -1924,11 +1938,13 @@ checkUnassignedReferences params t = warnings
|
||||
isArray _ = False
|
||||
|
||||
isGuarded (T_DollarBraced _ v) =
|
||||
any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]
|
||||
rest `matches` guardRegex
|
||||
where
|
||||
name = concat $ oversimplify v
|
||||
rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") name
|
||||
isGuarded _ = False
|
||||
-- :? or :- with optional array index and colon
|
||||
guardRegex = mkRegex "^(\\[.*\\])?:?[-?]"
|
||||
|
||||
match var candidate =
|
||||
if var /= candidate && map toLower var == map toLower candidate
|
||||
@@ -2467,6 +2483,7 @@ prop_checkUncheckedCd5 = verifyTree checkUncheckedCdPushdPopd "if true; then cd
|
||||
prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCdPushdPopd "cd .."
|
||||
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCdPushdPopd "#!/bin/bash -e\ncd foo\nrm bar"
|
||||
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCdPushdPopd "set -o errexit; cd foo; rm bar"
|
||||
prop_checkUncheckedCd9 = verifyTree checkUncheckedCdPushdPopd "builtin cd ~/src; rm -r foo"
|
||||
prop_checkUncheckedPushd1 = verifyTree checkUncheckedCdPushdPopd "pushd ~/src; rm -r foo"
|
||||
prop_checkUncheckedPushd2 = verifyNotTree checkUncheckedCdPushdPopd "pushd ~/src || exit; rm -r foo"
|
||||
prop_checkUncheckedPushd3 = verifyNotTree checkUncheckedCdPushdPopd "set -e; pushd ~/src; rm -r foo"
|
||||
@@ -2525,7 +2542,7 @@ checkLoopVariableReassignment params token =
|
||||
T_ForArithmetic _
|
||||
(TA_Sequence _
|
||||
[TA_Assignment _ "="
|
||||
(TA_Expansion _ [T_Literal _ var]) _])
|
||||
(TA_Variable _ var _ ) _])
|
||||
_ _ _ -> return var
|
||||
_ -> 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_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)"
|
||||
prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"
|
||||
prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true"
|
||||
checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity ()
|
||||
checkPipeToNowhere _ t =
|
||||
case t of
|
||||
@@ -2891,5 +2909,77 @@ checkUseBeforeDefinition _ t =
|
||||
then [x]
|
||||
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 []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Analyzer (analyzeScript) where
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,30 +15,31 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.AnalyzerLib where
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Regex
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.State
|
||||
import Control.Monad.Writer
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
import Control.Arrow (first)
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.State
|
||||
import Control.Monad.Writer
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe
|
||||
import Data.Semigroup
|
||||
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
|
||||
|
||||
type Analysis = AnalyzerM ()
|
||||
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
||||
@@ -47,7 +48,7 @@ nullCheck = const $ return ()
|
||||
|
||||
data Checker = Checker {
|
||||
perScript :: Root -> Analysis,
|
||||
perToken :: Token -> Analysis
|
||||
perToken :: Token -> Analysis
|
||||
}
|
||||
|
||||
runChecker :: Parameters -> Checker -> [TokenComment]
|
||||
@@ -57,28 +58,30 @@ runChecker params checker = notes
|
||||
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
|
||||
notes = snd $ evalRWS (check $ Root root) params Cache
|
||||
|
||||
instance Semigroup Checker where
|
||||
(<>) x y = Checker {
|
||||
perScript = perScript x `composeAnalyzers` perScript y,
|
||||
perToken = perToken x `composeAnalyzers` perToken y
|
||||
}
|
||||
|
||||
instance Monoid Checker where
|
||||
mempty = Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = nullCheck
|
||||
}
|
||||
mappend x y = Checker {
|
||||
perScript = perScript x `composeAnalyzers` perScript y,
|
||||
perToken = perToken x `composeAnalyzers` perToken y
|
||||
}
|
||||
|
||||
mappend = (Data.Semigroup.<>)
|
||||
|
||||
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
||||
composeAnalyzers f g x = f x >> g x
|
||||
|
||||
data Parameters = Parameters {
|
||||
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
|
||||
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
|
||||
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
|
||||
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
|
||||
shellType :: Shell, -- The shell type, such as Bash or Ksh
|
||||
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
|
||||
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
|
||||
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
|
||||
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
|
||||
shellType :: Shell, -- The shell type, such as Bash or Ksh
|
||||
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
|
||||
rootNode :: Token -- The root node of the AST
|
||||
rootNode :: Token -- The root node of the AST
|
||||
}
|
||||
|
||||
-- TODO: Cache results of common AST ops here
|
||||
@@ -151,8 +154,8 @@ makeParameters spec =
|
||||
case shellType params of
|
||||
Bash -> containsLastpipe root
|
||||
Dash -> False
|
||||
Sh -> False
|
||||
Ksh -> True,
|
||||
Sh -> False
|
||||
Ksh -> True,
|
||||
|
||||
shellTypeSpecified = isJust $ asShellType spec,
|
||||
parentMap = getParentTree root,
|
||||
@@ -205,7 +208,7 @@ determineShell t = fromMaybe Bash $ do
|
||||
forAnnotation t =
|
||||
case t of
|
||||
(ShellOverride s) -> return s
|
||||
_ -> fail ""
|
||||
_ -> fail ""
|
||||
getCandidates :: Token -> [Maybe String]
|
||||
getCandidates t@T_Script {} = [Just $ fromShebang t]
|
||||
getCandidates (T_Annotation _ annotations s) =
|
||||
@@ -233,7 +236,7 @@ getParentTree t =
|
||||
pre t = modify (first ((:) t))
|
||||
post t = do
|
||||
(_:rest, map) <- get
|
||||
case rest of [] -> put (rest, map)
|
||||
case rest of [] -> put (rest, map)
|
||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||
|
||||
-- Given a root node, make a map from Id to Token
|
||||
@@ -264,27 +267,27 @@ isQuoteFreeNode strict tree t =
|
||||
case t of
|
||||
T_Assignment {} -> return True
|
||||
T_FdRedirect {} -> return True
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
-- Are any subnodes inherently self-quoting?
|
||||
isQuoteFreeContext t =
|
||||
case t of
|
||||
TC_Nullary _ DoubleBracket _ -> return True
|
||||
TC_Unary _ DoubleBracket _ _ -> return True
|
||||
TC_Nullary _ DoubleBracket _ -> return True
|
||||
TC_Unary _ DoubleBracket _ _ -> return True
|
||||
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment {} -> return True
|
||||
T_Redirecting {} -> return False
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
T_CaseExpression {} -> return True
|
||||
T_HereDoc {} -> return True
|
||||
T_DollarBraced {} -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment {} -> return True
|
||||
T_Redirecting {} -> return False
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
T_CaseExpression {} -> return True
|
||||
T_HereDoc {} -> return True
|
||||
T_DollarBraced {} -> return True
|
||||
-- When non-strict, pragmatically assume it's desirable to split here
|
||||
T_ForIn {} -> return (not strict)
|
||||
T_SelectIn {} -> return (not strict)
|
||||
_ -> Nothing
|
||||
T_ForIn {} -> return (not strict)
|
||||
T_SelectIn {} -> return (not strict)
|
||||
_ -> Nothing
|
||||
|
||||
-- Check if a token is a parameter to a certain command by name:
|
||||
-- Example: isParamTo (parentMap params) "sed" t
|
||||
@@ -293,16 +296,16 @@ isParamTo tree cmd =
|
||||
go
|
||||
where
|
||||
go x = case Map.lookup (getId x) tree of
|
||||
Nothing -> False
|
||||
Nothing -> False
|
||||
Just parent -> check parent
|
||||
check t =
|
||||
case t of
|
||||
T_SingleQuoted _ _ -> go t
|
||||
T_DoubleQuoted _ _ -> go t
|
||||
T_NormalWord _ _ -> go t
|
||||
T_NormalWord _ _ -> go t
|
||||
T_SimpleCommand {} -> isCommand t cmd
|
||||
T_Redirecting {} -> isCommand t cmd
|
||||
_ -> False
|
||||
T_Redirecting {} -> isCommand t cmd
|
||||
_ -> False
|
||||
|
||||
-- Get the parent command (T_Redirecting) of a Token, if any.
|
||||
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
|
||||
@@ -312,8 +315,8 @@ getClosestCommand tree t =
|
||||
findCommand t =
|
||||
case t of
|
||||
T_Redirecting {} -> return True
|
||||
T_Script {} -> return False
|
||||
_ -> Nothing
|
||||
T_Script {} -> return False
|
||||
_ -> Nothing
|
||||
|
||||
-- Like above, if koala_man knew Haskell when starting this project.
|
||||
getClosestCommandM t = do
|
||||
@@ -334,7 +337,7 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
-- A list of the element and all its parents up to the root node.
|
||||
getPath tree t = t :
|
||||
case Map.lookup (getId t) tree of
|
||||
Nothing -> []
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
|
||||
-- Version of the above taking the map from the current context
|
||||
@@ -360,9 +363,9 @@ findFirst p l =
|
||||
[] -> Nothing
|
||||
(x:xs) ->
|
||||
case p x of
|
||||
Just True -> return x
|
||||
Just True -> return x
|
||||
Just False -> Nothing
|
||||
Nothing -> findFirst p xs
|
||||
Nothing -> findFirst p xs
|
||||
|
||||
-- Check whether a word is entirely output from a single command
|
||||
tokenIsJustCommandOutput t = case t of
|
||||
@@ -373,7 +376,7 @@ tokenIsJustCommandOutput t = case t of
|
||||
_ -> False
|
||||
where
|
||||
check [x] = not $ isOnlyRedirection x
|
||||
check _ = False
|
||||
check _ = False
|
||||
|
||||
-- TODO: Replace this with a proper Control Flow Graph
|
||||
getVariableFlow params t =
|
||||
@@ -393,9 +396,9 @@ getVariableFlow params t =
|
||||
unless (assignFirst t) $ setWritten t
|
||||
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
|
||||
|
||||
assignFirst T_ForIn {} = True
|
||||
assignFirst T_ForIn {} = True
|
||||
assignFirst T_SelectIn {} = True
|
||||
assignFirst _ = False
|
||||
assignFirst _ = False
|
||||
|
||||
setRead t =
|
||||
let read = getReferencedVariables (parentMap params) t
|
||||
@@ -423,7 +426,7 @@ leadType params t =
|
||||
parent <- Map.lookup (getId t) (parentMap params)
|
||||
case parent of
|
||||
T_Pipeline {} -> return parent
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
causesSubshell = do
|
||||
(T_Pipeline _ _ list) <- parentPipeline
|
||||
@@ -444,15 +447,12 @@ getModifiedVariables t =
|
||||
c@T_SimpleCommand {} ->
|
||||
getModifiedVariableCommand c
|
||||
|
||||
TA_Unary _ "++|" var -> maybeToList $ do
|
||||
name <- getLiteralString var
|
||||
return (t, t, name, DataString $ SourceFrom [t])
|
||||
TA_Unary _ "|++" var -> maybeToList $ do
|
||||
name <- getLiteralString var
|
||||
return (t, t, name, DataString $ SourceFrom [t])
|
||||
TA_Assignment _ op lhs rhs -> maybeToList $ do
|
||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> maybeToList $ do
|
||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||
name <- getLiteralString lhs
|
||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||
|
||||
-- Count [[ -v foo ]] as an "assignment".
|
||||
@@ -462,10 +462,10 @@ getModifiedVariables t =
|
||||
flip getLiteralStringExt token $ \x ->
|
||||
case x of
|
||||
T_Glob _ s -> return s -- Unquoted index
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
guard . not . null $ str
|
||||
return (t, token, str, DataString $ SourceChecked)
|
||||
return (t, token, str, DataString SourceChecked)
|
||||
|
||||
T_DollarBraced _ l -> maybeToList $ do
|
||||
let string = bracedString t
|
||||
@@ -489,7 +489,7 @@ isClosingFileOp op =
|
||||
case op of
|
||||
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
||||
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
||||
_ -> False
|
||||
_ -> False
|
||||
|
||||
|
||||
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||
@@ -498,7 +498,9 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
||||
"export" -> if "f" `elem` flags
|
||||
then []
|
||||
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
|
||||
else []
|
||||
"readonly" ->
|
||||
@@ -527,7 +529,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
"getopts" ->
|
||||
case rest of
|
||||
opts:var:_ -> maybeToList $ getLiteral var
|
||||
_ -> []
|
||||
_ -> []
|
||||
|
||||
"let" -> concatMap letParamToLiteral rest
|
||||
|
||||
@@ -591,9 +593,9 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
getSetParams (t:rest) =
|
||||
let s = getLiteralString t in
|
||||
case s of
|
||||
Just "--" -> return rest
|
||||
Just "--" -> return rest
|
||||
Just ('-':_) -> getSetParams rest
|
||||
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||
getSetParams [] = Nothing
|
||||
|
||||
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
|
||||
@@ -620,12 +622,17 @@ getIndexReferences s = fromMaybe [] $ do
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
||||
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
||||
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
-- if mods start with [, then drop until ]
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 0
|
||||
offsets <- match !!! 1
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^ *:([^-=?+].*)"
|
||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||
|
||||
getReferencedVariables parents t =
|
||||
case t of
|
||||
@@ -634,10 +641,10 @@ getReferencedVariables parents t =
|
||||
map (\x -> (l, l, x)) (
|
||||
getIndexReferences str
|
||||
++ getOffsetReferences (getBracedModifier str))
|
||||
TA_Expansion id _ ->
|
||||
TA_Variable id name _ ->
|
||||
if isArithmeticAssignment t
|
||||
then []
|
||||
else getIfReference t t
|
||||
else [(t, t, name)]
|
||||
T_Assignment id mode str _ word ->
|
||||
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
||||
|
||||
@@ -664,9 +671,8 @@ getReferencedVariables parents t =
|
||||
else []
|
||||
|
||||
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
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
getIfReference context token = maybeToList $ do
|
||||
str <- getLiteralStringExt literalizer token
|
||||
@@ -678,7 +684,7 @@ getReferencedVariables parents t =
|
||||
|
||||
isArithmeticAssignment t = case getPath parents t of
|
||||
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
_ -> False
|
||||
_ -> False
|
||||
|
||||
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||
|
||||
@@ -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.
|
||||
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||
|
||||
isCommandMatch token matcher = fromMaybe False $ do
|
||||
cmd <- getCommandName token
|
||||
return $ matcher cmd
|
||||
isCommandMatch token matcher = fromMaybe False $
|
||||
fmap matcher (getCommandName token)
|
||||
|
||||
-- Does this regex look like it was intended as a glob?
|
||||
-- True: *foo*
|
||||
@@ -701,7 +706,7 @@ isCommandMatch token matcher = fromMaybe False $ do
|
||||
isConfusedGlobRegex :: String -> Bool
|
||||
isConfusedGlobRegex ('*':_) = True
|
||||
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
||||
isConfusedGlobRegex _ = False
|
||||
isConfusedGlobRegex _ = False
|
||||
|
||||
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||
isVariableChar x = isVariableStartChar x || isDigit x
|
||||
@@ -711,7 +716,7 @@ prop_isVariableName1 = isVariableName "_fo123"
|
||||
prop_isVariableName2 = not $ isVariableName "4"
|
||||
prop_isVariableName3 = not $ isVariableName "test: "
|
||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
isVariableName _ = False
|
||||
|
||||
getVariablesFromLiteralToken token =
|
||||
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
|
||||
@@ -744,7 +749,7 @@ getBracedReference s = fromMaybe s $
|
||||
where
|
||||
noPrefix = dropPrefix s
|
||||
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
|
||||
dropPrefix "" = ""
|
||||
dropPrefix "" = ""
|
||||
takeName s = do
|
||||
let name = takeWhile isVariableChar s
|
||||
guard . not $ null name
|
||||
@@ -769,12 +774,12 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Useful generic functions.
|
||||
|
||||
@@ -789,12 +794,12 @@ potentially = fromMaybe (return ())
|
||||
|
||||
-- Get element 0 or a default. Like `head` but safe.
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
headOrDefault def _ = def
|
||||
|
||||
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||
(!!!) list i =
|
||||
case drop i list of
|
||||
[] -> Nothing
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
-- Run a command if the shell is in the given list
|
||||
@@ -815,7 +820,7 @@ filterByAnnotation asSpec params =
|
||||
any hasNum anns
|
||||
where
|
||||
hasNum (DisableComment ts) = num == ts
|
||||
hasNum _ = False
|
||||
hasNum _ = False
|
||||
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
|
||||
shouldIgnoreFor _ _ = False
|
||||
parents = parentMap params
|
||||
@@ -825,7 +830,7 @@ filterByAnnotation asSpec params =
|
||||
isCountingReference (T_DollarBraced id token) =
|
||||
case concat $ oversimplify token of
|
||||
'#':_ -> True
|
||||
_ -> False
|
||||
_ -> False
|
||||
isCountingReference _ = False
|
||||
|
||||
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
|
||||
@@ -837,7 +842,38 @@ isQuotedAlternativeReference t =
|
||||
where
|
||||
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 []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||
@@ -194,5 +194,9 @@ prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo
|
||||
prop_filewideAnnotation8 = null $
|
||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||
|
||||
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
|
||||
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
|
||||
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,15 +15,13 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
||||
-- This module contains checks that examine specific commands by name.
|
||||
module ShellCheck.Checks.Commands (checker
|
||||
, ShellCheck.Checks.Commands.runTests
|
||||
) where
|
||||
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
@@ -88,6 +86,12 @@ commandChecks = [
|
||||
,checkWhileGetoptsCase
|
||||
,checkCatastrophicRm
|
||||
,checkLetUsage
|
||||
,checkMvArguments, checkCpArguments, checkLnArguments
|
||||
,checkFindRedirections
|
||||
,checkReadExpansions
|
||||
,checkWhich
|
||||
,checkSudoRedirect
|
||||
,checkSudoArgs
|
||||
]
|
||||
|
||||
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_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
||||
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
||||
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\""
|
||||
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||
where
|
||||
nonOptions =
|
||||
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
|
||||
isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
|
||||
f args =
|
||||
case nonOptions args of
|
||||
(hostport:r@(_:_)) -> checkArg $ last r
|
||||
case partition isOption args of
|
||||
([], hostport:r@(_:_)) -> checkArg $ last r
|
||||
_ -> return ()
|
||||
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||
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_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
||||
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
||||
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
|
||||
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||
@@ -517,6 +525,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
case string of
|
||||
'%':'%':rest -> countFormats rest
|
||||
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
|
||||
'%':'*':rest -> 2 + countFormats rest -- width is specified as an argument
|
||||
'%':rest -> 1 + countFormats rest
|
||||
_:rest -> countFormats rest
|
||||
[] -> 0
|
||||
@@ -532,7 +541,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
"This printf format string has no variables. Other arguments are ignored."
|
||||
|
||||
when (vars > 0
|
||||
&& length more < vars
|
||||
&& ((length more) `mod` vars /= 0 || null more)
|
||||
&& all (not . mayBecomeMultipleArgs) more) $
|
||||
warn (getId format) 2183 $
|
||||
"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_checkExportedExpansions2 = verify 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
|
||||
check = mapM_ checkForVariables
|
||||
checkForVariables f =
|
||||
case getWordParts f of
|
||||
[t@(T_DollarBraced {})] ->
|
||||
warn (getId t) 2163 "Exporting an expansion rather than a variable."
|
||||
_ -> return ()
|
||||
check t = potentially $ do
|
||||
var <- getSingleUnmodifiedVariable t
|
||||
let name = bracedString var
|
||||
return . warn (getId t) 2163 $
|
||||
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||
|
||||
prop_checkReadExpansions1 = verify checkReadExpansions "read $var"
|
||||
prop_checkReadExpansions2 = verify checkReadExpansions "read -r $var"
|
||||
prop_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_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
||||
@@ -850,5 +892,112 @@ checkLetUsage = CommandCheck (Exactly "let") f
|
||||
f t = whenShell [Bash,Ksh] $ do
|
||||
style (getId t) 2219 $ "Instead of 'let expr', prefer (( expr )) ."
|
||||
|
||||
|
||||
missingDestination handler token = do
|
||||
case params of
|
||||
[single] -> do
|
||||
unless (hasTarget || mayBecomeMultipleArgs single) $
|
||||
handler token
|
||||
_ -> return ()
|
||||
where
|
||||
args = getAllFlags token
|
||||
params = 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 []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2016 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,13 +15,11 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.Checks.ShellSupport (checker
|
||||
, ShellCheck.Checks.ShellSupport.runTests
|
||||
) where
|
||||
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
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_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
params <- ask
|
||||
kludge params t
|
||||
@@ -191,11 +191,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
||||
|
||||
bashism t@(TA_Expansion id _) | isBashism =
|
||||
warnMsg id $ fromJust str ++ " is"
|
||||
where
|
||||
str = getLiteralString t
|
||||
isBashism = isJust str && isBashVariable (fromJust str)
|
||||
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||
warnMsg id $ str ++ " is"
|
||||
|
||||
bashism t@(T_DollarBraced id token) = do
|
||||
mapM_ check expansion
|
||||
when (isBashVariable var) $
|
||||
@@ -279,14 +277,18 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||
"typeset"
|
||||
] ++ if not isDash then ["local", "type"] else []
|
||||
] ++ if not isDash then ["local"] else []
|
||||
allowedFlags = Map.fromList [
|
||||
("read", if isDash then ["r", "p"] else ["r"]),
|
||||
("ulimit", ["f"]),
|
||||
("exec", []),
|
||||
("export", ["-p"]),
|
||||
("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 ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
@@ -295,8 +297,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
|
||||
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
|
||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", "string indexing is"),
|
||||
(re $ "^([*@][%#]|#[@*])", "string operations on $@/$* are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", "string replacement is")
|
||||
]
|
||||
bashVars = [
|
||||
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
@@ -79,10 +79,10 @@ commonCommands = [
|
||||
|
||||
nonReadingCommands = [
|
||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||
"cp", "du", "echo", "export", "fg", "fuser", "getconf", "getopt",
|
||||
"getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls", "locale", "mv",
|
||||
"nice", "printf", "ps", "pwd", "renice", "rm", "rmdir", "set", "sleep",
|
||||
"touch", "trap", "ulimit", "unalias", "uname"
|
||||
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
||||
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
||||
]
|
||||
|
||||
sampleWords = [
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.CheckStyle (format) where
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.Format where
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.GCC (format) where
|
||||
|
@@ -1,8 +1,9 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,17 +16,19 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.JSON (format) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import Text.JSON
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
format = do
|
||||
ref <- newIORef []
|
||||
@@ -36,19 +39,30 @@ format = do
|
||||
footer = finish ref
|
||||
}
|
||||
|
||||
instance JSON (PositionedComment) where
|
||||
showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [
|
||||
("file", showJSON $ posFile start),
|
||||
("line", showJSON $ posLine start),
|
||||
("endLine", showJSON $ posLine end),
|
||||
("column", showJSON $ posColumn start),
|
||||
("endColumn", showJSON $ posColumn end),
|
||||
("level", showJSON $ severityText comment),
|
||||
("code", showJSON code),
|
||||
("message", showJSON string)
|
||||
]
|
||||
instance ToJSON (PositionedComment) where
|
||||
toJSON comment@(PositionedComment start end (Comment level code string)) =
|
||||
object [
|
||||
"file" .= posFile start,
|
||||
"line" .= posLine start,
|
||||
"endLine" .= posLine end,
|
||||
"column" .= posColumn start,
|
||||
"endColumn" .= posColumn end,
|
||||
"level" .= severityText comment,
|
||||
"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
|
||||
collectResult ref result _ =
|
||||
@@ -56,5 +70,5 @@ collectResult ref result _ =
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
putStrLn $ encodeStrict list
|
||||
BL.putStrLn $ encode list
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Formatter.TTY (format) where
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
module ShellCheck.Interface where
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,9 +15,12 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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
|
||||
|
||||
import ShellCheck.AST
|
||||
@@ -25,7 +28,7 @@ import ShellCheck.ASTLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
|
||||
import Control.Applicative ((<*))
|
||||
import Control.Applicative ((<*), (*>))
|
||||
import Control.Monad
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.Trans
|
||||
@@ -143,8 +146,7 @@ data Context =
|
||||
deriving (Show)
|
||||
|
||||
data HereDocContext =
|
||||
HereDocPending Token -- on linefeed, read this T_HereDoc
|
||||
| HereDocBoundary -- but don't consider heredocs before this
|
||||
HereDocPending Token [Context] -- on linefeed, read this T_HereDoc
|
||||
deriving (Show)
|
||||
|
||||
data UserState = UserState {
|
||||
@@ -193,50 +195,21 @@ addToHereDocMap id list = do
|
||||
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
|
||||
state <- getState
|
||||
context <- getCurrentContexts
|
||||
let docs = pendingHereDocs state
|
||||
putState $ state {
|
||||
pendingHereDocs = HereDocPending t : docs
|
||||
pendingHereDocs = HereDocPending t context : docs
|
||||
}
|
||||
|
||||
popPendingHereDocs = do
|
||||
state <- getState
|
||||
let (pending, boundary) = break isHereDocBoundary $ pendingHereDocs state
|
||||
let pending = pendingHereDocs state
|
||||
putState $ state {
|
||||
pendingHereDocs = boundary
|
||||
pendingHereDocs = []
|
||||
}
|
||||
return . map extract . reverse $ pendingHereDocs state
|
||||
where
|
||||
extract (HereDocPending t) = t
|
||||
|
||||
isHereDocBoundary x = case x of
|
||||
HereDocBoundary -> True
|
||||
otherwise -> False
|
||||
return . reverse $ pendingHereDocs state
|
||||
|
||||
getMap = positionMap <$> getState
|
||||
getParseNotes = parseNotes <$> getState
|
||||
@@ -331,12 +304,15 @@ pushContext c = do
|
||||
parseProblemAtWithEnd start end level code msg = do
|
||||
irrelevant <- shouldIgnoreCode code
|
||||
unless irrelevant $
|
||||
Ms.modify (\state -> state {
|
||||
parseProblems = note:parseProblems state
|
||||
})
|
||||
addParseProblem note
|
||||
where
|
||||
note = ParseNote start end level code msg
|
||||
|
||||
addParseProblem note =
|
||||
Ms.modify (\state -> state {
|
||||
parseProblems = note:parseProblems state
|
||||
})
|
||||
|
||||
parseProblemAt pos = parseProblemAtWithEnd pos pos
|
||||
|
||||
parseProblemAtId :: Monad m => Id -> Severity -> Integer -> String -> SCParser m ()
|
||||
@@ -401,15 +377,16 @@ acceptButWarn parser level code note =
|
||||
parseProblemAt pos level code note
|
||||
)
|
||||
|
||||
withContext entry p = do
|
||||
pushContext entry
|
||||
do
|
||||
v <- p
|
||||
popContext
|
||||
return v
|
||||
<|> do -- p failed without consuming input, abort context
|
||||
v <- popContext
|
||||
fail ""
|
||||
parsecBracket before after op = do
|
||||
val <- before
|
||||
(op val <* optional (after val)) <|> (after val *> fail "")
|
||||
|
||||
swapContext contexts p =
|
||||
parsecBracket (getCurrentContexts <* setCurrentContexts contexts)
|
||||
setCurrentContexts
|
||||
(const p)
|
||||
|
||||
withContext entry p = parsecBracket (pushContext entry) (const popContext) (const p)
|
||||
|
||||
called s p = do
|
||||
pos <- getPosition
|
||||
@@ -421,7 +398,7 @@ withAnnotations anns =
|
||||
readConditionContents single =
|
||||
readCondContents `attempting` lookAhead (do
|
||||
pos <- getPosition
|
||||
s <- many1 letter
|
||||
s <- readVariableName
|
||||
when (s `elem` commonCommands) $
|
||||
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
|
||||
readRegex = called "regex" $ do
|
||||
id <- getNextId
|
||||
parts <- many1 (
|
||||
readGroup <|>
|
||||
readSingleQuoted <|>
|
||||
readDoubleQuoted <|>
|
||||
readDollarExpression <|>
|
||||
readNormalLiteral "( " <|>
|
||||
readPipeLiteral <|>
|
||||
readGlobLiteral)
|
||||
parts <- many1 readPart
|
||||
void spacing
|
||||
return $ T_NormalWord id parts
|
||||
where
|
||||
readPart = choice [
|
||||
readGroup,
|
||||
readSingleQuoted,
|
||||
readDoubleQuoted,
|
||||
readDollarExpression,
|
||||
readNormalLiteral "( ",
|
||||
readPipeLiteral,
|
||||
readGlobLiteral
|
||||
]
|
||||
readGlobLiteral = do
|
||||
id <- getNextId
|
||||
s <- extglobStart <|> oneOf "{}[]$"
|
||||
@@ -623,7 +602,7 @@ readConditionContents single =
|
||||
readGroup = called "regex grouping" $ do
|
||||
id <- getNextId
|
||||
char '('
|
||||
parts <- many (readGroup <|> readSingleQuoted <|> readDoubleQuoted <|> readDollarExpression <|> readRegexLiteral <|> readGlobLiteral)
|
||||
parts <- many (readPart <|> readRegexLiteral)
|
||||
char ')'
|
||||
return $ T_NormalWord id parts
|
||||
readRegexLiteral = do
|
||||
@@ -724,31 +703,35 @@ readArithmeticContents =
|
||||
spacing1
|
||||
return (str, alt)
|
||||
|
||||
|
||||
readArrayIndex = do
|
||||
id <- getNextId
|
||||
char '['
|
||||
middle <- readArithmeticContents
|
||||
pos <- getPosition
|
||||
middle <- readStringForParser readArithmeticContents
|
||||
char ']'
|
||||
return $ TA_Index id middle
|
||||
return $ T_UnparsedIndex id pos middle
|
||||
|
||||
literal s = do
|
||||
id <- getNextId
|
||||
string s
|
||||
return $ T_Literal id s
|
||||
|
||||
readArithmeticLiteral =
|
||||
readArrayIndex <|> literal "#"
|
||||
readVariable = do
|
||||
id <- getNextId
|
||||
name <- readVariableName
|
||||
indices <- many readArrayIndex
|
||||
spacing
|
||||
return $ TA_Variable id name indices
|
||||
|
||||
readExpansion = do
|
||||
id <- getNextId
|
||||
pieces <- many1 $ choice [
|
||||
readArithmeticLiteral,
|
||||
readSingleQuoted,
|
||||
readDoubleQuoted,
|
||||
readNormalDollar,
|
||||
readBraced,
|
||||
readUnquotedBackTicked,
|
||||
literal "#",
|
||||
readNormalLiteral "+-*/=%^,]?:"
|
||||
]
|
||||
spacing
|
||||
@@ -761,7 +744,7 @@ readArithmeticContents =
|
||||
spacing
|
||||
return s
|
||||
|
||||
readArithTerm = readGroup <|> readExpansion
|
||||
readArithTerm = readGroup <|> readVariable <|> readExpansion
|
||||
|
||||
readSequence = do
|
||||
spacing
|
||||
@@ -864,6 +847,9 @@ prop_readCondition16= isOk readCondition "[ foo \\< bar ]"
|
||||
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
|
||||
prop_readCondition18= isOk readCondition "[ ]"
|
||||
prop_readCondition19= isOk readCondition "[ '(' x \")\" ]"
|
||||
prop_readCondition20= isOk readCondition "[[ echo_rc -eq 0 ]]"
|
||||
prop_readCondition21= isOk readCondition "[[ $1 =~ ^(a\\ b)$ ]]"
|
||||
prop_readCondition22= isOk readCondition "[[ $1 =~ \\.a\\.(\\.b\\.)\\.c\\. ]]"
|
||||
readCondition = called "test expression" $ do
|
||||
opos <- getPosition
|
||||
id <- getNextId
|
||||
@@ -1166,6 +1152,7 @@ readBackTicked quoted = called "backtick expansion" $ do
|
||||
parseProblemAt pos ErrorC 1077
|
||||
"For command expansion, the tick should slant left (` vs ´). Use $(..) instead."
|
||||
|
||||
-- Run a parser on a new input, such as for `..` or here documents.
|
||||
subParse pos parser input = do
|
||||
lastPosition <- getPosition
|
||||
lastInput <- getInput
|
||||
@@ -1237,12 +1224,6 @@ doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBa
|
||||
"This is a unicode quote. Delete and retype it (or ignore/singlequote for literal)."
|
||||
return $ T_Literal id [c]
|
||||
|
||||
readDoubleQuotedLiteral = do
|
||||
doubleQuote
|
||||
x <- readDoubleLiteral
|
||||
doubleQuote
|
||||
return x
|
||||
|
||||
readDoubleLiteral = do
|
||||
id <- getNextId
|
||||
s <- many1 readDoubleLiteralPart
|
||||
@@ -1561,6 +1542,7 @@ prop_readDollarVariable = isOk readDollarVariable "$@"
|
||||
prop_readDollarVariable2 = isOk (readDollarVariable >> anyChar) "$?!"
|
||||
prop_readDollarVariable3 = isWarning (readDollarVariable >> anyChar) "$10"
|
||||
prop_readDollarVariable4 = isWarning (readDollarVariable >> string "[@]") "$arr[@]"
|
||||
prop_readDollarVariable5 = isWarning (readDollarVariable >> string "[f") "$arr[f"
|
||||
|
||||
readDollarVariable = do
|
||||
id <- getNextId
|
||||
@@ -1583,8 +1565,8 @@ readDollarVariable = do
|
||||
name <- readVariableName
|
||||
value <- wrap name
|
||||
return (T_DollarBraced id value) `attempting` do
|
||||
lookAhead $ void (string "[@]") <|> void (string "[*]") <|> void readArrayIndex
|
||||
parseNoteAt pos ErrorC 1087 "Braces are required when expanding arrays, as in ${array[idx]}."
|
||||
lookAhead $ char '['
|
||||
parseNoteAt pos ErrorC 1087 "Use braces when expanding arrays, e.g. ${array[idx]} (or ${var}[.. to quiet)."
|
||||
|
||||
try $ char '$' >> (positional <|> special <|> regular)
|
||||
|
||||
@@ -1607,9 +1589,9 @@ readDollarLonely = do
|
||||
return $ T_Literal id "$"
|
||||
|
||||
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_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_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
|
||||
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_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
|
||||
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_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
|
||||
fid <- getNextId
|
||||
pos <- getPosition
|
||||
@@ -1654,38 +1640,96 @@ readPendingHereDocs = do
|
||||
docs <- popPendingHereDocs
|
||||
mapM_ readDoc docs
|
||||
where
|
||||
readDoc (T_HereDoc id dashed quoted endToken _) = do
|
||||
pos <- getPosition
|
||||
hereData <- concat <$> rawLine `reluctantlyTill` do
|
||||
linewhitespace `reluctantlyTill` string endToken
|
||||
string endToken
|
||||
void linewhitespace <|> void (oneOf "\n;&#)") <|> eof
|
||||
do
|
||||
spaces <- linewhitespace `reluctantlyTill` string endToken
|
||||
verifyHereDoc dashed quoted spaces hereData
|
||||
string endToken
|
||||
trailingPos <- getPosition
|
||||
trailers <- lookAhead $ many (noneOf "\n")
|
||||
let ppt = parseProblemAt trailingPos ErrorC
|
||||
unless (null trailers) $
|
||||
if all isSpace trailers
|
||||
then ppt 1118 "Delete whitespace after the here-doc end token."
|
||||
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
|
||||
readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =
|
||||
swapContext ctx $
|
||||
do
|
||||
docPos <- getPosition
|
||||
tokenPos <- Map.findWithDefault (error "Missing ID") id <$> getMap
|
||||
(terminated, wasWarned, lines) <- readDocLines dashed endToken
|
||||
let hereData = unlines lines
|
||||
unless terminated $ do
|
||||
unless wasWarned $
|
||||
debugHereDoc tokenPos endToken hereData
|
||||
fail "Here document was not correctly terminated"
|
||||
list <- parseHereData quoted docPos hereData
|
||||
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
|
||||
c <- many $ noneOf "\n"
|
||||
void (char '\n') <|> eof
|
||||
return $ c ++ "\n"
|
||||
return c
|
||||
|
||||
parseHereData Quoted startPos hereData = do
|
||||
id <- getNextIdAt startPos
|
||||
@@ -1694,20 +1738,13 @@ readPendingHereDocs = do
|
||||
parseHereData Unquoted startPos hereData =
|
||||
subParse startPos readHereData hereData
|
||||
|
||||
readHereData = many $ try doubleQuotedPart <|> readHereLiteral
|
||||
readHereData = many $ doubleQuotedPart <|> readHereLiteral
|
||||
|
||||
readHereLiteral = do
|
||||
id <- getNextId
|
||||
chars <- many1 $ noneOf "`$\\"
|
||||
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
|
||||
| endToken `isInfixOf` doc =
|
||||
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
|
||||
id <- getNextId
|
||||
op <- g_GREATAND <|> g_LESSAND
|
||||
target <- readIoVariable <|> many1 digit <|> string "-"
|
||||
target <- readIoVariable <|> digitsAndOrDash
|
||||
return $ T_IoDuplicate id op target
|
||||
where
|
||||
-- either digits with optional dash, or a required dash
|
||||
digitsAndOrDash = do
|
||||
str <- many digit
|
||||
dash <- (if null str then id else option "") $ string "-"
|
||||
return $ str ++ dash
|
||||
|
||||
|
||||
prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\""
|
||||
readIoFile = called "redirection" $ do
|
||||
@@ -1755,6 +1799,7 @@ prop_readIoRedirect3 = isOk readIoRedirect "4>&-"
|
||||
prop_readIoRedirect4 = isOk readIoRedirect "&> lol"
|
||||
prop_readIoRedirect5 = isOk readIoRedirect "{foo}>&2"
|
||||
prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-"
|
||||
prop_readIoRedirect7 = isOk readIoRedirect "{foo}>&1-"
|
||||
readIoRedirect = do
|
||||
id <- getNextId
|
||||
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_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"
|
||||
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
|
||||
pos <- getPosition
|
||||
id1 <- getNextId
|
||||
id2 <- getNextId
|
||||
prefix <- option [] readCmdPrefix
|
||||
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"
|
||||
|
||||
case cmd of
|
||||
Nothing -> return $ makeSimpleCommand id1 id2 prefix [] []
|
||||
Just cmd -> do
|
||||
validateCommand pos cmd
|
||||
suffix <- option [] $ getParser readCmdSuffix cmd [
|
||||
(["declare", "export", "local", "readonly", "typeset"], readModifierSuffix),
|
||||
(["time"], readTimeSuffix),
|
||||
@@ -1869,6 +1923,22 @@ readSimpleCommand = called "simple command" $ do
|
||||
then action
|
||||
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 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
|
||||
return t
|
||||
Right script -> do
|
||||
id <- getNextIdAt pos
|
||||
id1 <- getNextIdAt pos
|
||||
id2 <- getNextIdAt pos
|
||||
|
||||
let included = do
|
||||
src <- subRead filename script
|
||||
return $ T_Include id t src
|
||||
return $ T_SourceCommand id1 t (T_Include id2 src)
|
||||
|
||||
let failed = do
|
||||
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_readIfClause2 = isWarning readIfClause "if false; then; echo oo; fi"
|
||||
prop_readIfClause3 = isWarning readIfClause "if false; then true; else; echo lol; fi"
|
||||
prop_readIfClause4 = isWarning readIfClause "if false; then true; else if true; then echo lol; fi"
|
||||
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"
|
||||
readIfClause = called "if expression" $ do
|
||||
id <- getNextId
|
||||
@@ -2080,12 +2151,9 @@ readIfPart = do
|
||||
|
||||
readElifPart = called "elif clause" $ do
|
||||
pos <- getPosition
|
||||
correctElif <- elif
|
||||
unless correctElif $
|
||||
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)."
|
||||
g_Elif
|
||||
allspacing
|
||||
condition <- readTerm
|
||||
|
||||
ifNextToken (g_Fi <|> g_Elif <|> g_Else) $
|
||||
parseProblemAt pos ErrorC 1049 "Did you forget the 'then' for this 'elif'?"
|
||||
|
||||
@@ -2095,13 +2163,14 @@ readElifPart = called "elif clause" $ do
|
||||
verifyNotEmptyIf "then"
|
||||
action <- readTerm
|
||||
return (condition, action)
|
||||
where
|
||||
elif = (g_Elif >> return True) <|>
|
||||
try (g_Else >> g_If >> return False)
|
||||
|
||||
readElsePart = called "else clause" $ do
|
||||
pos <- getPosition
|
||||
g_Else
|
||||
optional $ do
|
||||
try . lookAhead $ g_If
|
||||
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)."
|
||||
|
||||
acceptButWarn g_Semi ErrorC 1053 "Semicolons directly after 'else' are not allowed. Just remove it."
|
||||
allspacing
|
||||
verifyNotEmptyIf "else"
|
||||
@@ -2590,11 +2659,14 @@ tryParseWordToken keyword t = try $ do
|
||||
str <- anycaseString keyword
|
||||
|
||||
optional $ do
|
||||
try . lookAhead $ char '['
|
||||
parseProblem ErrorC 1069 "You need a space before the [."
|
||||
optional $ do
|
||||
try . lookAhead $ char '#'
|
||||
parseProblem ErrorC 1099 "You need a space before the #."
|
||||
c <- try . lookAhead $ anyChar
|
||||
let warning code = parseProblem ErrorC code $ "You need a space before the " ++ [c] ++ "."
|
||||
case c of
|
||||
'[' -> warning 1069
|
||||
'#' -> warning 1099
|
||||
'!' -> warning 1129
|
||||
':' -> warning 1130
|
||||
_ -> return ()
|
||||
|
||||
lookAhead keywordSeparator
|
||||
when (str /= keyword) $
|
||||
@@ -2666,20 +2738,23 @@ prop_readShebang1 = isOk readShebang "#!/bin/sh\n"
|
||||
prop_readShebang2 = isWarning readShebang "!# /bin/sh\n"
|
||||
prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n"
|
||||
prop_readShebang4 = isWarning readShebang "! /bin/sh"
|
||||
prop_readShebang5 = isWarning readShebang "\n#!/bin/sh"
|
||||
prop_readShebang6 = isWarning readShebang " # Copyright \n!#/bin/bash"
|
||||
prop_readShebang7 = isNotOk readShebang "# Copyright \nfoo\n#!/bin/bash"
|
||||
readShebang = do
|
||||
choice $ map try [
|
||||
readCorrect,
|
||||
readSwapped,
|
||||
readTooManySpaces,
|
||||
readMissingHash,
|
||||
readMissingBang
|
||||
]
|
||||
anyShebang <|> try readMissingBang <|> withHeader
|
||||
many linewhitespace
|
||||
str <- many $ noneOf "\r\n"
|
||||
optional carriageReturn
|
||||
optional linefeed
|
||||
return str
|
||||
where
|
||||
anyShebang = choice $ map try [
|
||||
readCorrect,
|
||||
readSwapped,
|
||||
readTooManySpaces,
|
||||
readMissingHash
|
||||
]
|
||||
readCorrect = void $ string "#!"
|
||||
|
||||
readSwapped = do
|
||||
@@ -2688,7 +2763,7 @@ readShebang = do
|
||||
parseProblemAt pos ErrorC 1084
|
||||
"Use #!, not !#, for the shebang."
|
||||
|
||||
skipSpaces = liftM (not . null) $ many linewhitespace
|
||||
skipSpaces = fmap (not . null) $ many linewhitespace
|
||||
readTooManySpaces = do
|
||||
startPos <- getPosition
|
||||
startSpaces <- skipSpaces
|
||||
@@ -2717,11 +2792,22 @@ readShebang = do
|
||||
parseProblemAt pos ErrorC 1113
|
||||
"Use #!, not just #, for the shebang."
|
||||
|
||||
|
||||
ensurePathAhead = lookAhead $ do
|
||||
many linewhitespace
|
||||
char '/'
|
||||
|
||||
withHeader = try $ do
|
||||
many1 headerLine
|
||||
pos <- getPosition
|
||||
anyShebang <*
|
||||
parseProblemAt pos ErrorC 1128 "The shebang must be on the first line. Delete blanks and move comments."
|
||||
|
||||
headerLine = do
|
||||
notFollowedBy2 anyShebang
|
||||
many linewhitespace
|
||||
optional readAnyComment
|
||||
linefeed
|
||||
|
||||
verifyEof = eof <|> choice [
|
||||
ifParsable g_Lparen $
|
||||
parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",
|
||||
@@ -2810,17 +2896,34 @@ readScriptFile = do
|
||||
|
||||
readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF"
|
||||
|
||||
readScript = do
|
||||
script <- readScriptFile
|
||||
reparseIndices script
|
||||
readScript = readScriptFile
|
||||
|
||||
|
||||
-- Interactively run a parser in ghci:
|
||||
-- debugParse readScript "echo 'hello world'"
|
||||
-- Interactively run a specific parser in ghci:
|
||||
-- debugParse readSimpleCommand "echo 'hello world'"
|
||||
debugParse p string = runIdentity $ do
|
||||
(res, _) <- runParser testEnvironment p "-" string
|
||||
return res
|
||||
|
||||
-- Interactively run the complete parser in ghci:
|
||||
-- debugParseScript "#!/bin/bash\necho 'Hello World'\n"
|
||||
debugParseScript string =
|
||||
result {
|
||||
-- Remove the noisiest parts
|
||||
prTokenPositions = Map.fromList [
|
||||
(Id 0, Position {
|
||||
posFile = "removed for clarity",
|
||||
posLine = -1,
|
||||
posColumn = -1
|
||||
})]
|
||||
}
|
||||
where
|
||||
result = runIdentity $
|
||||
parseScript (mockedSystemInterface []) $ ParseSpec {
|
||||
psFilename = "debug",
|
||||
psScript = string,
|
||||
psCheckSourced = False
|
||||
}
|
||||
|
||||
testEnvironment =
|
||||
Environment {
|
||||
systemInterface = (mockedSystemInterface []),
|
||||
@@ -2912,14 +3015,15 @@ parseShell env name contents = do
|
||||
prTokenPositions = Map.empty,
|
||||
prRoot = Nothing
|
||||
}
|
||||
|
||||
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
||||
where
|
||||
isName (ContextName _ _) = True
|
||||
isName _ = False
|
||||
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
||||
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 $
|
||||
"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
|
||||
-- depending on declare -A statements.
|
||||
@@ -2936,6 +3040,9 @@ reparseIndices root =
|
||||
return $ T_Array id2 newWords
|
||||
x -> return x
|
||||
return $ T_Assignment id mode name newIndices newValue
|
||||
f (TA_Variable id name indices) = do
|
||||
newIndices <- mapM (fixAssignmentIndex name) indices
|
||||
return $ TA_Variable id name newIndices
|
||||
f t = return t
|
||||
|
||||
fixIndexElement name word =
|
||||
@@ -2943,13 +3050,13 @@ reparseIndices root =
|
||||
T_IndexedElement id indices value -> do
|
||||
new <- mapM (fixAssignmentIndex name) indices
|
||||
return $ T_IndexedElement id new value
|
||||
otherwise -> return word
|
||||
_ -> return word
|
||||
|
||||
fixAssignmentIndex name word =
|
||||
case word of
|
||||
T_UnparsedIndex id pos src -> do
|
||||
T_UnparsedIndex id pos src ->
|
||||
parsed name pos src
|
||||
otherwise -> return word
|
||||
_ -> return word
|
||||
|
||||
parsed name pos src =
|
||||
if isAssociative name
|
||||
@@ -2986,6 +3093,36 @@ parseScript sys 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 []
|
||||
runTests = $quickCheckAll
|
@@ -2,7 +2,7 @@
|
||||
Copyright 2012-2015 Vidar Holen
|
||||
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# 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)
|
||||
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