130 Commits

Author SHA1 Message Date
Vidar Holen
a3be776f80 Stable version 0.5.0
This release is dedicated to Valve for keeping PC gaming awesome.
Also, for that time they proved the need for a tool like ShellCheck.
2018-05-31 20:01:42 -07:00
Vidar Holen
b5e5d249c4 Merge pull request #1237 from ngzhian/1234
Check if builtin cd is called
2018-05-27 18:34:18 -07:00
Vidar Holen
efffc6150b Warn about string operations on $@/$* (fixes #1236) 2018-05-27 12:53:01 -07:00
Ng Zhi An
cb4a0c0250 Check if builtin cd is called
Fixes #1234
2018-05-27 11:42:37 -07:00
Vidar Holen
135cf5932f Parse here docs as per spec (fixes #1050) 2018-05-26 21:01:18 -07:00
Vidar Holen
467dfe07b6 Add a unit test and separate ids for 884eff0c 2018-05-23 19:51:36 -07:00
Vidar Holen
d140388bea Merge pull request #1232 from ngzhian/source-x
Add T_SourceCommand to wrap source commands and sourced code
2018-05-23 19:41:55 -07:00
Ng Zhi An
884eff0c36 Add T_SourceCommand to wrap source commands and sourced code
Fixes #1181
2018-05-22 22:43:26 -07:00
Vidar Holen
1d8047cce1 Warn about unnecessary subshells in tests 2018-05-22 22:35:37 -07:00
Vidar Holen
77546fba2f Merge pull request #1223 from ngzhian/1124
Handle offset references of the form [foo]:bar (fixes #1124)
2018-05-20 17:26:08 -07:00
Vidar Holen
4a5ee06ce4 Merge pull request #1206 from ngzhian/aeson
Change to aeson (fixes #1085)
2018-05-20 17:19:20 -07:00
Ng Zhi An
4dceecb1ed Handle offset references of the form [foo]:bar (fixes #1124) 2018-05-13 22:48:02 -07:00
Vidar Holen
5b226e733b Merge pull request #1203 from ngzhian/1199
Whitelist rename for SC2016 (fixes #1199)
2018-05-13 16:56:09 -07:00
Vidar Holen
46c10c1571 Merge branch 'master' into 1199 2018-05-13 16:55:53 -07:00
Vidar Holen
1de8ba0210 Merge pull request #1208 from ngzhian/1186
Add unset to list of commands exempt from 2016
2018-05-13 16:47:29 -07:00
Vidar Holen
407f6a63b9 Merge branch 'master' into 1186 2018-05-13 16:47:18 -07:00
Vidar Holen
7ee7448a70 Merge pull request #1221 from ngzhian/1192
Assignments are okay in SC2094 (fixes #1192)
2018-05-13 16:44:11 -07:00
Vidar Holen
48ebd41e22 Merge pull request #1222 from ngzhian/196
Suppress SC2016 for git filter-branch (fixes #196)
2018-05-13 15:45:20 -07:00
Ng Zhi An
0c88fbc76d Suppress SC2016 for git filter-branch (fixes #196) 2018-05-13 15:18:55 -07:00
Ng Zhi An
b3362f1dc3 Assignments are okay in SC2094 (fixes #1192) 2018-05-13 15:17:32 -07:00
Ng Zhi An
2c6bc43614 Add Data.Monoid 2018-05-13 11:44:21 -07:00
Vidar Holen
235bf6605f Merge pull request #1205 from ngzhian/remove-unused
Remove unused code
2018-05-12 19:15:42 -07:00
Vidar Holen
7029a713c7 Merge pull request #1207 from ngzhian/1184
When given a %* format string, expect one more argument
2018-05-12 18:50:23 -07:00
Vidar Holen
cf608dc2f6 Parse FD move operations like 2>&1- correctly. Fixes #1180. 2018-05-12 18:30:35 -07:00
Vidar Holen
aa3b3fdc56 Make .ghci look in ./src 2018-05-12 17:47:21 -07:00
Vidar Holen
bca2ad4e18 Don't think declare -x -F var is used (fixes #1209). 2018-05-12 17:34:23 -07:00
Vidar Holen
719e1854e5 Clarify 'export' suggestion in SC2034 (unused vars). 2018-05-11 21:39:54 -07:00
Ng Zhi An
20ad7dc8de Add unset to list of commands exempt from 2016
Fixes #1186
2018-05-06 16:45:57 -07:00
Ng Zhi An
f84859ab90 When given a %* format string, expect one more argument
Fixes #1184
2018-05-06 16:39:51 -07:00
Ng Zhi An
08235a1cb2 Change to aeson (fixes #1085)
Adds bytestring as a dependency for putStrLn encoded values.
2018-05-06 16:07:53 -07:00
Ng Zhi An
728922d2b8 Remove unused code 2018-05-06 15:24:34 -07:00
Ng Zhi An
a953dd3454 Whitelist rename for SC2016 (fixes #1199) 2018-05-06 10:28:17 -07:00
Vidar Holen
ef6a5b97b9 Refactor sudo checks into CommandChecks 2018-04-30 22:59:23 -07:00
Vidar Holen
8873a1732b Merge pull request #1195 from sdknudsen/master
Warn about invalid arguments to sudo
2018-04-30 21:50:28 -07:00
Stefan Knudsen
5adfce72e1 Warn about invalid arguments to sudo 2018-04-29 01:16:31 -04:00
Vidar Holen
12b3fdf661 Update gallery of bad code 2018-04-28 12:58:44 -07:00
Vidar Holen
bb4ce86fab Account for array index in SC2154 ${var:?} (fixes #1166) 2018-04-28 12:09:54 -07:00
Vidar Holen
7ec2fa2d3e Remove 'nice' from list of non-reading commands (fixes #1169) 2018-04-28 11:39:42 -07:00
Vidar Holen
5481ccd7f7 Warn about elseif or elsif as command names (fixes #1177) 2018-04-27 22:23:37 -07:00
Vidar Holen
a1d8947297 Fix broken test stripping 2018-04-22 14:57:43 -07:00
Vidar Holen
683a30abde Merge pull request #1174 from rasa/patch-1
Added scoop install
2018-04-22 14:13:35 -07:00
Vidar Holen
573936f353 Merge pull request #1185 from 0mp/master
Add FreeBSD installation instructions
2018-04-22 14:10:59 -07:00
Vidar Holen
ce7658ed86 Merge pull request #1168 from vmchale/master
Bump to ghc 8.4.1
2018-04-22 14:10:25 -07:00
Mateusz Piotrowski
0136d9ccce Add FreeBSD installation instructions 2018-04-19 13:25:25 +02:00
Ross Smith II
a4c6cea5e6 Added scoop install
See https://github.com/lukesampson/scoop/blob/master/bucket/shellcheck.json
2018-04-07 16:22:29 -07:00
Vidar Holen
32af2783f0 Allow stripping unit tests 2018-04-02 21:14:23 -07:00
Vanessa McHale
08aab3c161 hlints 2018-04-02 11:48:21 -05:00
Vanessa McHale
cf39adff75 bump to latest ghc 2018-04-02 11:44:18 -05:00
Vidar Holen
da4072a118 Blacklist base 4.6.0.1 to disable GHC 7.6.3 (#1131) 2018-03-29 21:41:00 -07:00
Vidar Holen
08d2eef411 Whitelist docker for SC2016 about '$var'. Fixes #1161 2018-03-29 19:27:34 -07:00
Vidar Holen
de257a6cf3 Merge pull request #1164 from Lin-Buo-Ren/improve-snap-package
Improve snap packaging
2018-03-29 11:49:26 -07:00
林博仁(Buo-Ren Lin)
68c24925bc Add info on connecting to removable-media interface in snap description
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 23:47:59 +08:00
林博仁(Buo-Ren Lin)
366dc5d3f8 Add snap install instructions to README
Currently shellcheck is only provided by the edge channel, should remove the --channel argument after it is in candidate/stable.

Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:24:31 +08:00
林博仁(Buo-Ren Lin)
1ed743e410 Add snapcraft generated files to the Git tracking ignore rules
This patches uses the following gitignore syntax so that only entries in the root folder is ignored, it is suggested to apply it to existing rules as well.

```
A leading slash matches the beginning of the pathname. For
example, "/*.c" matches "cat-file.c" but not
"mozilla-sha1/sha1.c".
```

Refer-to: gitignore(5) manpage
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:05:52 +08:00
林博仁(Buo-Ren Lin)
9a2aad16ad Add removable-media plug so that scripts in removable media can be checked
Otherwise it will be blocked by Apparmor with the following message:

```
$ shellcheck script
audit: type=1400 audit(TIMESTAMP): apparmor="DENIED" operation="open" profile="snap.shellcheck.shellcheck" name=2F6D656469612F4C696E2D42756F2D52656E2F57696E646F7773205553422F717569636B72756E pid=10175 comm="shellcheck" requested_mask="r" denied_mask="r" fsuid=FSUID ouid=OUID
script: script: openBinaryFile: permission denied (Permission denied)
```

NOTE:

* This plug is not Auto-connect plug, it has to be manually connected by user with `snap connect shellcheck:removable-media :removable-media`
* Currently files under /mnt is not checkable as snapd doesn't provide an interface for it for now.

Refer-to: Interfaces reference - Snaps are universal Linux packages <https://docs.snapcraft.io/reference/interfaces>
Signed-off-by: 林博仁(Buo-Ren Lin) <Buo.Ren.Lin@gmail.com>
2018-03-29 20:00:23 +08:00
Vidar Holen
177cb10daa Enable strict mode with home access 2018-03-28 18:26:16 -07:00
Vidar Holen
4aca1ff128 Warn when printf arg count is not a multiple of format count 2018-03-28 08:57:38 -07:00
Vidar Holen
ffed7caff4 quickrun script now works with new source layout 2018-03-28 08:57:38 -07:00
Vidar Holen
ef28200199 Merge pull request #1159 from geirha/patch-1
Consider type a valid command in sh
2018-03-27 11:38:07 -07:00
Geir Hauge
55216792c9 Consider type a valid command in sh
[type](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/type.html) *is* defined by POSIX, and even the bourne shell has the `type` command.
2018-03-25 12:11:57 +02:00
Vidar Holen
51e115cf47 Attempt to fix snap build 2018-03-24 17:43:32 -07:00
Vidar Holen
764b242f1b Suggest quoting expansions in for loop globs 2018-03-24 17:43:20 -07:00
Vidar Holen
c3b606c68a Encourage users to use stable rather than latest 2018-03-21 17:55:21 -07:00
Vidar Holen
9f53109dfa Fix tagging of 'stable' 2018-03-21 17:54:36 -07:00
Vidar Holen
795a881219 Fix docker image workdir and add test 2018-03-21 09:39:06 -07:00
Vidar Holen
6dd5350e3b Merge pull request #1143 from pratikmallya/simplify_dockerbuild
Simplify Dockerfile
2018-03-20 22:31:36 -07:00
Vidar Holen
a5b359591c Merge branch 'master' into simplify_dockerbuild 2018-03-20 22:31:21 -07:00
Vidar Holen
966194e387 Merge branch 'pratikmallya-simplify_dockerbuild' 2018-03-20 22:10:24 -07:00
Vidar Holen
71bcc80c2f Allow more Docker build caching 2018-03-20 22:09:13 -07:00
Vidar Holen
48616225b3 Merge pull request #1147 from tdmalone/patch-1
README - Travis script for updating shellcheck ver
2018-03-20 19:28:39 -07:00
Tim Malone
99276cb9f5 README - Travis script for updating shellcheck ver
Shellcheck in default Travis builds is currently a version behind
2018-03-19 11:15:28 +11:00
Pratik Mallya
f769d4e92c Add TravisCI Build Status file 2018-03-15 23:13:54 -05:00
Pratik Mallya
71df01c00f Simplify Dockerfile
Use multi stage Dockerfile to greatly simplify build.
2018-03-15 23:12:28 -05:00
Vidar Holen
5364701914 Merge branch 'simplify_dockerbuild' of https://github.com/pratikmallya/shellcheck into pratikmallya-simplify_dockerbuild 2018-03-15 09:53:50 -07:00
Vidar Holen
fb97aca5a6 Merge pull request #1140 from phadej/src
Move library into src/
2018-03-15 16:52:26 +00:00
Pratik Mallya
6b81a9924c Simplify Dockerfile
Use multi stage Dockerfile to greatly simplify build.
2018-03-14 21:43:34 -05:00
Oleg Grenrus
cd7c077ecc Move library into src/ 2018-03-08 19:57:40 +02:00
Oleg Grenrus
b33607b048 Add custom-setup stanza and containers lowerbound
custom-setup:

- http://cabal.readthedocs.io/en/latest/developing-packages.html#custom-setup-scripts
- https://www.well-typed.com/blog/2015/07/cabal-setup-deps/

Bounds:
- containers-0.5 is required if Data.Map.Strict
- parsec-3.0 for Text.Parsec
- json-0.3.6 for makeObj
2018-03-08 19:35:50 +02:00
Vidar Holen
969230f171 MacPorts version appears unmaintained. Remove from docs. 2018-03-08 08:48:03 -08:00
Vidar Holen
a98d69f4ff Update CHANGELOG with $var[ and here doc expansion fixes 2018-03-04 15:42:52 -08:00
Vidar Holen
f71c142a44 Don't ignore parse failures in here documents. Fixes #1135. 2018-03-04 15:24:04 -08:00
Vidar Holen
9dfcf54f10 Functionality for emitting parse errors but still continue 2018-03-04 14:42:47 -08:00
Vidar Holen
c8cd9dd09c Add a debugParseScript for development 2018-03-03 15:36:50 -08:00
Vidar Holen
8b8aeb4409 Rephrase SC2069 (cmd 2>&1 > file) and make it a warning. Fixes #633 2018-03-03 13:33:24 -08:00
Vidar Holen
ee354ffce8 POSIX warning for export -[^p]. Fixes #1130. 2018-02-27 20:38:02 -08:00
Vidar Holen
9fc3ddf849 Fix SC1087 to trigger on any $var[, not just $var[@] 2018-02-25 18:25:47 -08:00
Vidar Holen
ecb9d07f52 Update changelog with associative arrays in (()) fix 2018-02-25 18:15:55 -08:00
Vidar Holen
d16bf41c3d Better support arrays in arithmetic contexts. Fixes #1074 2018-02-25 18:08:38 -08:00
Vidar Holen
8d5e3a80ae Merge pull request #1123 from jonhiggs/check-for-which-usage
Check for calls to `which`.
2018-02-25 17:08:37 -08:00
Vidar Holen
34e0fa53c8 Merge pull request #1125 from jonhiggs/nextnumber-macos-support
MacOS support for nextnumber
2018-02-25 17:08:15 -08:00
Jon Higgs
7fb27310e1 Rely upon /usr/bin/env to find bash
This allows you to use the homebrew install Bash 4 on MacOS systems. It
should compatible with most if not all modern Linux distros.
2018-02-26 11:24:09 +11:00
Jon Higgs
00d3c09ddb Raise error unless interpreter supports globstar 2018-02-26 11:23:42 +11:00
Jon Higgs
e8fc09414a Check for calls to which.
Favour the builtin `command -v` instead.
2018-02-26 10:00:25 +11:00
Vidar Holen
b7a8b090d2 SC2229: Warn about 'read $var' 2018-02-25 13:47:58 -08:00
Vidar Holen
72044a79c6 Update changelog 2018-02-25 13:47:58 -08:00
Vidar Holen
6511dc0246 Add missing import 2018-02-25 13:29:06 -08:00
Vidar Holen
740441f2c4 Merge pull request #1121 from PeterDaveHello/update-travis-ci-doc
[Docs] Update Travis CI part in README.md
2018-02-25 11:37:00 -08:00
Peter Dave Hello
b311563421 Update Travis CI part in README.md 2018-02-20 04:52:12 +08:00
Vidar Holen
6d257bfa17 Warn about 'while!' and 'while:' 2018-02-17 21:58:29 -08:00
Vidar Holen
d8717c7046 s/parser error/syntax error/g 2018-02-17 17:18:05 -08:00
Vidar Holen
7aa3a7ffc3 Improve message for SC2163 (export $var). Helps #1117 2018-02-17 17:12:11 -08:00
Vidar Holen
017af8333f Merge pull request #1112 from Nightfirecat/sc2154-local-test
Add test for `local` keyword in SC2154
2018-02-10 08:04:07 -08:00
Jordan Atwood
f73d6f2332 Add test for local keyword in SC2154
Follow-up to koalaman/shellcheck#988
2018-02-07 17:50:55 -08:00
koalaman
a840f4e464 Merge pull request #1100 from mkhl/SC2029
SC2029: Skip when there are options to ssh
2018-01-24 19:38:38 -08:00
Vidar Holen
0f5e40c076 Mention shellcheck-static for Arch 2018-01-24 09:02:56 -08:00
Martin Kühl
ccaacb108a SC2029: Skip when there are options to ssh
Fixes #327

SC2029 generates false positives when given an ssh command that includes
options with arguments because it assumes the first non-option must be
the host:port argument and the last argument is a command to run.
As suggested the comments on #327, this change fixes those by skipping
the check when there are any options present.
2018-01-24 13:05:22 +01:00
Martin Kühl
56751413b4 SC2029: Add false positive test
This change adds a test case for a valid command that gets falsely
flagged with SC2029.
2018-01-24 13:04:26 +01:00
Vidar Holen
ba5f20deda Fix parsing of escaped chars in regex groups. Fixes #1077 2018-01-21 16:13:16 -08:00
Vidar Holen
c86885427c Warn about comments/blanks before shebang. Fixes #844 2018-01-21 13:57:44 -08:00
Vidar Holen
7b3c4025fb Warn about redirs in the middle of 'find' commands. Fixes #405 2018-01-21 11:12:22 -08:00
Vidar Holen
3b004275cf Add unit test for issue #1091 2018-01-20 11:42:31 -08:00
koalaman
72971fa52b Merge pull request #1097 from sdknudsen/fix/recognize-ids-with-underscores
Use readVariableName combinator
2018-01-20 11:35:22 -08:00
Stefan Knudsen
dbdab5705f Use readVariableName combinator 2018-01-19 16:19:06 -05:00
Vidar Holen
46a3019ed7 Fix annotations for here documents (fixes #1071) 2018-01-17 19:20:10 -08:00
Vidar Holen
81978d15bd Remove unused here doc boundary concept. 2018-01-17 18:48:17 -08:00
Vidar Holen
1d0db9267d Mention SC2224-6 about mv/cp/ln without destination 2018-01-17 18:32:07 -08:00
Vidar Holen
a6fb9d1ef8 Warn about C-style comments 2018-01-17 18:14:36 -08:00
Vidar Holen
dc1e7c1bd4 Make docker image shellcheck-alpine behave more like alpine. 2018-01-14 18:01:18 -08:00
Vidar Holen
5b14dba489 Parse 'else if' correctly, and not like elif. Fixes #1088. 2018-01-13 22:42:19 -08:00
koalaman
ee997fdec4 Merge pull request #1093 from albertodonato/snap-package
Add snapcraft.yaml to build snap package
2018-01-13 19:36:06 -08:00
koalaman
1badeff383 Merge pull request #1092 from etam/patch-1
Update openSUSE instruction
2018-01-13 19:33:18 -08:00
Vidar Holen
2d5ed23ca1 Warn about cp/mv/ln with a single argument. Fixes #1080. 2018-01-13 16:44:58 -08:00
Alberto Donato
ec581cee90 Add snapcraft.yaml to build snap package 2018-01-13 00:31:26 +01:00
Adam Mizerski
bb32289ee3 Update openSUSE instruction
Package is main repo of all versions. No need to add repository.
2018-01-11 18:51:39 +01:00
Vidar Holen
31d6b063d9 Improve indented here doc token message. 2018-01-10 21:12:22 -08:00
Vidar Holen
3c5c74ff04 Add quote warning specific to : ${var=val}. Fixes #1084 2018-01-06 10:53:53 -08:00
koalaman
9657e8dda3 Merge pull request #1078 from CyberShadow/pull-20171221-031403
Extend SC2216/SC2217 with 'true' and 'false'
2017-12-20 20:44:06 -08:00
Vladimir Panteleev
6ed60b403f Extend SC2216/SC2217 with 'true' and 'false' 2017-12-21 03:29:17 +00:00
koalaman
8fa8823981 Merge pull request #1072 from vapier/master
convert http:// URIs to https://
2017-12-14 09:35:57 -08:00
Mike Frysinger
161801a86e convert http:// URIs to https://
Also update the ShellCheck homepage to the new dedicated domain.
2017-12-14 01:06:43 -05:00
33 changed files with 1177 additions and 500 deletions

2
.ghci
View File

@@ -1 +1 @@
:set -idist/build/autogen
:set -idist/build/autogen -isrc

9
.gitignore vendored
View File

@@ -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

View File

@@ -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.

View File

@@ -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;

View File

@@ -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)!

View File

@@ -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"]

View File

@@ -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>.

View File

@@ -1,3 +1,5 @@
[![Build Status](https://travis-ci.org/koalaman/shellcheck.svg?branch=master)](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!

View File

@@ -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

View File

@@ -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

View File

@@ -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 "$@"

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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 ()

View File

@@ -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

View File

@@ -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 }) ) |])

View File

@@ -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

View File

@@ -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 }) ) |])

View File

@@ -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

View File

@@ -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 }) ) |])

View File

@@ -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",

View File

@@ -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 = [

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 #-}

View File

@@ -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
View 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