141 Commits

Author SHA1 Message Date
Vidar Holen
08d1d37094 Stable version 0.4.6
This release is dedicated to Stardew Valley, for fixing the Harvest Moon
unplayable issue where Ann gives up on her inventions after marriage ;_;
2017-03-26 11:48:27 -07:00
Vidar Holen
e28e90133d Add missing import 2017-03-18 16:42:17 -07:00
Vidar Holen
2688a81526 Don't suggest indirection for 'declare var$n=foo' 2017-03-18 14:54:52 -07:00
Vidar Holen
82c3084438 Add HandBrakeCLI to list of stdin swallowing apps 2017-03-18 13:10:54 -07:00
Russ Kubik
c6ff1933b7 Update README.md 2017-03-17 09:32:53 -07:00
Vidar Holen
22c86256ac Decode UTF-8 sequences over 0x10FFFF as latin1 2017-03-10 10:11:05 -08:00
Daniel M. Capella
a3a4873190 readme: Add ALE Vim plugin 2017-03-08 10:10:24 -08:00
Vidar Holen
750212af39 Add less common actions to find -o check. 2017-03-03 20:43:00 -08:00
Vidar Holen
2154583fd3 Don't parse unicode quotes as real quotes. 2017-02-25 15:14:52 -08:00
Vidar Holen
35c74e4747 Getting command name from busybox now gets applet name 2017-02-12 10:56:29 -08:00
Vidar Holen
46fb91b44d Manually decode input files as lenient UTF-8. 2017-01-22 15:24:21 -08:00
Vidar Holen
128d5d6013 Don't warn about grep pattern issues when using -F. 2017-01-21 16:20:55 -08:00
koalaman
41176c23a6 Merge pull request #822 from bittorf/master
SC2164: show two possible variants for circumenting the warning
2017-01-15 15:58:19 -08:00
koalaman
342726f480 Merge pull request #823 from dasilvacontin/patch-1
Add TravisCI Setup to README.md
2017-01-15 15:57:09 -08:00
Vidar Holen
1863f2f12d Warn about += bashism in sh and dash. 2017-01-14 12:40:09 -08:00
Vidar Holen
8809a36952 Warn when finding HTML entities like & 2017-01-14 11:59:31 -08:00
Vidar Holen
7f307c5775 Count | as a regex metacharacter for 2076. 2017-01-12 19:02:46 -08:00
David da Silva
eb12086d05 Fix typos in Travis CI section of the README.md 2017-01-11 11:48:57 +01:00
David da Silva
6259a32601 Add TravisCI Setup to README.md 2017-01-11 10:04:48 +01:00
Bastian Bittorf
4e13c7cbc1 SC2164: show two possible variants for circumenting the warning
always calling 'exit' is not good in e.g. functions.
the basic idea is at least that the returncode of
cd *is* evaluated somehow and not ignored.

Reported-by: Garance Alistair Drosehn <drosehn@rpi.edu>

Signed-off-by: Bastian Bittorf <bittorf@bluebottle.com>
2017-01-10 12:30:47 +01:00
Vidar Holen
edb01fa855 Warn about ( -d foo ) and similar. 2017-01-09 23:49:22 -08:00
koalaman
85e6c35845 Merge pull request #814 from Scorpiokat/master
This commit fixes #797
2017-01-03 21:25:45 -08:00
Ekaterina Efimova
43f667a8f9 Update Analytics.hs 2017-01-03 21:48:35 +03:00
Ekaterina Efimova
daacc98a8f Update Analytics.hs 2017-01-03 21:40:32 +03:00
Scorpiokat
40907b1636 This commit fixes #803 2017-01-03 14:55:12 +00:00
Scorpiokat
96168fc707 This commit fixes #797 2017-01-02 18:01:24 +00:00
Vidar Holen
6aee12a572 Warnings for braces/globs/arrays in [/[[. 2016-12-31 13:18:36 -08:00
Vidar Holen
0048f34b11 Merge branch 'master' of github.com:koalaman/shellcheck 2016-12-30 17:11:32 -08:00
Vidar Holen
e679ff222a Warn when using deprecated egrep/fgrep. 2016-12-30 16:55:31 -08:00
koalaman
6f5648faca Update README.md 2016-12-30 10:31:34 -08:00
Vidar Holen
30e94ea7ab Warn about comparisons and cases that can never match. 2016-12-29 14:04:49 -08:00
Vidar Holen
d8f8a2fa14 Minor 2% parser speedup 2016-12-29 10:56:00 -08:00
Vidar Holen
df3cc70658 Improve code for warning about escaped single quotes. 2016-12-28 21:51:28 -08:00
Vidar Holen
5669702362 Warn about missing and invalid subscripts in array assignments. 2016-12-28 18:58:03 -08:00
Vidar Holen
bd9d05c759 Warn about missing space in [ foo= bar ] 2016-12-27 21:20:59 -08:00
Austin English
af87fe9315 add missing references to dash 2016-12-20 17:10:55 -08:00
koalaman
c2850e436f Merge pull request #804 from austin987/readme2
README.md: sort package managers, add emerge (Gentoo)
2016-12-20 10:16:42 -08:00
Austin English
d73fc8f91d README.md: sort package managers, add emerge (Gentoo) 2016-12-20 12:11:10 -06:00
Vidar Holen
7124c113e8 Don't warn about .sh.version being unused (for Ksh) 2016-12-17 16:03:06 -08:00
Vidar Holen
f594f01d35 Add warnings about redirections without commands. 2016-12-17 15:03:52 -08:00
Vidar Holen
6ccb7e9129 Fix 'for file; do ..' counting $file as a safe variable. 2016-12-17 12:55:14 -08:00
Vidar Holen
838f0ce4dc Count "%(%Y%m%d)T" as a single format in printf. 2016-12-17 12:48:09 -08:00
Vidar Holen
105b09792c Fix SC095 about < /dev/null when using ssh -n 2016-12-17 12:41:00 -08:00
Vidar Holen
f6618d4332 Fix BSD style flag parsing to stop at -- 2016-12-11 10:58:01 -08:00
Vidar Holen
0a381be37b Improve heredoc delimiter handling. 2016-12-11 10:09:13 -08:00
Vidar Holen
46e47dad45 Don't quote source in 2041 and 2043. 2016-12-10 22:41:25 -08:00
Vidar Holen
fee6c94d40 Alias ash to dash with warning. 2016-12-10 10:57:01 -08:00
Vidar Holen
cf1c46d852 Add deprecation warning for tempfile 2016-12-10 09:58:27 -08:00
Vidar Holen
a40efffec9 Explicitly import Data.Monoid 2016-12-10 08:49:07 -08:00
Vidar Holen
44b96fca66 Merge branch 'bug562' 2016-12-03 10:21:49 -08:00
Vidar Holen
b4fb439191 Save string read by T_ParamSubSpecialChar 2016-12-03 10:19:14 -08:00
MortimerMcMire315
0897ab7092 Add handling for special characters in parameter substitutions.
Fixes koalaman/shellcheck#562. Special characters inside braces are
parsed into T_ParamSubSpecialChar instead of T_Literal so that they are
not flagged in the function checkInexplicablyUnquoted when sandwiched
between double quotes.
2016-11-28 16:06:02 -05:00
koalaman
9703f89f79 Merge pull request #774 from oyvindio/tag-releases-in-dockerhub
Push git tags to dockerhub as docker tags
2016-11-28 11:39:48 -08:00
koalaman
aadf02e635 Merge pull request #781 from austin987/patch-1
Add info/link on ignoring issues to README
2016-11-28 11:33:05 -08:00
Øyvind Ingebrigtsen Øvergaard
090051bdc1 Use $TRAVIS_TAG in $TAG if defined 2016-11-27 13:42:49 +01:00
Austin English
b8c96b4361 Add info/link on ignoring issues to README 2016-11-21 19:53:26 -06:00
Vidar Holen
f26038125d Allow spaces/comments before filewide annotations. 2016-11-20 18:06:36 -08:00
Vidar Holen
08f7ff37c5 Some cleanup and refactoring. 2016-11-12 15:51:36 -08:00
Øyvind Ingebrigtsen Øvergaard
60fc33ebdf Push git tags to dockerhub as docker tags 2016-11-11 20:32:05 +01:00
Vidar Holen
3a006f7bcb Use camelcase for cabal commands to align with stack. 2016-11-10 13:13:42 -08:00
koalaman
89b6fd58fa Reformat issue template 2016-10-31 18:20:39 -07:00
koalaman
069ddeffcc Merge pull request #766 from ryanoasis/issue-template
Add an issue template
2016-10-31 17:46:47 -07:00
Ryan L McIntyre
59c4ed106c Create ISSUE_TEMPLATE.md
* Suggestion for an issue template to help with the amount of issues
2016-10-31 20:25:55 -04:00
Vidar Holen
5efb724a3e Stable version 0.4.5
This release is dedicated to Google Inc for four great years of
employment and being good sports about hobby projects like this.
2016-10-21 14:00:50 -07:00
Vidar Holen
619b6c42f3 Improve parsing of fd close/duplicate redirections. 2016-10-21 11:31:58 -07:00
Vidar Holen
88c56ecd53 Allow unrecognized directives with warnings. 2016-10-14 12:14:20 -07:00
Vidar Holen
6b62b5bf7e Don't warn about [ a '>' b ] needing escapes. 2016-10-01 14:54:28 -07:00
Vidar Holen
8672af29ef Split duplicate SC1009 into SC1014 for if [ grep foo bar ] 2016-10-01 13:34:14 -07:00
Vidar Holen
1a8e34bfea Don't suggest grep -c when used with -o 2016-10-01 13:26:53 -07:00
Vidar Holen
b0dae063bf Add info when using 'find' without path 2016-09-25 11:56:32 -07:00
Vidar Holen
4fc4899803 Consider ${foo:=bar} an assignment. 2016-09-24 19:01:13 -07:00
Vidar Holen
cd4896192c Don't consider ~foo constant. 2016-09-24 15:32:44 -07:00
Vidar Holen
868d53af95 Warn about passing globs to unset. 2016-09-24 14:49:52 -07:00
Vidar Holen
6a4b86cbea Fix warning for >& 2016-09-24 14:08:00 -07:00
Vidar Holen
fe2398edc9 Warn about >& in sh 2016-09-24 14:03:54 -07:00
Vidar Holen
3a7dc86de1 Don't warn about unused vars with readonly -f 2016-09-24 13:42:20 -07:00
koalaman
1c0ec9c6f6 Merge pull request #734 from NLKNguyen/add_CI_CD_solution_with_Docker
Add CI/CD solution with Docker
2016-09-11 23:42:06 -07:00
NLKNguyen
84110dbef4 Change DOCKER_REPO value and add test runner 2016-09-08 23:57:46 -07:00
NLKNguyen
1a7e98beaf Use cleaner escaping method 2016-09-08 21:58:51 -07:00
NLKNguyen
a5d5831a2e Fix syntax error with traditional if-clause instead of escaping bracket 2016-09-08 21:41:06 -07:00
NLKNguyen
47201822f9 Change syntax style for readability 2016-09-08 21:31:23 -07:00
NLKNguyen
32689ef5eb Test Dockerfiles and Travis CI on downstream repos 2016-09-08 21:05:54 -07:00
Vidar Holen
87481dce25 Warn about printf hello world and printf "%s %s" foo 2016-09-06 21:16:59 -07:00
Vidar Holen
a90b6d14b3 Count b as a reference in ${a:b} 2016-09-05 14:01:53 -07:00
Vidar Holen
5a46eeb09a Allow #inline comments without SC2046. 2016-09-05 12:38:35 -07:00
Vidar Holen
47a7065a7a Add style note for 'mycmd; if [ $? -eq 0 ]'. 2016-08-28 20:54:08 -07:00
koalaman
dbafbb3b3b Merge pull request #722 from kpankonen/docker
add Dockerfile that will build shellcheck
2016-08-07 11:43:37 -07:00
Vidar Holen
13a2070a32 Support multidimensional KSH arrays and warn in Bash. 2016-08-06 18:40:08 -07:00
Kevin Pankonen
fa4874c044 add Dockerfile that will build shellcheck 2016-08-05 16:42:21 -07:00
Vidar Holen
6a71ff6f46 Don't suggest removing $ in (( ${COLUMNS-80} )) 2016-07-30 10:42:33 -07:00
Vidar Holen
36263fb3f5 s/range/class/ when warning about tr '[abc]' 2016-07-05 08:51:40 -07:00
Vidar Holen
6dc419bbf5 Improve warning for 'else if'. 2016-07-02 15:40:29 -07:00
Vidar Holen
7af3470a91 Improve parser errors when reparsing array indices. 2016-07-01 22:06:50 -07:00
Vidar Holen
42f7479fb8 Don't warn about missing shebang when using directives. 2016-07-01 22:02:06 -07:00
Vidar Holen
50084c06c5 Don't warn when $(seq) is used unquoted. 2016-07-01 21:26:46 -07:00
Vidar Holen
e3bef9dc97 Warn about (( 1 -lt 2 )) 2016-07-01 20:33:07 -07:00
Vidar Holen
6c1abb2dee Performance: make readDollarExpr fail early if no $ 2016-06-30 10:01:03 -07:00
Vidar Holen
43c26061b9 Improve parsing for ambiguous $((foo) ) and ((foo) ). 2016-06-26 22:13:48 -07:00
Vidar Holen
07fd5724b8 Recognize declare -A statements when value is inlined. 2016-06-26 14:57:52 -07:00
Vidar Holen
eb2472ada8 Merge branch 'master' of github.com:koalaman/shellcheck 2016-06-26 14:40:43 -07:00
Vidar Holen
3e5ecaa262 Parse indices of associative arrays properly 2016-06-26 14:39:49 -07:00
koalaman
e1cec6c5d3 Merge pull request #694 from eatnumber1/end_column
Emit the end line in the JSON
2016-06-18 15:09:58 -07:00
Russell Harmon
eaa319ec57 Emit the end line in the JSON.
This handles the case where the end line is not on the same line as the
start line when using the new end column feature.
2016-06-18 15:06:28 -07:00
koalaman
717b5e91f5 Merge pull request #693 from eatnumber1/end_column
Make SC1035 emit an end column
2016-06-18 15:05:44 -07:00
Russell Harmon
7f5f5b7fb5 Make SC1035 emit a proper end column
Example JSON output:
```
$ shellcheck -s bash -f json /dev/stdin <<< "[[0 -eq 1 ]]"
[{"file":"/tmp/zshNCNwPz","line":1,"column":1,"endColumn":3,"level":"error","code":1035,"message":"You need a space after the [[ and before the ]]."}]
```
2016-06-18 14:59:47 -07:00
Russell Harmon
856d57f7d8 PositionedComment and ParseNote contains end cols.
This change makes PositionedComment and ParseNote contain end columns.
It additionally modifies the JSON formatter to show the end column in an
"endColumn" property. No modifications to the messages shown by any
other formatter have been made.

Currently, all checks set the end column to the start column. It should
now be possible however to start setting the end column in the parser.
Additional work is needed to set the end column during AST analysis.
2016-06-18 14:58:00 -07:00
koalaman
c45e9d4878 Merge pull request #677 from Maffblaster/patch-1
Add Gentoo to supported distribution list.
2016-05-26 12:11:02 -07:00
Matthew Marchese
89c6f6c800 Add Gentoo to supported distribution list.
It was missing from the list. I thought it was best to add it. :)
2016-05-26 09:56:36 -07:00
Vidar Holen
85e69f86eb In (( x = y )), logic to not reference x also grabbed y 2016-05-24 09:12:47 -07:00
Vidar Holen
47fd16b8e8 Stable version 0.4.4
This release is dedicated to AlphaGo.
The second golden age of AI is upon us!
2016-05-15 13:53:37 -07:00
Vidar Holen
1d04754b37 Don't warn about a && b || c in if/while/until. 2016-05-14 17:14:32 -07:00
Vidar Holen
13ff0a7432 Warn when arrays are appended/assigned scalars. 2016-05-14 16:24:18 -07:00
Vidar Holen
40136fe249 Fix parsing of [[ a =~ {$var} ]] 2016-05-08 12:19:25 -07:00
Vidar Holen
86999ded1f Improve 'let' parsing, trigger unused var for ((a=1)) 2016-04-30 13:45:39 -07:00
Vidar Holen
7551a241ad Add missing warnings for {$i..10} similar to {1..$i} 2016-04-23 16:00:56 -07:00
Vidar Holen
2f0ae44de4 Fix parsing of here documents 2016-04-16 19:14:02 -07:00
Vidar Holen
51d8caf2c9 Add missing import 2016-04-16 09:53:15 -07:00
Vidar Holen
f835c2d4c1 Fix handling of spaces in shebangs. 2016-04-16 09:42:07 -07:00
Vidar Holen
db0c8c2dc9 Separate out command specific checks.
The checks use a better interface and give
an overall speed boost of 10%.
2016-04-10 17:01:40 -07:00
Vidar Holen
9911470d67 Don't warn about LINENO in dash 2016-04-09 09:56:07 -07:00
Vidar Holen
a5821c3a4d s/sh/bash/ in SC2176 because of posix guarantees. 2016-03-19 16:47:55 -07:00
Vidar Holen
c91083354f Warn about timing pipelines and compound commands in sh/dash. 2016-03-19 16:14:55 -07:00
Vidar Holen
2957fb64c9 Allow parsing 'time ( foo )' 2016-03-19 16:13:54 -07:00
koalaman
459e30804f Merge pull request #622 from Arguggi/master
Add stack support
2016-03-19 14:50:48 -07:00
koalaman
49569e10e6 Merge pull request #628 from benmwebb/patch-1
Fix a handful of typos.
2016-03-11 09:02:02 -08:00
Ben Webb
ba0221a1da Fix a handful of typos. 2016-03-10 21:27:07 -08:00
Vidar Holen
944313c6ba Directives after the shebang now apply to the entire script.
Also adds support for the shell= directive.
2016-03-08 20:16:16 -08:00
Vidar Holen
6af1aeb259 Add warning for multi-digit FDs in posix/dash. 2016-03-08 17:37:12 -08:00
Arguggi
b7c9d23452 Add stack support 2016-03-02 15:38:02 +01:00
koalaman
e792d69293 Merge pull request #608 from cs-shadow/master
Fix broken links in README
2016-02-17 09:25:56 -08:00
Chandan Singh
4d8f2eb707 Fix broken links in README
Presently, the links pointed to by "GCC error compatibility" and
"CheckStyle compatible XML output" are broken due to improper
way of referring files. This change fixes those links.

Also, this commit removes some trailing whitespaces in README.
2016-02-16 18:09:15 +00:00
Vidar Holen
8a3bd25f7c Improve error for missing final ) in $((foo) 2016-02-07 14:16:40 -08:00
Vidar Holen
825c1b5d22 Support parsing $((( as $( ((, with warning. 2016-02-06 22:19:29 -08:00
Vidar Holen
92473b512a Add warning for trailing spaces after \ breaks. 2016-02-01 20:20:34 -08:00
Vidar Holen
7e75d12ce1 Improve expansion in single quote false positives. 2016-01-26 19:57:18 -08:00
Vidar Holen
7d278c3ca1 Ignore SC2055 if rhs of either != is a glob. 2016-01-26 19:12:47 -08:00
Vidar Holen
5f1175fb58 Add special case warning to quote in eval echo {1..$n} 2016-01-25 18:46:55 -08:00
koalaman
257b794322 Merge pull request #578 from jwilk/master
Fix typo
2016-01-19 17:08:32 -08:00
Jakub Wilk
89572d3a96 Fix typo 2016-01-20 01:00:22 +01:00
koalaman
15edcbd4d5 Merge pull request #577 from neil-greenwood/readme-sed-quote
Fix missing quote in README.md
2016-01-18 09:00:53 -08:00
Neil Greenwood
736febaa3c Update README.md
Fix missing quote in `sed` example.
2016-01-18 14:53:15 +00:00
26 changed files with 3216 additions and 1575 deletions

27
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,27 @@
#### For bugs
- Rule Id (if any, e.g. SC1000):
- My shellcheck version (`shellcheck --version` or "online"):
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
- [ ] It's not reproducible on shellcheck.net, but I think that's because it's an OS, configuration or encoding issue
#### For new checks and feature suggestions
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
#### Here's a snippet or screenshot that shows the problem:
```sh
#!/your/interpreter
your script here
```
#### Here's what shellcheck currently says:
#### Here's what I wanted or expected to see:

2
.gitignore vendored
View File

@@ -12,4 +12,4 @@ cabal-dev
.cabal-sandbox/
cabal.sandbox.config
cabal.config
.stack-work

21
.travis.yml Normal file
View File

@@ -0,0 +1,21 @@
sudo: required
language: sh
services:
- docker
before_install:
- export DOCKER_REPO=koalaman/shellcheck
- |-
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || ([ -n "$TRAVIS_TAG" ] && echo "$TRAVIS_TAG") || echo "$TRAVIS_BRANCH")
script:
- docker build -t builder -f Dockerfile_builder .
- docker run --rm -it -v $(pwd):/mnt builder
- docker build -t $DOCKER_REPO:$TAG .
after_success:
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- |-
([ "$TRAVIS_BRANCH" == "master" ] || [ -n "$TRAVIS_TAG" ]) && docker push "$DOCKER_REPO:$TAG"

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM alpine:latest
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
COPY package/bin/shellcheck /usr/local/bin/
COPY package/lib/ /usr/local/lib/
RUN ldconfig /usr/local/lib
WORKDIR /mnt
ENTRYPOINT ["shellcheck"]

54
Dockerfile_builder Normal file
View File

@@ -0,0 +1,54 @@
FROM mitchty/alpine-ghc:latest
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
RUN apk add --no-cache build-base
RUN mkdir -p /usr/src/shellcheck
WORKDIR /usr/src/shellcheck
# # ------------------------------------------------------------
# # Build & Test
# # ------------------------------------------------------------
# Obtain the dependencies first, which are less likely to change, in order to reduce
# subsequent build time by leveraging image cache. This benefits developers when they
# build their code with this image locally. In case of Travis CI, this doesn't help
# reduce building time because Travis CI doesn't use cache.
COPY ShellCheck.cabal .
RUN cabal update && cabal install --only-dependencies
# Copy the rest of the source files, including ShellCheck.cabal again but doesn't matter
COPY . .
# Build
RUN cabal install
# Test
RUN cabal test
# # ------------------------------------------------------------
# # Set PATH
# # ------------------------------------------------------------
# Add runtime path to easily reach the executable file. This only exists during build.
ENV PATH "/root/.cabal/bin:$PATH"
# Make it permanent for someone who login to the container of this image
RUN echo "export PATH=${PATH}" >> /etc/profile
# # ------------------------------------------------------------
# # Extract Binaries
# # ------------------------------------------------------------
# Get shellcheck binary
RUN mkdir -p /package/bin/
RUN cp $(which shellcheck) /package/bin/
# Get shared libraries using magic
RUN mkdir -p /package/lib/
RUN ldd $(which shellcheck) | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /package/lib/
# Copy shellcheck package out to mounted directory
CMD ["cp", "-avr", "/package", "/mnt/"]

View File

@@ -7,14 +7,14 @@ ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell
The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues
that causes a shell to give cryptic error messages.
that cause a shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems
that causes a shell to behave strangely and counter-intuitively.
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.
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
@@ -36,9 +36,9 @@ Run `shellcheck yourscript` in your terminal for instant output, as seen above.
You can see ShellCheck suggestions directly in a variety of editors.
* Vim, through [Syntastic](https://github.com/scrooloose/syntastic):
* Vim, through [ALE](https://github.com/w0rp/ale) or [Syntastic](https://github.com/scrooloose/syntastic):
![Screenshot of vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
![Screenshot of Vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
@@ -48,14 +48,27 @@ You can see ShellCheck suggestions directly in a variety of editors.
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
* Most other editors, through [GCC error compatibility](blob/master/shellcheck.1.md#user-content-formats).
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
#### In your build or test suites
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
Use ShellCheck's exit code, or it's [CheckStyle compatible XML output](blob/master/shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
## Travis CI Setup
If you want to use ShellCheck in Travis CI, setting it up is simple :tada:.
```yml
language: bash
addons:
apt:
sources:
- debian-sid # Grab ShellCheck from the Debian repo
packages:
- shellcheck
```
## Installing
@@ -64,12 +77,21 @@ The easiest way to install ShellCheck locally is through your package manager.
On systems with Cabal (installs to `~/.cabal/bin`):
cabal update
cabal install shellcheck
cabal install ShellCheck
On Debian based distros:
apt-get install shellcheck
On Gentoo based distros:
emerge --ask shellcheck
On EPEL based distros:
yum -y install epel-release
yum install ShellCheck
On Fedora based distros:
dnf install ShellCheck
@@ -95,15 +117,18 @@ add OBS devel:languages:haskell repository from https://build.opensuse.org/proje
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
From Docker Hub:
docker pull koalaman/shellcheck
## Compiling from source
This sections describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
#### Installing Cabal
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `yum`, `zypper` or `brew`).
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/
@@ -174,11 +199,13 @@ ShellCheck can recognize many types of incorrect test statements.
[[ -e *.mpg ]] # Existence checks of globs
[[ $foo==0 ]] # Always true due to missing spaces
[[ -n "$foo " ]] # Always true due to literals
[[ $foo =~ "fo+" ]] # Quoted regex in =~
[[ $foo =~ "fo+" ]] # Quoted regex in =~
[ foo =~ re ] # Unsupported [ ] operators
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
[ $n && $m ] # && in [ .. ]
[ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..))
#### Frequently misused commands
@@ -207,9 +234,11 @@ ShellCheck recognizes many common beginner's syntax errors:
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 "Argument 10 is $10" # Positional parameter misreference
if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if'
#### Style
@@ -219,7 +248,7 @@ ShellCheck can make suggestions to improve style:
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
a >> log; b >> log; c >> log # Use a redirection block instead
echo "The time is `date`" # Use $() instead
cd dir; process *; cd ..; # Use subshells instead
cd dir; process *; cd ..; # Use subshells instead
echo $[1+2] # Use standard $((..)) instead of old $[]
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
echo "$(date)" # Useless use of echo
@@ -232,7 +261,8 @@ ShellCheck can recognize issues related to data and typing:
args="$@" # Assigning arrays to strings
files=(foo bar); echo "$files" # Referencing arrays as strings
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays.
declare -A arr=(foo bar) # Associative arrays without index
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
[[ $# > 2 ]] # Comparing numbers as strings
var=World; echo "Hello " var # Unused lowercase variables
echo "Hello $name" # Unassigned lowercase variables
@@ -265,7 +295,8 @@ ShellCheck will warn when using features not supported by the shebang. For examp
foo-bar() { ..; } # Undefined/unsupported function name
[ $UID = 0 ] # Variable undefined in dash/sh
local var=value # local is undefined in sh
time sleep 1 | sleep 5 # Undefined uses of 'time'
#### Miscellaneous
@@ -275,11 +306,13 @@ ShellCheck recognizes a menagerie of other issues:
PATH="$PATH:~/bin" # Literal tilde in $PATH
rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings
echo hello \ # Trailing spaces after \
var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/ file > file # Redirecting to input
sed 's/foo/bar/' file > file # Redirecting to input
## Testimonials
@@ -289,17 +322,22 @@ ShellCheck recognizes a menagerie of other issues:
Alexander Tarasikov,
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)
## Ignoring issues
Issues can be ignored via environmental variable, command line, individually or globally within a file:
https://github.com/koalaman/shellcheck/wiki/Ignore
## Reporting bugs
Please use the Github issue tracker for any bugs or feature suggestions:
Please use the GitHub issue tracker for any bugs or feature suggestions:
https://github.com/koalaman/shellcheck/issues
## Contributing
Please submit patches to code or documentation as Github pull requests!
Please submit patches to code or documentation as GitHub pull requests!
Contributions must be licensed under the GNU GPLv3.
The contributor retains the copyright.

View File

@@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.4.3
Version: 0.4.6
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@@ -52,7 +52,10 @@ library
ShellCheck.ASTLib
ShellCheck.Analytics
ShellCheck.Analyzer
ShellCheck.AnalyzerLib
ShellCheck.Checker
ShellCheck.Checks.Commands
ShellCheck.Checks.ShellSupport
ShellCheck.Data
ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle

View File

@@ -21,6 +21,7 @@ module ShellCheck.AST where
import Control.Monad
import Control.Monad.Identity
import Text.Parsec
import qualified ShellCheck.Regex as Re
data Id = Id Int deriving (Show, Eq, Ord)
@@ -32,8 +33,10 @@ data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
data Root = Root Token
data Token =
TA_Binary Id String Token Token
| TA_Assignment Id String Token Token
| TA_Expansion Id [Token]
| TA_Index Id Token
| TA_Sequence Id [Token]
@@ -42,15 +45,17 @@ data Token =
| TC_And Id ConditionType String Token Token
| TC_Binary Id ConditionType String Token Token
| TC_Group Id ConditionType Token
| TC_Noary Id ConditionType Token
| TC_Nullary Id ConditionType Token
| TC_Or Id ConditionType String Token Token
| TC_Unary Id ConditionType String Token
| T_AND_IF Id
| T_AndIf Id (Token) (Token)
| T_Arithmetic Id Token
| T_Array Id [Token]
| T_IndexedElement Id Token Token
| T_Assignment Id AssignmentMode String (Maybe Token) Token
| T_IndexedElement Id [Token] Token
-- Store the index as string, and parse as arithmetic or string later
| T_UnparsedIndex Id SourcePos String
| T_Assignment Id AssignmentMode String [Token] Token
| T_Backgrounded Id Token
| T_Backticked Id [Token]
| T_Bang Id
@@ -95,6 +100,7 @@ data Token =
| T_IfExpression Id [([Token],[Token])] [Token]
| T_In Id
| T_IoFile Id Token Token
| T_IoDuplicate Id Token String
| T_LESSAND Id
| T_LESSGREAT Id
| T_Lbrace Id
@@ -105,6 +111,7 @@ data Token =
| T_NormalWord Id [Token]
| T_OR_IF Id
| T_OrIf Id (Token) (Token)
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
| T_ProcSub Id String [Token]
| T_Rbrace Id
@@ -129,7 +136,11 @@ data Token =
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
deriving (Show)
data Annotation = DisableComment Integer | SourceOverride String deriving (Show, Eq)
data Annotation =
DisableComment Integer
| SourceOverride String
| ShellOverride String
deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
-- This is an abomination.
@@ -140,7 +151,7 @@ tokenEquals a b = kludge a == kludge b
instance Eq Token where
(==) = tokenEquals
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
analyze f g i =
round
where
@@ -148,7 +159,7 @@ analyze f g i =
f t
newT <- delve t
g t
return . i $ newT
i newT
roundAll = mapM round
roundMaybe Nothing = return Nothing
@@ -162,7 +173,7 @@ analyze f g i =
dll l m v = do
x <- roundAll l
y <- roundAll m
return $ v x m
return $ v x y
d1 t v = do
x <- round t
return $ v x
@@ -181,14 +192,18 @@ analyze f g i =
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num
delve (T_HereString id word) = d1 word $ T_HereString id
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
delve (T_Assignment id mode var index value) = do
a <- roundMaybe index
delve (T_Assignment id mode var indices value) = do
a <- roundAll indices
b <- round value
return $ T_Assignment id mode var a b
delve (T_Array id t) = dl t $ T_Array id
delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id
delve (T_IndexedElement id indices t) = do
a <- roundAll indices
b <- round t
return $ T_IndexedElement id a b
delve (T_Redirecting id redirs cmd) = do
a <- roundAll redirs
b <- round cmd
@@ -243,9 +258,10 @@ analyze f g i =
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
delve (TA_Sequence id l) = dl l $ TA_Sequence id
delve (TA_Trinary id t1 t2 t3) = do
@@ -304,8 +320,10 @@ getId t = case t of
T_DollarBraced id _ -> id
T_DollarArithmetic id _ -> id
T_BraceExpansion id _ -> id
T_ParamSubSpecialChar id _ -> id
T_DollarBraceCommandExpansion id _ -> id
T_IoFile id _ _ -> id
T_IoDuplicate id _ _ -> id
T_HereDoc id _ _ _ _ -> id
T_HereString id _ -> id
T_FdRedirect id _ _ -> id
@@ -338,8 +356,9 @@ getId t = case t of
TC_Group id _ _ -> id
TC_Binary id _ _ _ _ -> id
TC_Unary id _ _ _ -> id
TC_Noary id _ _ -> id
TC_Nullary id _ _ -> id
TA_Binary id _ _ _ -> id
TA_Assignment id _ _ _ -> id
TA_Unary id _ _ -> id
TA_Sequence id _ -> id
TA_Trinary id _ _ _ -> id
@@ -356,10 +375,11 @@ getId t = case t of
T_CoProc id _ _ -> id
T_CoProcBody id _ -> id
T_Include id _ _ -> id
T_UnparsedIndex id _ _ -> id
blank :: Monad m => Token -> m ()
blank = const $ return ()
doAnalysis f = analyze f blank id
doStackAnalysis startToken endToken = analyze startToken endToken id
doTransform i = runIdentity . analyze blank blank i
doAnalysis f = analyze f blank return
doStackAnalysis startToken endToken = analyze startToken endToken return
doTransform i = runIdentity . analyze blank blank (return . i)

View File

@@ -21,7 +21,9 @@ module ShellCheck.ASTLib where
import ShellCheck.AST
import Control.Monad.Writer
import Control.Monad
import Data.Functor
import Data.List
import Data.Maybe
@@ -54,6 +56,8 @@ isGlob _ = False
-- Is this shell word a constant?
isConstant token =
case token of
-- This ignores some cases like ~"foo":
T_NormalWord _ (T_Literal _ ('~':_) : _) -> False
T_NormalWord _ l -> all isConstant l
T_DoubleQuoted _ l -> all isConstant l
T_SingleQuoted _ _ -> True
@@ -83,13 +87,14 @@ oversimplify token =
(T_Glob _ s) -> [s]
(T_Pipeline _ _ [x]) -> oversimplify x
(T_Literal _ x) -> [x]
(T_ParamSubSpecialChar _ x) -> [x]
(T_SimpleCommand _ vars words) -> concatMap oversimplify words
(T_Redirecting _ _ foo) -> oversimplify foo
(T_DollarSingleQuoted _ s) -> [s]
(T_Annotation _ _ s) -> oversimplify s
-- Workaround for let "foo = bar" parsing
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
otherwise -> []
_ -> []
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
@@ -108,8 +113,11 @@ getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags o
-- Get all flags in a GNU way, up until --
getAllFlags = getFlagsUntil (== "--")
-- Get all flags in a BSD way, up until first non-flag argument
getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`))
-- Get all flags in a BSD way, up until first non-flag argument or --
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
-- Check if a command has a flag.
hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd)
-- Given a T_DollarBraced, return a simplified version of the string contents.
@@ -167,6 +175,20 @@ getUnquotedLiteral (T_NormalWord _ list) =
str _ = Nothing
getUnquotedLiteral _ = Nothing
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS
-- or nothing if the word does not end in an unquoted literal.
getTrailingUnquotedLiteral :: Token -> Maybe Token
getTrailingUnquotedLiteral t =
case t of
(T_NormalWord _ list@(_:_)) ->
from (last list)
_ -> Nothing
where
from t =
case t of
(T_Literal {}) -> return t
_ -> Nothing
-- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f
where
@@ -185,6 +207,7 @@ getLiteralStringExt more = g
g (TA_Expansion _ l) = allInList l
g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s
g (T_ParamSubSpecialChar _ s) = return s
g x = more x
-- Is this token a string literal?
@@ -194,6 +217,8 @@ isLiteral t = isJust $ getLiteralString t
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
getWordParts (T_DoubleQuoted _ l) = l
-- TA_Expansion is basically T_NormalWord for arithmetic expressions
getWordParts (TA_Expansion _ l) = concatMap getWordParts l
getWordParts other = [other]
-- Return a list of NormalWords that would result from brace expansion
@@ -206,14 +231,39 @@ braceExpand (T_NormalWord id list) = take 1000 $ do
braceExpand item
part x = return x
-- Maybe get the command name of a token representing a command
getCommandName t =
-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections
getCommand t =
case t of
T_Redirecting _ _ w -> getCommandName w
T_SimpleCommand _ _ (w:_) -> getLiteralString w
T_Annotation _ _ t -> getCommandName t
T_Redirecting _ _ w -> getCommand w
T_SimpleCommand _ _ (w:_) -> return t
T_Annotation _ _ t -> getCommand t
otherwise -> Nothing
-- Maybe get the command name of a token representing a command
getCommandName t = do
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
s <- getLiteralString w
if "busybox" `isSuffixOf` s
then
case rest of
(applet:_) -> getLiteralString applet
_ -> return s
else
return s
-- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date"
getCommandNameFromExpansion :: Token -> Maybe String
getCommandNameFromExpansion t =
case t of
T_DollarExpansion _ [c] -> extract c
T_Backticked _ [c] -> extract c
T_DollarBraceCommandExpansion _ [c] -> extract c
otherwise -> Nothing
where
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
extract _ = Nothing
-- Get the basename of a token representing a command
getCommandBasename = liftM basename . getCommandName
where
@@ -237,8 +287,10 @@ isOnlyRedirection t =
isFunction t = case t of T_Function {} -> True; _ -> False
-- Get the list of commands from tokens that contain them, such as
-- the body of while loops and if statements.
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
-- Get the lists of commands from tokens that contain them, such as
-- the body of while loops or branches of if statements.
getCommandSequences t =
case t of
T_Script _ _ cmds -> [cmds]
@@ -251,3 +303,84 @@ getCommandSequences t =
T_IfExpression _ thens elses -> map snd thens ++ [elses]
otherwise -> []
-- Get a list of names of associative arrays
getAssociativeArrays t =
nub . execWriter $ doAnalysis f t
where
f :: Token -> Writer [String] ()
f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do
name <- getCommandName t
guard $ name == "declare" || name == "typeset"
let flags = getAllFlags t
guard $ elem "A" $ map snd flags
let args = map fst . filter ((==) "" . snd) $ flags
let names = mapMaybe (getLiteralStringExt nameAssignments) args
return $ tell names
f _ = return ()
nameAssignments t =
case t of
T_Assignment _ _ name _ _ -> return name
otherwise -> Nothing
-- A Pseudoglob is a wildcard pattern used for checking if a match can succeed.
-- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which
-- can be proven never to match.
data PseudoGlob = PGAny | PGMany | PGChar Char
deriving (Eq, Show)
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
-- PGMany.
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
wordToPseudoGlob word =
simplifyPseudoGlob <$> concat <$> mapM f (getWordParts word)
where
f x = case x of
T_Literal _ s -> return $ map PGChar s
T_SingleQuoted _ s -> return $ map PGChar s
T_DollarBraced {} -> return [PGMany]
T_DollarExpansion {} -> return [PGMany]
T_Backticked {} -> return [PGMany]
T_Glob _ "?" -> return [PGAny]
T_Glob _ ('[':_) -> return [PGAny]
T_Glob {} -> return [PGMany]
T_Extglob {} -> return [PGMany]
_ -> return [PGMany]
-- Reorder a PseudoGlob for more efficient matching, e.g.
-- f?*?**g -> f??*g
simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]
simplifyPseudoGlob = f
where
f [] = []
f (x@(PGChar _) : rest ) = x : f rest
f list =
let (anys, rest) = span (\x -> x == PGMany || x == PGAny) list in
order anys ++ f rest
order s = let (any, many) = partition (== PGAny) s in
any ++ take 1 many
-- Check whether the two patterns can ever overlap.
pseudoGlobsCanOverlap :: [PseudoGlob] -> [PseudoGlob] -> Bool
pseudoGlobsCanOverlap = matchable
where
matchable x@(xf:xs) y@(yf:ys) =
case (xf, yf) of
(PGMany, _) -> matchable x ys || matchable xs y
(_, PGMany) -> matchable x ys || matchable xs y
(PGAny, _) -> matchable xs ys
(_, PGAny) -> matchable xs ys
(_, _) -> xf == yf && matchable xs ys
matchable [] [] = True
matchable (PGMany : rest) [] = matchable rest []
matchable (_:_) [] = False
matchable [] r = matchable r []
wordsCanBeEqual x y = fromMaybe True $
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,27 @@
-}
module ShellCheck.Analyzer (analyzeScript) where
import ShellCheck.Interface
import ShellCheck.Analytics
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import Data.List
import Data.Monoid
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ShellSupport
-- TODO: Clean up the cruft this is layered on
analyzeScript :: AnalysisSpec -> AnalysisResult
analyzeScript = runAnalytics
analyzeScript spec = AnalysisResult {
arComments =
filterByAnnotation (asScript spec) . nub $
runAnalytics spec
++ runChecker params (checkers params)
}
where
params = makeParameters spec
checkers params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker,
ShellCheck.Checks.ShellSupport.checker
]

727
ShellCheck/AnalyzerLib.hs Normal file
View File

@@ -0,0 +1,727 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.AnalyzerLib where
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 Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
type Analysis = AnalyzerM ()
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
nullCheck = const $ return ()
data Checker = Checker {
perScript :: Root -> Analysis,
perToken :: Token -> Analysis
}
runChecker :: Parameters -> Checker -> [TokenComment]
runChecker params checker = notes
where
root = rootNode params
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
notes = snd $ evalRWS (check $ Root root) params Cache
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
}
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
composeAnalyzers f g x = f x >> g x
data Parameters = Parameters {
variableFlow :: [StackData],
parentMap :: Map.Map Id Token,
shellType :: Shell,
shellTypeSpecified :: Bool,
rootNode :: Token
}
-- TODO: Cache results of common AST ops here
data Cache = Cache {}
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
data StackData =
StackScope Scope
| StackScopeEnd
-- (Base expression, specific position, var name, assigned values)
| Assignment (Token, Token, String, DataType)
| Reference (Token, Token, String)
deriving (Show)
data DataType = DataString DataSource | DataArray DataSource
deriving (Show)
data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger
deriving (Show)
data VariableState = Dead Token String | Alive deriving (Show)
defaultSpec root = AnalysisSpec {
asScript = root,
asShellType = Nothing,
asExecutionMode = Executed
}
pScript s =
let
pSpec = ParseSpec {
psFilename = "script",
psScript = s
}
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
-- For testing. If parsed, returns whether there are any comments
producesComments :: Checker -> String -> Maybe Bool
producesComments c s = do
root <- pScript s
let spec = defaultSpec root
let params = makeParameters spec
return . not . null $ runChecker params c
makeComment :: Severity -> Id -> Code -> String -> TokenComment
makeComment severity id code note =
TokenComment id $ Comment severity code note
addComment note = tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
warn id code str = addComment $ makeComment WarningC id code str
err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str
makeParameters spec =
let params = Parameters {
rootNode = root,
shellType = fromMaybe (determineShell root) $ asShellType spec,
shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root,
variableFlow =
getVariableFlow (shellType params) (parentMap params) root
} in params
where root = asScript spec
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
prop_determineShell4 = determineShell (fromJust $ pScript
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
prop_determineShell5 = determineShell (fromJust $ pScript
"#shellcheck shell=sh\nfoo") == Sh
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
prop_determineShell7 = determineShell (fromJust $ pScript "#! /bin/ash") == Dash
determineShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString
where
forAnnotation t =
case t of
(ShellOverride s) -> return s
_ -> fail ""
getCandidates :: Token -> [Maybe String]
getCandidates t@(T_Script {}) = [Just $ fromShebang t]
getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++
[Just $ fromShebang s]
fromShebang (T_Script _ s t) = executableFromShebang s
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
-- return the shell basename like "bash" or "dash"
executableFromShebang :: String -> String
executableFromShebang = shellFor
where
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
--- Context seeking
getParentTree t =
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
where
pre t = modify (first ((:) t))
post t = do
(_:rest, map) <- get
case rest of [] -> put (rest, map)
(x:_) -> put (rest, Map.insert (getId t) x map)
getTokenMap t =
execState (doAnalysis f t) Map.empty
where
f t = modify (Map.insert (getId t) t)
-- Is this node self quoting for a regular element?
isQuoteFree = isQuoteFreeNode False
-- Is this node striclty self quoting, for array expansions
isStrictlyQuoteFree = isQuoteFreeNode True
isQuoteFreeNode strict tree t =
(isQuoteFreeElement t == Just True) ||
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
where
-- Is this node self-quoting in itself?
isQuoteFreeElement t =
case t of
T_Assignment {} -> return True
T_FdRedirect {} -> return True
_ -> Nothing
-- Are any subnodes inherently self-quoting?
isQuoteFreeContext t =
case t of
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
-- When non-strict, pragmatically assume it's desirable to split here
T_ForIn {} -> return (not strict)
T_SelectIn {} -> return (not strict)
_ -> Nothing
isParamTo tree cmd =
go
where
go x = case Map.lookup (getId x) tree of
Nothing -> False
Just parent -> check parent
check t =
case t of
T_SingleQuoted _ _ -> go t
T_DoubleQuoted _ _ -> go t
T_NormalWord _ _ -> go t
T_SimpleCommand {} -> isCommand t cmd
T_Redirecting {} -> isCommand t cmd
_ -> False
getClosestCommand tree t =
msum . map getCommand $ getPath tree t
where
getCommand t@(T_Redirecting {}) = return t
getCommand _ = Nothing
getClosestCommandM t = do
tree <- asks parentMap
return $ getClosestCommand tree t
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where
go currentId (T_NormalWord id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_DoubleQuoted id [word]:rest)
| currentId == getId word = go id rest
go currentId (T_SimpleCommand _ _ (word:_):_)
| currentId == getId word = True
go _ _ = False
-- A list of the element and all its parents
getPath tree t = t :
case Map.lookup (getId t) tree of
Nothing -> []
Just parent -> getPath tree parent
-- Version of the above taking the map from the current context
-- Todo: give this the name "getPath"
getPathM t = do
map <- asks parentMap
return $ getPath map t
isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child
parents params = getPath (parentMap params)
pathTo t = do
parents <- reader parentMap
return $ getPath parents t
-- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
T_NormalWord id [T_Backticked _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
_ -> False
where
check [x] = not $ isOnlyRedirection x
check _ = False
-- TODO: Replace this with a proper Control Flow Graph
getVariableFlow shell parents t =
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
in reverse stack
where
startScope t =
let scopeType = leadType shell parents t
in do
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
when (assignFirst t) $ setWritten t
endScope t =
let scopeType = leadType shell parents t
in do
setRead t
unless (assignFirst t) $ setWritten t
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
assignFirst (T_ForIn {}) = True
assignFirst (T_SelectIn {}) = True
assignFirst _ = False
setRead t =
let read = getReferencedVariables parents t
in mapM_ (\v -> modify (Reference v:)) read
setWritten t =
let written = getModifiedVariables t
in mapM_ (\v -> modify (Assignment v:)) written
leadType shell parents t =
case t of
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
T_Backticked _ _ -> SubshellScope "`..` expansion"
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
T_Subshell _ _ -> SubshellScope "(..) group"
T_CoProcBody _ _ -> SubshellScope "coproc"
T_Redirecting {} ->
if fromMaybe False causesSubshell
then SubshellScope "pipeline"
else NoneScope
_ -> NoneScope
where
parentPipeline = do
parent <- Map.lookup (getId t) parents
case parent of
T_Pipeline {} -> return parent
_ -> Nothing
causesSubshell = do
(T_Pipeline _ _ list) <- parentPipeline
if length list <= 1
then return False
else if lastCreatesSubshell
then return True
else return . not $ (getId . head $ reverse list) == getId t
lastCreatesSubshell =
case shell of
Bash -> True
Dash -> True
Sh -> True
Ksh -> False
getModifiedVariables t =
case t of
T_SimpleCommand _ vars [] ->
concatMap (\x -> case x of
T_Assignment id _ name _ w ->
[(x, x, name, dataTypeFrom DataString w)]
_ -> []
) vars
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
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
name <- getLiteralString lhs
return (t, t, name, DataString $ SourceFrom [rhs])
T_DollarBraced _ l -> maybeToList $ do
let string = bracedString t
let modifier = getBracedModifier string
guard $ ":=" `isPrefixOf` modifier
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
t@(T_CoProc _ name _) ->
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable
T_ForIn id str [] _ -> [(t, t, str, DataString $ SourceExternal)]
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> []
isClosingFileOp op =
case op of
T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
T_IoFile _ (T_LESSAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
_ -> False
-- Consider 'export/declare -x' a reference, since it makes the var available
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
case x of
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if any (`elem` flags) ["x", "p"]
then concatMap getReference rest
else []
"readonly" ->
if any (`elem` flags) ["f", "p"]
then []
else concatMap getReference rest
"trap" ->
case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
_ -> []
_ -> []
where
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
getReference _ = []
flags = map snd $ getAllFlags base
getReferencedVariableCommand _ = []
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
case x of
"read" ->
let params = map getLiteral rest in
catMaybes . takeWhile isJust . reverse $ params
"getopts" ->
case rest of
opts:var:_ -> maybeToList $ getLiteral var
_ -> []
"let" -> concatMap letParamToLiteral rest
"export" ->
if "f" `elem` flags then [] else concatMap getModifierParamString rest
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
"typeset" -> declaredVars
"local" -> concatMap getModifierParamString rest
"readonly" ->
if any (`elem` flags) ["f", "p"]
then []
else concatMap getModifierParamString rest
"set" -> maybeToList $ do
params <- getSetParams rest
return (base, base, "@", DataString $ SourceFrom params)
"printf" -> maybeToList $ getPrintfVariable rest
"mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest
_ -> []
where
flags = map snd $ getAllFlags base
stripEquals s = let rest = dropWhile (/= '=') s in
if rest == "" then "" else tail rest
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
stripEqualsFrom t = t
declaredVars = concatMap (getModifierParam defaultType) rest
where
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
getLiteral t = do
s <- getLiteralString t
when ("-" `isPrefixOf` s) $ fail "argument"
return (base, t, s, DataString SourceExternal)
getModifierParamString = getModifierParam DataString
getModifierParam def t@(T_Assignment _ _ name _ value) =
[(base, t, name, dataTypeFrom def value)]
getModifierParam def t@(T_NormalWord {}) = maybeToList $ do
name <- getLiteralString t
guard $ isVariableName name
return (base, t, name, def SourceDeclaration)
getModifierParam _ _ = []
letParamToLiteral token =
if var == ""
then []
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
getSetParams (t:rest) =
let s = getLiteralString t in
case s of
Just "--" -> return rest
Just ('-':_) -> getSetParams rest
_ -> return (t:fromMaybe [] (getSetParams rest))
getSetParams [] = Nothing
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
f (_:rest) = f rest
f [] = fail "not found"
-- mapfile has some curious syntax allowing flags plus 0..n variable names
-- where only the first non-option one is used if any. Here we cheat and
-- just get the last one, if it's a variable name.
getMapfileArray base arguments = do
lastArg <- listToMaybe (reverse arguments)
name <- getLiteralString lastArg
guard $ isVariableName name
return (base, lastArg, name, DataArray SourceExternal)
getModifiedVariableCommand _ = []
getIndexReferences s = fromMaybe [] $ do
match <- matchRegex re s
index <- match !!! 0
return $ matchAllStrings variableNameRegex index
where
re = mkRegex "(\\[.*\\])"
getOffsetReferences mods = fromMaybe [] $ do
match <- matchRegex re mods
offsets <- match !!! 0
return $ matchAllStrings variableNameRegex offsets
where
re = mkRegex "^ *:(.*)"
getReferencedVariables parents t =
case t of
T_DollarBraced id l -> let str = bracedString t in
(t, t, getBracedReference str) :
map (\x -> (l, l, x)) (
getIndexReferences str
++ getOffsetReferences (getBracedModifier str))
TA_Expansion id _ ->
if isArithmeticAssignment t
then []
else getIfReference t t
T_Assignment id mode str _ word ->
[(t, t, str) | mode == Append] ++ specialReferences str t word
TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op
then concatMap (getIfReference t) [lhs, rhs]
else []
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x
where
-- Try to reduce false positives for unused vars only referenced from evaluated vars
specialReferences name base word =
if name `elem` [
"PS1", "PS2", "PS3", "PS4",
"PROMPT_COMMAND"
]
then
map (\x -> (base, base, x)) $
getVariablesFromLiteralToken word
else []
literalizer (TA_Index {}) = return "" -- x[0] becomes a reference of x
literalizer _ = Nothing
getIfReference context token = maybeToList $ do
str <- getLiteralStringExt literalizer token
guard . not $ null str
when (isDigit $ head str) $ fail "is a number"
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
isArithmeticAssignment t = case getPath parents t of
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
_ -> False
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
--- Command specific checks
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $ do
cmd <- getCommandName token
return $ matcher cmd
isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123"
prop_isVariableName2 = not $ isVariableName "4"
prop_isVariableName3 = not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False
getVariablesFromLiteralToken token =
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 =
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string
where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
prop_getBracedReference1 = getBracedReference "foo" == "foo"
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#"
prop_getBracedReference4 = getBracedReference "##" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!"
prop_getBracedReference6 = getBracedReference "!#" == "#"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == ""
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where
noPrefix = dropPrefix s
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
dropPrefix "" = ""
takeName s = do
let name = takeWhile isVariableChar s
guard . not $ null name
return name
getSpecial (c:_) =
if c `elem` "*@#?-$!" then return [c] else fail "not special"
getSpecial _ = fail "empty"
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
let suffix = dropWhile isVariableChar rest
guard $ suffix /= rest -- e.g. ${!@}
first <- suffix !!! 0
guard $ first `elem` "*?"
return ""
nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do
let var = getBracedReference s
a <- dropModifier s
dropPrefix var a
where
dropPrefix [] t = return t
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
dropPrefix _ _ = []
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
dropModifier x = [x]
-- Useful generic functions
potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ())
headOrDefault _ (a:_) = a
headOrDefault def _ = def
(!!!) list i =
case drop i list of
[] -> Nothing
(r:_) -> Just r
-- Run a command if the shell is in the given list
whenShell l c = do
shell <- asks shellType
when (shell `elem` l ) c
filterByAnnotation token =
filter (not . shouldIgnore)
where
idFor (TokenComment id _) = id
shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ idFor note)
shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns
where
hasNum (DisableComment ts) = num == ts
hasNum _ = False
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
shouldIgnoreFor _ _ = False
parents = getParentTree token
getCode (TokenComment _ (Comment _ c _)) = c
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -39,7 +39,7 @@ import Test.QuickCheck.All
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
position <- Map.lookup id map
return $ PositionedComment position c
return $ PositionedComment position position c
where
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
@@ -65,13 +65,13 @@ checkScript sys spec = do
return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages)
shouldInclude (PositionedComment _ (Comment _ code _)) =
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
code `notElem` csExcludedWarnings spec
sortMessages = sortBy (comparing order)
order (PositionedComment pos (Comment severity code message)) =
order (PositionedComment pos _ (Comment severity code message)) =
(posFile pos, posLine pos, posColumn pos, severity, code, message)
getPosition (PositionedComment pos _) = pos
getPosition (PositionedComment pos _ _) = pos
analysisSpec root =
AnalysisSpec {
@@ -84,7 +84,7 @@ getErrors sys spec =
sort . map getCode . crComments $
runIdentity (checkScript sys spec)
where
getCode (PositionedComment _ (Comment _ code _)) = code
getCode (PositionedComment _ _ (Comment _ code _)) = code
check = checkWithIncludes []
@@ -158,5 +158,25 @@ prop_sourceDirectiveDoesntFollowFile =
[("foo", "source bar"), ("bar", "baz=3")]
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
prop_filewideAnnotation1 = null $
check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
prop_filewideAnnotation2 = null $
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation3 = null $
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation4 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotation5 = null $
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation6 = null $
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation7 = null $
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
prop_filewideAnnotation8 = null $
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
return []
runTests = $quickCheckAll

View File

@@ -0,0 +1,685 @@
{-
Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
-- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker
, ShellCheck.Checks.Commands.runTests
) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import Control.Monad
import Control.Monad.RWS
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data CommandName = Exactly String | Basename String
deriving (Eq, Ord)
data CommandCheck =
CommandCheck CommandName (Token -> Analysis)
verify :: CommandCheck -> String -> Bool
verify f s = producesComments (getChecker [f]) s == Just True
verifyNot f s = producesComments (getChecker [f]) s == Just False
arguments (T_SimpleCommand _ _ (cmd:args)) = args
commandChecks :: [CommandCheck]
commandChecks = [
checkTr
,checkFindNameGlob
,checkNeedlessExpr
,checkGrepRe
,checkTrapQuotes
,checkReturn
,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes
,checkInjectableFindSh
,checkFindActionPrecedence
,checkMkdirDashPM
,checkNonportableSignals
,checkInteractiveSu
,checkSshCommandString
,checkPrintfVar
,checkUuoeCmd
,checkSetAssignment
,checkExportedExpansions
,checkAliasesUsesArgs
,checkAliasesExpandEarly
,checkUnsetGlobs
,checkFindWithoutPath
,checkTimeParameters
,checkTimedCommand
,checkLocalScope
,checkDeprecatedTempfile
,checkDeprecatedEgrep
,checkDeprecatedFgrep
]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty
where
addCheck map (CommandCheck name function) =
Map.insertWith' composeAnalyzers name function map
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
name <- getLiteralString cmd
return $
if '/' `elem` name
then
Map.findWithDefault nullCheck (Basename $ basename name) map t
else do
Map.findWithDefault nullCheck (Exactly name) map t
Map.findWithDefault nullCheck (Basename name) map t
where
basename = reverse . takeWhile (/= '/') . reverse
checkCommand _ _ = return ()
getChecker :: [CommandCheck] -> Checker
getChecker list = Checker {
perScript = const $ return (),
perToken = checkCommand map
}
where
map = buildCommandMap list
checker :: Parameters -> Checker
checker params = getChecker commandChecks
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
prop_checkTr5 = verify checkTr "tr foo bar"
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion."
f word =
case getLiteralString word of
Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets."
Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets."
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
unless ("[:" `isPrefixOf` s) $
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
Nothing -> return ()
duplicated s =
let relevant = filter isAlpha s
in relevant /= nub relevant
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
acceptsGlob _ = False
f [] = return ()
f [x] = return ()
f (a:b:r) = do
when (acceptsGlob (getLiteralString a) && isGlob b) $ do
let (Just s) = getLiteralString a
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
f (b:r)
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t =
when (all (`notElem` exceptions) (words $ arguments t)) $
style (getId t) 2003
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
-- These operators are hard to replicate in POSIX
exceptions = [ ":", "<", ">", "<=", ">=" ]
words = mapMaybe getLiteralString
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *"
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd)
-- --regex=*(extglob) doesn't work. Fixme?
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
skippable _ = False
f _ [] = return ()
f cmd (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f cmd r
f cmd (re:_) = do
when (isGlob re) $
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
unless (cmd `hasFlag` "F") $ do
let string = concat $ oversimplify re
if isConfusedGlobRegex string then
warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
else potentially $ do
char <- getSuspiciousRegexWildcard string
return $ info (getId re) 2022 $
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
wordStartingWith c =
head . filter ([c] `isPrefixOf`) $ candidates
where
candidates =
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
getSuspiciousRegexWildcard str =
if not $ str `matches` contra
then do
match <- matchRegex suspicious str
str <- match !!! 0
str !!! 0
else
fail "looks good"
where
suspicious = mkRegex "([A-Za-z1-9])\\*"
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
f (x:_) = checkTrap x
f _ = return ()
checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs
checkTrap _ = return ()
warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
checkExpansions (T_DollarExpansion id _) = warning id
checkExpansions (T_Backticked id _) = warning id
checkExpansions (T_DollarBraced id _) = warning id
checkExpansions (T_DollarArithmetic id _) = warning id
checkExpansions _ = return ()
prop_checkReturn1 = verifyNot checkReturn "return"
prop_checkReturn2 = verifyNot checkReturn "return 1"
prop_checkReturn3 = verifyNot checkReturn "return $var"
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
prop_checkReturn5 = verify checkReturn "return -1"
prop_checkReturn6 = verify checkReturn "return 1000"
prop_checkReturn7 = verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (f . arguments)
where
f (first:second:_) =
err (getId second) 2151
"Only one integer 0-255 can be returned. Use stdout for other data."
f [value] =
when (isInvalid $ literal value) $
err (getId value) 2152
"Can only return 0-255. Other data should be written to stdout."
f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|| let value = (read s :: Integer) in value > 255
literal token = fromJust $ getLiteralStringExt lit token
lit (T_DollarBraced {}) = return "0"
lit (T_DollarArithmetic {}) = return "0"
lit (T_DollarExpansion {}) = return "0"
lit (T_Backticked {}) = return "0"
lit _ = return "WTF"
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
where
f = void . sequence . mapMaybe check . tails
check (exec:arg:term:_) = do
execS <- getLiteralString exec
termS <- getLiteralString term
cmdS <- getLiteralStringExt (const $ return " ") arg
guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
guard $ cmdS `matches` commandRegex
return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."
check _ = Nothing
commandRegex = mkRegex "[ |;]"
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
where
isDashE = mkRegex "^-.*e"
hasEscapes = mkRegex "\\\\[rnt]"
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
return ()
where allButLast = reverse . drop 1 . reverse $ args
f args = mapM_ checkEscapes args
checkEscapes (T_NormalWord _ args) =
mapM_ checkEscapes args
checkEscapes (T_DoubleQuoted id args) =
mapM_ checkEscapes args
checkEscapes (T_Literal id str) = examine id str
checkEscapes (T_SingleQuoted id str) = examine id str
checkEscapes _ = return ()
examine id str =
when (str `matches` hasEscapes) $
info id 2028 "echo won't expand escape sequences. Consider printf."
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
where
check args = do
let idStrings = map (\x -> (getId x, onlyLiteralString x)) args
match pattern idStrings
match _ [] = return ()
match [] (next:_) = action next
match (p:tests) ((id, arg):args) = do
when (p arg) $ match tests args
match (p:tests) args
pattern = [
(`elem` ["-exec", "-execdir"]),
(`elem` ["sh", "bash", "dash", "ksh"]),
(== "-c")
]
action (id, arg) =
when ("{}" `isInfixOf` arg) $
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
where
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
f list | length list < length pattern = return ()
f list@(_:rest) =
if and (zipWith ($) pattern list)
then warnFor (list !! (length pattern - 1))
else f rest
isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0", "-fls", "-fprint", "-fprint0", "-fprintf", "-ls", "-ok", "-okdir", "-printf" ]
isParam strs t = fromMaybe False $ do
param <- getLiteralString t
return $ param `elem` strs
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where
check t = potentially $ do
let flags = getAllFlags t
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
couldHaveSubdirs t = fromMaybe True $ do
name <- getLiteralString t
return $ '/' `elem` name
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where
f = mapM_ check
check param = potentially $ do
str <- getLiteralString param
let id = getId param
return $ sequence_ $ mapMaybe (\f -> f id str) [
checkNumeric,
checkUntrappable
]
checkNumeric id str = do
guard $ not (null str)
guard $ all isDigit str
guard $ str /= "0" -- POSIX exit trap
guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
return $ warn id 2172
"Trapping signals by number is not well defined. Prefer signal names."
checkUntrappable id str = do
guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
return $ err id 2173
"SIGKILL/SIGSTOP can not be trapped."
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
checkInteractiveSu = CommandCheck (Basename "su") f
where
f cmd = when (length (arguments cmd) <= 1) $ do
path <- pathTo cmd
when (all undirected path) $
info (getId cmd) 2117
"To run commands as another user, use su -c or sudo."
undirected (T_Pipeline _ _ l) = length l <= 1
-- This should really just be modifications to stdin, but meh
undirected (T_Redirecting _ list _) = null list
undirected _ = True
-- This is hard to get right without properly parsing ssh args
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
where
nonOptions =
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
f args =
case nonOptions args of
(hostport:r@(_:_)) -> checkArg $ last r
_ -> return ()
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
case filter (not . isConstant) parts of
[] -> return ()
(x:_) -> info (getId x) 2029
"Note that, unescaped, this expands on the client side."
checkArg _ = return ()
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar"
prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
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"
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
f (format:params) = check format params
f _ = return ()
countFormats string =
case string of
'%':'%':rest -> countFormats rest
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
'%':rest -> 1 + countFormats rest
_:rest -> countFormats rest
[] -> 0
check format more = do
fromMaybe (return ()) $ do
string <- getLiteralString format
let vars = countFormats string
return $ do
when (vars == 0 && more /= []) $
err (getId format) 2182
"This printf format string has no variables. Other arguments are ignored."
when (vars > 0
&& length more < vars
&& all (not . mayBecomeMultipleArgs) more) $
warn (getId format) 2183 $
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
info (getId format) 2059
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
f _ = return ()
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where
f (var:value:rest) =
let str = literal var in
when (isVariableName str || isAssignment str) $
msg (getId var)
f (var:_) =
when (isAssignment $ literal var) $
msg (getId var)
f _ = return ()
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
isAssignment str = '=' `elem` str
literal (T_NormalWord _ l) = concatMap literal l
literal (T_Literal _ str) = str
literal _ = "*"
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
checkExportedExpansions = CommandCheck (Exactly "export") (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 ()
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
where
re = mkRegex "\\$\\{?[0-9*@]"
f = mapM_ checkArg
checkArg arg =
let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
when ('=' `elem` string && string `matches` re) $
err (getId arg) 2142
"Aliases can't use positional parameters. Use a function."
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
where
f = mapM_ checkArg
checkArg arg | '=' `elem` concat (oversimplify arg) =
forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
\x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
checkArg _ = return ()
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
where
check arg =
when (isGlob arg) $
warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded."
prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f"
prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find"
prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
checkFindWithoutPath = CommandCheck (Basename "find") f
where
f (T_SimpleCommand _ _ (cmd:args)) =
unless (hasPath args) $
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
-- This is a bit of a kludge. find supports flag arguments both before and after the path,
-- as well as multiple non-flag arguments that are not the path. We assume that all the
-- pre-path flags are single characters, which is generally the case.
hasPath (first:rest) =
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
not ("-" `isPrefixOf` flag) || length flag <= 2 && hasPath rest
hasPath [] = False
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
prop_checkTimeParameters4 = verifyNot checkTimeParameters "command time -f lol sleep 10"
checkTimeParameters = CommandCheck (Exactly "time") f
where
f (T_SimpleCommand _ _ (cmd:args:_)) =
whenShell [Bash, Sh] $
let s = concat $ oversimplify args in
when ("-" `isPrefixOf` s && s /= "-p") $
info (getId cmd) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one."
f _ = return ()
prop_checkTimedCommand1 = verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
checkTimedCommand = CommandCheck (Exactly "time") f where
f (T_SimpleCommand _ _ (c:args@(_:_))) =
whenShell [Sh, Dash] $ do
let cmd = last args -- "time" is parsed with a command as argument
when (isPiped cmd) $
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
when (isSimple cmd == Just False) $
warn (getId cmd) 2177 "'time' is undefined for compound commands, time sh -c instead."
f _ = return ()
isPiped cmd =
case cmd of
T_Pipeline _ _ (_:_:_) -> True
_ -> False
getCommand cmd =
case cmd of
T_Pipeline _ _ (T_Redirecting _ _ a : _) -> return a
_ -> fail ""
isSimple cmd = do
innerCommand <- getCommand cmd
case innerCommand of
T_SimpleCommand {} -> return True
_ -> return False
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t
unless (any isFunction path) $
err (getId t) 2168 "'local' is only valid in functions."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -0,0 +1,388 @@
{-
Copyright 2012-2016 Vidar Holen
This file is part of ShellCheck.
http://www.vidarholen.net/contents/shellcheck
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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/>.
-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module ShellCheck.Checks.ShellSupport (checker
, ShellCheck.Checks.ShellSupport.runTests
) where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib
import ShellCheck.Interface
import ShellCheck.Regex
import Control.Monad
import Control.Monad.RWS
import Data.Char
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data ForShell = ForShell [Shell] (Token -> Analysis)
getChecker params list = Checker {
perScript = nullCheck,
perToken = foldl composeAnalyzers nullCheck $ mapMaybe include list
}
where
shell = shellType params
include (ForShell list a) = do
guard $ shell `elem` list
return a
checker params = getChecker params checks
checks = [
checkForDecimals
,checkBashisms
,checkEchoSed
,checkBraceExpansionVars
,checkMultiDimensionalArrays
]
testChecker (ForShell _ t) =
Checker {
perScript = nullCheck,
perToken = t
}
verify c s = producesComments (testChecker c) s == Just True
verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f
where
f t@(TA_Expansion id _) = potentially $ do
str <- getLiteralString t
first <- str !!! 0
guard $ isDigit first && '.' `elem` str
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
prop_checkBashisms5 = verify checkBashisms "source file"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms13= verify checkBashisms "exec -c env"
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms15= verify checkBashisms "let n++"
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
prop_checkBashisms20= verify checkBashisms "read -ra foo"
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
prop_checkBashisms41= verify checkBashisms "echo `<file`"
prop_checkBashisms42= verify checkBashisms "trap foo int"
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
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"
checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask
kludge params t
where
-- This code was copy-pasted from Analytics where params was a variable
kludge params = bashism
where
isDash = shellType params == Dash
warnMsg id s =
if isDash
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
bashism (T_HereString id _) = warnMsg id "here-strings are"
bashism (TC_Binary id SingleBracket op _ _)
| op `elem` [ "-nt", "-ef", "\\<", "\\>"] =
unless isDash $ warnMsg id $ op ++ " is"
bashism (TC_Binary id SingleBracket "==" _ _) =
warnMsg id "== in place of = is"
bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _)
| op `elem` [ "|++", "|--", "++|", "--|"] =
warnMsg id $ filter (/= '|') op ++ " is"
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
bashism (T_FdRedirect id num _)
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
bashism (T_Assignment id Append _ _ _) =
warnMsg id "+= is"
bashism (T_IoFile id _ word) | isNetworked =
warnMsg id "/dev/{tcp,udp} is"
where
file = onlyLiteralString word
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
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@(T_DollarBraced id token) = do
mapM_ check expansion
when (isBashVariable var) $
warnMsg id $ var ++ " is"
where
str = bracedString t
var = getBracedReference str
check (regex, feature) =
when (isJust $ matchRegex regex str) $ warnMsg id feature
bashism t@(T_Pipe id "|&") =
warnMsg id "|& in place of 2>&1 | is"
bashism (T_Array id _) =
warnMsg id "arrays are"
bashism (T_IoFile id _ t) | isGlob t =
warnMsg id "redirecting to/from globs is"
bashism (T_CoProc id _ _) =
warnMsg id "coproc is"
bashism (T_Function id _ _ str _) | not (isVariableName str) =
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
warnMsg id "$(<file) to read files is"
bashism (T_Backticked id [x]) | isOnlyRedirection x =
warnMsg id "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && "-" `isPrefixOf` argString =
unless ("--" `isPrefixOf` argString) $ -- echo "-----"
if isDash
then
when (argString /= "-n") $
warnMsg (getId arg) "echo flags besides -n"
else
warnMsg (getId arg) "echo flags are"
where argString = concat $ oversimplify arg
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) "exec flags are"
bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id "'let' is"
bashism t@(T_SimpleCommand id _ (cmd:rest)) =
let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t
in do
when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is"
potentially $ do
allowed <- Map.lookup name allowedFlags
(word, flag) <- listToMaybe $
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
when (name == "source") $ warnMsg id "'source' in place of '.' is"
when (name == "trap") $
let
check token = potentially $ do
str <- getLiteralString token
let upper = map toUpper str
return $ do
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
warnMsg (getId token) $ "trapping " ++ str ++ " is"
when ("SIG" `isPrefixOf` upper) $
warnMsg (getId token)
"prefixing signal names with 'SIG' is"
when (not isDash && upper /= str) $
warnMsg (getId token)
"using lower/mixed case for signal names is"
in
mapM_ check (drop 1 rest)
when (name == "printf") $ potentially $ do
format <- rest !!! 0 -- flags are covered by allowedFlags
let literal = onlyLiteralString format
guard $ "%q" `isInfixOf` literal
return $ warnMsg (getId format) "printf %q is"
where
unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset"
] ++ if not isDash then ["local", "type"] else []
allowedFlags = Map.fromList [
("read", if isDash then ["r", "p"] else ["r"]),
("ulimit", ["f"]),
("printf", []),
("exec", [])
]
bashism _ = return ()
varChars="_0-9a-zA-Z"
expansion = let re = mkRegex in [
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
(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")
]
bashVars = [
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "LINENO" ]
isBashVariable var =
(var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var))
&& not (isDash && var `elem` dashVars)
isAssigned var = any f (variableFlow params)
where
f x = case x of
Assignment (_, _, name, _) -> name == var
_ -> False
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
checkEchoSed = ForShell [Bash, Ksh] f
where
f (T_Pipeline id _ [a, b]) =
when (acmd == ["echo", "${VAR}"]) $
case bcmd of
["sed", v] -> checkIn v
["sed", "-e", v] -> checkIn v
_ -> return ()
where
-- This should have used backreferences, but TDFA doesn't support them
sedRe = mkRegex "^s(.)([^\n]*)g?$"
isSimpleSed s = fromMaybe False $ do
[first,rest] <- matchRegex sedRe s
let delimiters = filter (== head first) rest
guard $ length delimiters == 2
return True
acmd = oversimplify a
bcmd = oversimplify b
checkIn s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
f _ = return ()
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
checkBraceExpansionVars = ForShell [Bash] f
where
f t@(T_BraceExpansion id list) = mapM_ check list
where
check element =
when (any (`isInfixOf` toString element) ["$..", "..$"]) $ do
c <- isEvaled element
if c
then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval."
else warn id 2051 "Bash doesn't support variables in brace range expansions."
f _ = return ()
literalExt t =
case t of
T_DollarBraced {} -> return "$"
T_DollarExpansion {} -> return "$"
T_DollarArithmetic {} -> return "$"
otherwise -> return "-"
toString t = fromJust $ getLiteralStringExt literalExt t
isEvaled t = do
cmd <- getClosestCommandM t
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
checkMultiDimensionalArrays = ForShell [Bash] f
where
f token =
case token of
T_Assignment _ _ name (first:second:_) _ -> about second
T_IndexedElement _ (first:second:_) _ -> about second
T_DollarBraced {} ->
when (isMultiDim token) $ about token
_ -> return ()
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -33,6 +33,9 @@ internalVariables = [
-- Other
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
-- Ksh
, ".sh.version"
]
variablesWithoutSpaces = [
@@ -82,12 +85,24 @@ sampleWords = [
"zulu"
]
binaryTestOps = [
"-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le",
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
]
unaryTestOps = [
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
"-o", "-v", "-R"
]
shellForExecutable :: String -> Maybe Shell
shellForExecutable name =
case name of
"sh" -> return Sh
"bash" -> return Bash
"dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh
"ksh88" -> return Ksh
"ksh93" -> return Ksh

View File

@@ -30,13 +30,15 @@ data Formatter = Formatter {
footer :: IO ()
}
lineNo (PositionedComment pos _) = posLine pos
colNo (PositionedComment pos _) = posColumn pos
codeNo (PositionedComment _ (Comment _ code _)) = code
messageText (PositionedComment _ (Comment _ _ t)) = t
lineNo (PositionedComment pos _ _) = posLine pos
endLineNo (PositionedComment _ end _) = posLine end
colNo (PositionedComment pos _ _) = posColumn pos
endColNo (PositionedComment _ end _) = posColumn end
codeNo (PositionedComment _ _ (Comment _ code _)) = code
messageText (PositionedComment _ _ (Comment _ _ t)) = t
severityText :: PositionedComment -> String
severityText (PositionedComment _ (Comment c _ _)) =
severityText (PositionedComment _ _ (Comment c _ _)) =
case c of
ErrorC -> "error"
WarningC -> "warning"
@@ -48,12 +50,15 @@ makeNonVirtual comments contents =
map fix comments
where
ls = lines contents
fix c@(PositionedComment pos comment) = PositionedComment pos {
posColumn =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
else colNo c
fix c@(PositionedComment start end comment) = PositionedComment start {
posColumn = realignColumn lineNo colNo c
} end {
posColumn = realignColumn endLineNo endColNo c
} comment
realignColumn lineNo colNo c =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
else colNo c
real _ r v target | target <= v = r
real [] r v _ = r -- should never happen
real ('\t':rest) r v target =

View File

@@ -37,10 +37,12 @@ format = do
}
instance JSON (PositionedComment) where
showJSON comment@(PositionedComment pos (Comment level code string)) = makeObj [
("file", showJSON $ posFile pos),
("line", showJSON $ posLine pos),
("column", showJSON $ posColumn pos),
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)

View File

@@ -94,7 +94,7 @@ data Position = Position {
} deriving (Show, Eq)
data Comment = Comment Severity Code String deriving (Show, Eq)
data PositionedComment = PositionedComment Position Comment deriving (Show, Eq)
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq)
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
data ColorOption =

File diff suppressed because it is too large Load Diff

10
nextnumber Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
# TODO: Find a less trashy way to get the next available error code
shopt -s globstar
for i in 1 2
do
last=$(grep -hv "^prop" **/*.hs | grep -Ewo "$i[0-9]{3}" | sort -n | tail -n 1)
echo "Next ${i}xxx: $((last+1))"
done

View File

@@ -4,12 +4,19 @@
# 'cabal test' remains the source of truth.
(
var=$(echo 'liftM and $ sequence [ShellCheck.Analytics.runTests, ShellCheck.Parser.runTests, ShellCheck.Checker.runTests]' | cabal repl | tee /dev/stderr)
var=$(echo 'liftM and $ sequence [
ShellCheck.Analytics.runTests
,ShellCheck.Parser.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.AnalyzerLib.runTests
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]]
then
exit 0
else
grep -C 3 "Fail" <<< "$var"
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
exit 1
fi
) 2>&1

View File

@@ -34,7 +34,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-C**[*WHEN*],\ **--color**[=*WHEN*]
: For TTY outut, enable colors *always*, *never* or *auto*. The default
: For TTY output, enable colors *always*, *never* or *auto*. The default
is *auto*. **--color** without an argument is equivalent to
**--color=always**.
@@ -175,6 +175,15 @@ ShellCheck uses the follow exit codes:
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
# LOCALE
This version of ShellCheck is only available in English. All files are
leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid
sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`.
# AUTHOR
ShellCheck is written and maintained by Vidar Holen.

View File

@@ -31,6 +31,7 @@ import qualified ShellCheck.Formatter.TTY
import Control.Exception
import Control.Monad
import Control.Monad.Except
import Data.Bits
import Data.Char
import Data.Functor
import Data.Either
@@ -288,14 +289,47 @@ ioInterface options files = do
fallback path _ = return path
inputFile file = do
contents <-
handle <-
if file == "-"
then getContents
else readFile file
then return stdin
else openBinaryFile file ReadMode
hSetBinaryMode handle True
contents <- decodeString <$> hGetContents handle -- closes handle
seq (length contents) $
return contents
-- Decode a char8 string into a utf8 string, with fallback on
-- ISO-8859-1. This avoids depending on additional libraries.
decodeString = decode
where
decode [] = []
decode (c:rest) | isAscii c = c : decode rest
decode (c:rest) =
let num = (fromIntegral $ ord c) :: Int
next = case num of
_ | num >= 0xF8 -> Nothing
| num >= 0xF0 -> construct (num .&. 0x07) 3 rest
| num >= 0xE0 -> construct (num .&. 0x0F) 2 rest
| num >= 0xC0 -> construct (num .&. 0x1F) 1 rest
| True -> Nothing
in
case next of
Just (n, remainder) -> chr n : decode remainder
Nothing -> c : decode rest
construct x 0 rest = do
guard $ x <= 0x10FFFF
return (x, rest)
construct x n (c:rest) =
let num = (fromIntegral $ ord c) :: Int in
if num >= 0x80 && num <= 0xBF
then construct ((x `shiftL` 6) .|. (num .&. 0x3f)) (n-1) rest
else Nothing
construct _ _ _ = Nothing
verifyFiles files =
when (null files) $ do
printErr "No files specified.\n"

35
stack.yaml Normal file
View File

@@ -0,0 +1,35 @@
# This file was automatically generated by stack init
# For more information, see: http://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-5.5
# Local packages, usually specified by relative directory name
packages:
- '.'
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
extra-deps: []
# Override default flag values for local packages and extra-deps
flags: {}
# Extra package databases containing global packages
extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
# Require a specific version of stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: >= 1.0.0
# Override the architecture used by stack, especially useful on Windows
# arch: i386
# arch: x86_64
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

View File

@@ -4,13 +4,19 @@ import Control.Monad
import System.Exit
import qualified ShellCheck.Checker
import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.Parser
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ShellSupport
main = do
putStrLn "Running ShellCheck tests..."
results <- sequence [
ShellCheck.Checker.runTests,
ShellCheck.Checks.Commands.runTests,
ShellCheck.Checks.ShellSupport.runTests,
ShellCheck.Analytics.runTests,
ShellCheck.AnalyzerLib.runTests,
ShellCheck.Parser.runTests
]
if and results