63 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
19 changed files with 1223 additions and 533 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:

View File

@@ -8,7 +8,7 @@ services:
before_install: before_install:
- export DOCKER_REPO=koalaman/shellcheck - export DOCKER_REPO=koalaman/shellcheck
- |- - |-
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || echo $TRAVIS_BRANCH) export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || ([ -n "$TRAVIS_TAG" ] && echo "$TRAVIS_TAG") || echo "$TRAVIS_BRANCH")
script: script:
- docker build -t builder -f Dockerfile_builder . - docker build -t builder -f Dockerfile_builder .
@@ -18,4 +18,4 @@ script:
after_success: after_success:
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" - docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
- |- - |-
[ "$TRAVIS_BRANCH" == "master" ] && docker push $DOCKER_REPO:$TAG ([ "$TRAVIS_BRANCH" == "master" ] || [ -n "$TRAVIS_TAG" ]) && docker push "$DOCKER_REPO:$TAG"

View File

@@ -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. 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): * Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
@@ -54,8 +54,21 @@ You can see ShellCheck suggestions directly in a variety of editors.
#### In your build or test suites #### In your build or test suites
While ShellCheck is mostly intended for interactive use, it can easily be added to builds 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 its [CheckStyle compatible XML output](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 ## Installing
@@ -64,7 +77,7 @@ The easiest way to install ShellCheck locally is through your package manager.
On systems with Cabal (installs to `~/.cabal/bin`): On systems with Cabal (installs to `~/.cabal/bin`):
cabal update cabal update
cabal install shellcheck cabal install ShellCheck
On Debian based distros: On Debian based distros:
@@ -74,6 +87,11 @@ On Gentoo based distros:
emerge --ask shellcheck emerge --ask shellcheck
On EPEL based distros:
yum -y install epel-release
yum install ShellCheck
On Fedora based distros: On Fedora based distros:
dnf install ShellCheck dnf install ShellCheck
@@ -99,6 +117,9 @@ add OBS devel:languages:haskell repository from https://build.opensuse.org/proje
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
From Docker Hub:
docker pull koalaman/shellcheck
## Compiling from source ## Compiling from source
@@ -107,7 +128,7 @@ This section describes how to build ShellCheck from a source directory. ShellChe
#### Installing Cabal #### 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/ 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/
@@ -183,6 +204,8 @@ ShellCheck can recognize many types of incorrect test statements.
[ $1 -eq "shellcheck" ] # Numerical comparison of strings [ $1 -eq "shellcheck" ] # Numerical comparison of strings
[ $n && $m ] # && in [ .. ] [ $n && $m ] # && in [ .. ]
[ grep -q foo file ] # Command without $(..) [ grep -q foo file ] # Command without $(..)
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
(( 1 -lt 2 )) # Using test operators in ((..))
#### Frequently misused commands #### Frequently misused commands
@@ -211,9 +234,11 @@ ShellCheck recognizes many common beginner's syntax errors:
var$n="Hello" # Wrong indirect assignment var$n="Hello" # Wrong indirect assignment
echo ${var$n} # Wrong indirect reference echo ${var$n} # Wrong indirect reference
var=(1, 2, 3) # Comma separated arrays var=(1, 2, 3) # Comma separated arrays
array=( [index] = value ) # Incorrect index initialization
echo "Argument 10 is $10" # Positional parameter misreference echo "Argument 10 is $10" # Positional parameter misreference
if $(myfunction); then ..; fi # Wrapping commands in $() if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if' else if othercondition; then .. # Using 'else if'
#### Style #### Style
@@ -236,7 +261,8 @@ ShellCheck can recognize issues related to data and typing:
args="$@" # Assigning arrays to strings args="$@" # Assigning arrays to strings
files=(foo bar); echo "$files" # Referencing arrays as 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 [[ $# > 2 ]] # Comparing numbers as strings
var=World; echo "Hello " var # Unused lowercase variables var=World; echo "Hello " var # Unused lowercase variables
echo "Hello $name" # Unassigned lowercase variables echo "Hello $name" # Unassigned lowercase variables
@@ -269,6 +295,7 @@ ShellCheck will warn when using features not supported by the shebang. For examp
foo-bar() { ..; } # Undefined/unsupported function name foo-bar() { ..; } # Undefined/unsupported function name
[ $UID = 0 ] # Variable undefined in dash/sh [ $UID = 0 ] # Variable undefined in dash/sh
local var=value # local is undefined in sh local var=value # local is undefined in sh
time sleep 1 | sleep 5 # Undefined uses of 'time'
#### Miscellaneous #### Miscellaneous
@@ -279,11 +306,13 @@ ShellCheck recognizes a menagerie of other issues:
PATH="$PATH:~/bin" # Literal tilde in $PATH PATH="$PATH:~/bin" # Literal tilde in $PATH
rm “file” # Unicode quotes rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings echo "Hello world" # Carriage return / DOS line endings
echo hello \ # Trailing spaces after \
var=42 echo $var # Expansion of inlined environment var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors #!/bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/' file > file # Redirecting to input sed 's/foo/bar/' file > file # Redirecting to input
## Testimonials ## Testimonials
@@ -293,6 +322,11 @@ ShellCheck recognizes a menagerie of other issues:
Alexander Tarasikov, Alexander Tarasikov,
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330) [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 ## Reporting bugs

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.4.5 Version: 0.4.6
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -55,6 +55,7 @@ library
ShellCheck.AnalyzerLib ShellCheck.AnalyzerLib
ShellCheck.Checker ShellCheck.Checker
ShellCheck.Checks.Commands ShellCheck.Checks.Commands
ShellCheck.Checks.ShellSupport
ShellCheck.Data ShellCheck.Data
ShellCheck.Formatter.Format ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle ShellCheck.Formatter.CheckStyle

View File

@@ -33,6 +33,7 @@ data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq) data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq) data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
data Root = Root Token
data Token = data Token =
TA_Binary Id String Token Token TA_Binary Id String Token Token
| TA_Assignment Id String Token Token | TA_Assignment Id String Token Token
@@ -44,7 +45,7 @@ data Token =
| TC_And Id ConditionType String Token Token | TC_And Id ConditionType String Token Token
| TC_Binary Id ConditionType String Token Token | TC_Binary Id ConditionType String Token Token
| TC_Group Id ConditionType Token | TC_Group Id ConditionType Token
| TC_Noary Id ConditionType Token | TC_Nullary Id ConditionType Token
| TC_Or Id ConditionType String Token Token | TC_Or Id ConditionType String Token Token
| TC_Unary Id ConditionType String Token | TC_Unary Id ConditionType String Token
| T_AND_IF Id | T_AND_IF Id
@@ -110,6 +111,7 @@ data Token =
| T_NormalWord Id [Token] | T_NormalWord Id [Token]
| T_OR_IF Id | T_OR_IF Id
| T_OrIf Id (Token) (Token) | 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_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
| T_ProcSub Id String [Token] | T_ProcSub Id String [Token]
| T_Rbrace Id | T_Rbrace Id
@@ -256,7 +258,7 @@ analyze f g i =
delve (TC_Group id typ token) = d1 token $ TC_Group id typ 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_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_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_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_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
@@ -318,6 +320,7 @@ getId t = case t of
T_DollarBraced id _ -> id T_DollarBraced id _ -> id
T_DollarArithmetic id _ -> id T_DollarArithmetic id _ -> id
T_BraceExpansion id _ -> id T_BraceExpansion id _ -> id
T_ParamSubSpecialChar id _ -> id
T_DollarBraceCommandExpansion id _ -> id T_DollarBraceCommandExpansion id _ -> id
T_IoFile id _ _ -> id T_IoFile id _ _ -> id
T_IoDuplicate id _ _ -> id T_IoDuplicate id _ _ -> id
@@ -353,7 +356,7 @@ getId t = case t of
TC_Group id _ _ -> id TC_Group id _ _ -> id
TC_Binary id _ _ _ _ -> id TC_Binary id _ _ _ _ -> id
TC_Unary id _ _ _ -> id TC_Unary id _ _ _ -> id
TC_Noary id _ _ -> id TC_Nullary id _ _ -> id
TA_Binary id _ _ _ -> id TA_Binary id _ _ _ -> id
TA_Assignment id _ _ _ -> id TA_Assignment id _ _ _ -> id
TA_Unary id _ _ -> id TA_Unary id _ _ -> id
@@ -376,7 +379,7 @@ getId t = case t of
blank :: Monad m => Token -> m () blank :: Monad m => Token -> m ()
blank = const $ return () blank = const $ return ()
doAnalysis f = analyze f blank (return . id) doAnalysis f = analyze f blank return
doStackAnalysis startToken endToken = analyze startToken endToken (return . id) doStackAnalysis startToken endToken = analyze startToken endToken return
doTransform i = runIdentity . analyze blank blank (return . i) doTransform i = runIdentity . analyze blank blank (return . i)

View File

@@ -23,6 +23,7 @@ import ShellCheck.AST
import Control.Monad.Writer import Control.Monad.Writer
import Control.Monad import Control.Monad
import Data.Functor
import Data.List import Data.List
import Data.Maybe import Data.Maybe
@@ -86,13 +87,14 @@ oversimplify token =
(T_Glob _ s) -> [s] (T_Glob _ s) -> [s]
(T_Pipeline _ _ [x]) -> oversimplify x (T_Pipeline _ _ [x]) -> oversimplify x
(T_Literal _ x) -> [x] (T_Literal _ x) -> [x]
(T_ParamSubSpecialChar _ x) -> [x]
(T_SimpleCommand _ vars words) -> concatMap oversimplify words (T_SimpleCommand _ vars words) -> concatMap oversimplify words
(T_Redirecting _ _ foo) -> oversimplify foo (T_Redirecting _ _ foo) -> oversimplify foo
(T_DollarSingleQuoted _ s) -> [s] (T_DollarSingleQuoted _ s) -> [s]
(T_Annotation _ _ s) -> oversimplify s (T_Annotation _ _ s) -> oversimplify s
-- Workaround for let "foo = bar" parsing -- Workaround for let "foo = bar" parsing
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v (TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
otherwise -> [] _ -> []
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar", -- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
@@ -111,8 +113,11 @@ getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags o
-- Get all flags in a GNU way, up until -- -- Get all flags in a GNU way, up until --
getAllFlags = getFlagsUntil (== "--") getAllFlags = getFlagsUntil (== "--")
-- Get all flags in a BSD way, up until first non-flag argument -- Get all flags in a BSD way, up until first non-flag argument or --
getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`)) 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. -- Given a T_DollarBraced, return a simplified version of the string contents.
@@ -170,6 +175,20 @@ getUnquotedLiteral (T_NormalWord _ list) =
str _ = Nothing str _ = Nothing
getUnquotedLiteral _ = 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. -- Maybe get the literal string of this token and any globs in it.
getGlobOrLiteralString = getLiteralStringExt f getGlobOrLiteralString = getLiteralStringExt f
where where
@@ -188,6 +207,7 @@ getLiteralStringExt more = g
g (TA_Expansion _ l) = allInList l g (TA_Expansion _ l) = allInList l
g (T_SingleQuoted _ s) = return s g (T_SingleQuoted _ s) = return s
g (T_Literal _ s) = return s g (T_Literal _ s) = return s
g (T_ParamSubSpecialChar _ s) = return s
g x = more x g x = more x
-- Is this token a string literal? -- Is this token a string literal?
@@ -221,8 +241,15 @@ getCommand t =
-- Maybe get the command name of a token representing a command -- Maybe get the command name of a token representing a command
getCommandName t = do getCommandName t = do
(T_SimpleCommand _ _ (w:_)) <- getCommand t (T_SimpleCommand _ _ (w:rest)) <- getCommand t
getLiteralString w 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. -- If a command substitution is a single command, get its name.
-- $(date +%s) = Just "date" -- $(date +%s) = Just "date"
@@ -260,6 +287,8 @@ isOnlyRedirection t =
isFunction t = case t of T_Function {} -> True; _ -> False isFunction t = case t of T_Function {} -> True; _ -> False
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
-- Get the lists of commands from tokens that contain them, such as -- Get the lists of commands from tokens that contain them, such as
-- the body of while loops or branches of if statements. -- the body of while loops or branches of if statements.
getCommandSequences t = getCommandSequences t =
@@ -281,7 +310,7 @@ getAssociativeArrays t =
f :: Token -> Writer [String] () f :: Token -> Writer [String] ()
f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do
name <- getCommandName t name <- getCommandName t
guard $ name == "declare" guard $ name == "declare" || name == "typeset"
let flags = getAllFlags t let flags = getAllFlags t
guard $ elem "A" $ map snd flags guard $ elem "A" $ map snd flags
let args = map fst . filter ((==) "" . snd) $ flags let args = map fst . filter ((==) "" . snd) $ flags
@@ -293,3 +322,65 @@ getAssociativeArrays t =
case t of case t of
T_Assignment _ _ name _ _ -> return name T_Assignment _ _ name _ _ -> return name
otherwise -> Nothing 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)

View File

@@ -22,7 +22,7 @@ module ShellCheck.Analytics (runAnalytics, ShellCheck.Analytics.runTests) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib import ShellCheck.AnalyzerLib hiding (producesComments)
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Parser import ShellCheck.Parser
import ShellCheck.Interface import ShellCheck.Interface
@@ -50,7 +50,7 @@ treeChecks :: [Parameters -> Token -> [TokenComment]]
treeChecks = [ treeChecks = [
runNodeAnalysis runNodeAnalysis
(\p t -> (mapM_ ((\ f -> f t) . (\ f -> f p)) (\p t -> (mapM_ ((\ f -> f t) . (\ f -> f p))
(nodeChecks ++ checksFor (shellType p)))) nodeChecks))
,subshellAssignmentCheck ,subshellAssignmentCheck
,checkSpacefulness ,checkSpacefulness
,checkQuotesInLiterals ,checkQuotesInLiterals
@@ -62,30 +62,7 @@ treeChecks = [
,checkShebang ,checkShebang
,checkUnassignedReferences ,checkUnassignedReferences
,checkUncheckedCd ,checkUncheckedCd
] ,checkArrayAssignmentIndices
checksFor Sh = [
checkBashisms
,checkTimeParameters
,checkForDecimals
,checkTimedCommand
]
checksFor Dash = [
checkBashisms
,checkForDecimals
,checkLocalScope
,checkTimedCommand
]
checksFor Ksh = [
checkEchoSed
]
checksFor Bash = [
checkTimeParameters
,checkBraceExpansionVars
,checkEchoSed
,checkForDecimals
,checkLocalScope
,checkMultiDimensionalArrays
] ]
runAnalytics :: AnalysisSpec -> [TokenComment] runAnalytics :: AnalysisSpec -> [TokenComment]
@@ -122,7 +99,7 @@ nodeChecks = [
,checkSingleBracketOperators ,checkSingleBracketOperators
,checkDoubleBracketOperators ,checkDoubleBracketOperators
,checkLiteralBreakingTest ,checkLiteralBreakingTest
,checkConstantNoary ,checkConstantNullary
,checkDivBeforeMult ,checkDivBeforeMult
,checkArithmeticDeref ,checkArithmeticDeref
,checkArithmeticBadOctal ,checkArithmeticBadOctal
@@ -172,7 +149,7 @@ nodeChecks = [
,checkMultipleAppends ,checkMultipleAppends
,checkSuspiciousIFS ,checkSuspiciousIFS
,checkShouldUseGrepQ ,checkShouldUseGrepQ
,checkTestGlobs ,checkTestArgumentSplitting
,checkConcatenatedDollarAt ,checkConcatenatedDollarAt
,checkTildeInPath ,checkTildeInPath
,checkMaskedReturns ,checkMaskedReturns
@@ -180,6 +157,9 @@ nodeChecks = [
,checkLoopVariableReassignment ,checkLoopVariableReassignment
,checkTrailingBracket ,checkTrailingBracket
,checkReturnAgainstZero ,checkReturnAgainstZero
,checkRedirectedNowhere
,checkUnmatchableCases
,checkSubshellAsTest
] ]
@@ -264,30 +244,6 @@ checkEchoWc _ (T_Pipeline id _ [a, b]) =
countMsg = style id 2000 "See if you can use ${#variable} instead." countMsg = style id 2000 "See if you can use ${#variable} instead."
checkEchoWc _ _ = return () checkEchoWc _ _ = return ()
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 _ (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."
checkEchoSed _ _ = return ()
prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo" prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo"
prop_checkPipedAssignment2 = verifyNot checkPipedAssignment "A=foo cmd | grep foo" prop_checkPipedAssignment2 = verifyNot checkPipedAssignment "A=foo cmd | grep foo"
prop_checkPipedAssignment3 = verifyNot checkPipedAssignment "A=foo" prop_checkPipedAssignment3 = verifyNot checkPipedAssignment "A=foo"
@@ -373,6 +329,13 @@ prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo"
prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | xargs foo" prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | xargs foo"
prop_checkPipePitfalls8 = verify checkPipePitfalls "foo | grep bar | wc -l" prop_checkPipePitfalls8 = verify checkPipePitfalls "foo | grep bar | wc -l"
prop_checkPipePitfalls9 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -l" prop_checkPipePitfalls9 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -l"
prop_checkPipePitfalls10 = verifyNot checkPipePitfalls "foo | grep -o bar | wc"
prop_checkPipePitfalls11 = verifyNot checkPipePitfalls "foo | grep bar | wc"
prop_checkPipePitfalls12 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -c"
prop_checkPipePitfalls13 = verifyNot checkPipePitfalls "foo | grep bar | wc -c"
prop_checkPipePitfalls14 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -cmwL"
prop_checkPipePitfalls15 = verifyNot checkPipePitfalls "foo | grep bar | wc -cmwL"
prop_checkPipePitfalls16 = verifyNot checkPipePitfalls "foo | grep -r bar | wc -l"
checkPipePitfalls _ (T_Pipeline id _ commands) = do checkPipePitfalls _ (T_Pipeline id _ commands) = do
for ["find", "xargs"] $ for ["find", "xargs"] $
\(find:xargs:_) -> \(find:xargs:_) ->
@@ -394,10 +357,11 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
for ["grep", "wc"] $ for ["grep", "wc"] $
\(grep:wc:_) -> \(grep:wc:_) ->
let flags = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep let flagsGrep = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep
flagsWc = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand wc
in in
unless (any (`elem` ["o", "only-matching"]) flags) $ unless ((any (`elem` ["o", "only-matching", "r", "R", "recursive"]) flagsGrep) || (any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc) || ((length flagsWc) == 0)) $
style (getId grep) 2126 "Consider using grep -c instead of grep|wc." style (getId grep) 2126 "Consider using grep -c instead of grep|wc -l."
didLs <- liftM or . sequence $ [ didLs <- liftM or . sequence $ [
for' ["ls", "grep"] $ for' ["ls", "grep"] $
@@ -448,233 +412,20 @@ prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho co
prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l " prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l "
prop_checkShebang3 = verifyTree checkShebang "ls -l" prop_checkShebang3 = verifyTree checkShebang "ls -l"
prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo" prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"
checkShebang params (T_Annotation _ list t) = checkShebang params (T_Annotation _ list t) =
if any isOverride list then [] else checkShebang params t if any isOverride list then [] else checkShebang params t
where where
isOverride (ShellOverride _) = True isOverride (ShellOverride _) = True
isOverride _ = False isOverride _ = False
checkShebang params (T_Script id sb _) = checkShebang params (T_Script id sb _) = execWriter $
[makeComment ErrorC id 2148 unless (shellTypeSpecified params) $ do
"Tips depend on target shell and yours is unknown. Add a shebang." when (sb == "") $
| not (shellTypeSpecified params) && sb == "" ] err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
when (executableFromShebang sb == "ash") $
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
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"
checkBashisms 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_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_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done" prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
@@ -690,13 +441,13 @@ checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]]
when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list
|| (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $ || (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $
err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once." err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once."
checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id s]] _) = checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id _]] _) =
warn id 2041 $ "This is a literal string. To run as a command, use $(" ++ s ++ ")." warn id 2041 "This is a literal string. To run as a command, use $(..) instead of '..' . "
checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_Literal id s]] _) = checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_Literal id s]] _) =
if ',' `elem` s if ',' `elem` s
then unless ('{' `elem` s) $ then unless ('{' `elem` s) $
warn id 2042 "Use spaces, not commas, to separate loop elements." warn id 2042 "Use spaces, not commas, to separate loop elements."
else warn id 2043 $ "This loop will only run once, with " ++ f ++ "='" ++ s ++ "'." else warn id 2043 "This loop will only ever run once for a constant value. Did you perhaps mean to loop over dir/*, $var or $(cmd)?"
checkForInQuoted _ _ = return () checkForInQuoted _ _ = return ()
prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done" prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"
@@ -994,9 +745,9 @@ checkStderrRedirect params redir@(T_Redirecting _ [
usesOutput t = usesOutput t =
case t of case t of
(T_Pipeline _ _ list) -> length list > 1 && not (isParentOf (parentMap params) (last list) redir) (T_Pipeline _ _ list) -> length list > 1 && not (isParentOf (parentMap params) (last list) redir)
(T_ProcSub {}) -> True T_ProcSub {} -> True
(T_DollarExpansion {}) -> True T_DollarExpansion {} -> True
(T_Backticked {}) -> True T_Backticked {} -> True
_ -> False _ -> False
isCaptured = any usesOutput $ getPath (parentMap params) redir isCaptured = any usesOutput $ getPath (parentMap params) redir
@@ -1025,6 +776,7 @@ prop_checkSingleQuotedVariables9 = verifyNot checkSingleQuotedVariables "find .
prop_checkSingleQuotedVariables10= verify checkSingleQuotedVariables "echo '`pwd`'" prop_checkSingleQuotedVariables10= verify checkSingleQuotedVariables "echo '`pwd`'"
prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${/lol/d}'" prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${/lol/d}'"
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'" prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
checkSingleQuotedVariables params t@(T_SingleQuoted id s) = checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
when (s `matches` re) $ when (s `matches` re) $
if "sed" == commandName if "sed" == commandName
@@ -1062,7 +814,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
isOkAssignment t = isOkAssignment t =
case t of case t of
T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted
otherwise -> False _ -> False
re = mkRegex "\\$[{(0-9a-zA-Z_]|`.*`" re = mkRegex "\\$[{(0-9a-zA-Z_]|`.*`"
sedContra = mkRegex "\\$[{dpsaic]($|[^a-zA-Z])" sedContra = mkRegex "\\$[{dpsaic]($|[^a-zA-Z])"
@@ -1222,6 +974,7 @@ prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)'
prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]" prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]"
prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]" prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"
prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]" prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]"
prop_checkQuotedCondRegex6 = verify checkQuotedCondRegex "[[ $foo =~ 'cow|bar' ]]"
checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) = checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
case rhs of case rhs of
T_NormalWord id [T_DoubleQuoted _ _] -> error rhs T_NormalWord id [T_DoubleQuoted _ _] -> error rhs
@@ -1232,7 +985,7 @@ checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
unless (isConstantNonRe t) $ unless (isConstantNonRe t) $
err (getId t) 2076 err (getId t) 2076
"Don't quote rhs of =~, it'll match literally rather than as a regex." "Don't quote rhs of =~, it'll match literally rather than as a regex."
re = mkRegex "[][*.+()]" re = mkRegex "[][*.+()|]"
hasMetachars s = s `matches` re hasMetachars s = s `matches` re
isConstantNonRe t = fromMaybe False $ do isConstantNonRe t = fromMaybe False $ do
s <- getLiteralString t s <- getLiteralString t
@@ -1260,14 +1013,20 @@ prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"
prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]" prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]"
prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]" prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"
prop_checkConstantIfs8 = verifyNot checkConstantIfs "[[ ~foo == '~foo' ]]" prop_checkConstantIfs8 = verifyNot checkConstantIfs "[[ ~foo == '~foo' ]]"
prop_checkConstantIfs9 = verify checkConstantIfs "[[ *.png == [a-z] ]]"
checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic = checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =
when (isConstant lhs && isConstant rhs) $ if isConstant lhs && isConstant rhs
warn id 2050 "This expression is constant. Did you forget the $ on a variable?" then warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
else checkUnmatchable id op lhs rhs
where where
isDynamic = isDynamic =
op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ] op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]
&& typ == DoubleBracket && typ == DoubleBracket
|| op `elem` [ "-nt", "-ot", "-ef"] || op `elem` [ "-nt", "-ot", "-ef"]
checkUnmatchable id op lhs rhs =
when (op `elem` ["=", "==", "!="] && not (wordsCanBeEqual lhs rhs)) $
warn id 2193 "The arguments to this comparison can never be equal. Make sure your syntax is correct."
checkConstantIfs _ _ = return () checkConstantIfs _ _ = return ()
prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]" prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]"
@@ -1281,7 +1040,7 @@ prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(
prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]" prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]"
checkLiteralBreakingTest _ t = potentially $ checkLiteralBreakingTest _ t = potentially $
case t of case t of
(TC_Noary _ _ w@(T_NormalWord _ l)) -> do (TC_Nullary _ _ w@(T_NormalWord _ l)) -> do
guard . not $ isConstant w -- Covered by SC2078 guard . not $ isConstant w -- Covered by SC2078
comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings." comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings."
(TC_Unary _ _ op w@(T_NormalWord _ l)) -> (TC_Unary _ _ op w@(T_NormalWord _ l)) ->
@@ -1305,14 +1064,14 @@ checkLiteralBreakingTest _ t = potentially $
token <- listToMaybe $ filter isNonEmpty $ getWordParts t token <- listToMaybe $ filter isNonEmpty $ getWordParts t
return $ err (getId token) 2157 s return $ err (getId token) 2157 s
prop_checkConstantNoary = verify checkConstantNoary "[[ '$(foo)' ]]" prop_checkConstantNullary = verify checkConstantNullary "[[ '$(foo)' ]]"
prop_checkConstantNoary2 = verify checkConstantNoary "[ \"-f lol\" ]" prop_checkConstantNullary2 = verify checkConstantNullary "[ \"-f lol\" ]"
prop_checkConstantNoary3 = verify checkConstantNoary "[[ cmd ]]" prop_checkConstantNullary3 = verify checkConstantNullary "[[ cmd ]]"
prop_checkConstantNoary4 = verify checkConstantNoary "[[ ! cmd ]]" prop_checkConstantNullary4 = verify checkConstantNullary "[[ ! cmd ]]"
prop_checkConstantNoary5 = verify checkConstantNoary "[[ true ]]" prop_checkConstantNullary5 = verify checkConstantNullary "[[ true ]]"
prop_checkConstantNoary6 = verify checkConstantNoary "[ 1 ]" prop_checkConstantNullary6 = verify checkConstantNullary "[ 1 ]"
prop_checkConstantNoary7 = verify checkConstantNoary "[ false ]" prop_checkConstantNullary7 = verify checkConstantNullary "[ false ]"
checkConstantNoary _ (TC_Noary _ _ t) | isConstant t = checkConstantNullary _ (TC_Nullary _ _ t) | isConstant t =
case fromMaybe "" $ getLiteralString t of case fromMaybe "" $ getLiteralString t of
"false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets." "false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets."
"0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead." "0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead."
@@ -1322,29 +1081,7 @@ checkConstantNoary _ (TC_Noary _ _ t) | isConstant t =
where where
string = fromMaybe "" $ getLiteralString t string = fromMaybe "" $ getLiteralString t
checkConstantNoary _ _ = return () checkConstantNullary _ _ = 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 params t@(T_BraceExpansion id list) = mapM_ check list
where
check element =
when (any (`isInfixOf` toString element) ["$..", "..$"]) $
if isEvaled
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."
literalExt t =
case t of
T_DollarBraced {} -> return "$"
T_DollarExpansion {} -> return "$"
T_DollarArithmetic {} -> return "$"
otherwise -> return "-"
toString t = fromJust $ getLiteralStringExt literalExt t
isEvaled = fromMaybe False $
(`isUnqualifiedCommand` "eval") <$> getClosestCommand (parentMap params) t
checkBraceExpansionVars _ _ = return ()
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
@@ -1419,7 +1156,7 @@ checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T
warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching." warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word) checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
| (op == "=" || op == "==") && isGlob word = | (op == "=" || op == "==") && isGlob word =
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or grep." err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
checkComparisonAgainstGlob _ _ = return () checkComparisonAgainstGlob _ _ = return ()
prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)" prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)"
@@ -1462,10 +1199,10 @@ prop_checkValidCondOps2a= verifyNot checkValidCondOps "[ 3 \\> 2 ]"
prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]" prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]"
prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]" prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]"
checkValidCondOps _ (TC_Binary id _ s _ _) checkValidCondOps _ (TC_Binary id _ s _ _)
| s `notElem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="] = | s `notElem` binaryTestOps =
warn id 2057 "Unknown binary operator." warn id 2057 "Unknown binary operator."
checkValidCondOps _ (TC_Unary id _ s _) checkValidCondOps _ (TC_Unary id _ s _)
| s `notElem` [ "!", "-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"] = | s `notElem` unaryTestOps =
warn id 2058 "Unknown unary operator." warn id 2058 "Unknown unary operator."
checkValidCondOps _ _ = return () checkValidCondOps _ _ = return ()
@@ -1503,40 +1240,6 @@ checkUuoeVar _ p =
"Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'." "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."
otherwise -> return () otherwise -> return ()
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
checkTimeParameters _ = checkUnqualifiedCommand "time" f where
f cmd (x:_) = let s = concat $ oversimplify x 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 "time -p foo | bar"
prop_checkTimedCommand2 = verify checkTimedCommand "time ( foo; bar; )"
prop_checkTimedCommand3 = verifyNot checkTimedCommand "time sleep 1"
checkTimedCommand _ = checkUnqualifiedCommand "time" f where
f c args@(_:_) = do
let cmd = last args
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_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1" prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"
prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1" prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1"
@@ -1646,6 +1349,7 @@ prop_checkInexplicablyUnquoted3 = verifyNot checkInexplicablyUnquoted "wget --us
prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUES (\"id\")\"" prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUES (\"id\")\""
prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\"" prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""
prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\"" prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\""
prop_checkInexplicablyUnquoted7 = verifyNot checkInexplicablyUnquoted "${dir/\"foo\"/\"bar\"}"
checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens) checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)
where where
check (T_SingleQuoted _ _:T_Literal id str:_) check (T_SingleQuoted _ _:T_Literal id str:_)
@@ -1674,7 +1378,7 @@ checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens
warnAboutExpansion id = warnAboutExpansion id =
warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them." warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them."
warnAboutLiteral id = warnAboutLiteral id =
warn id 2140 "Word is on the form \"A\"B\"C\" (B indicated). Did you mean \"ABC\" or \"A\\\"B\\\"C\"?" warn id 2140 "Word is of the form \"A\"B\"C\" (B indicated). Did you mean \"ABC\" or \"A\\\"B\\\"C\"?"
checkInexplicablyUnquoted _ _ = return () checkInexplicablyUnquoted _ _ = return ()
prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\"" prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\""
@@ -1880,6 +1584,8 @@ prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>
prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;" prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;"
prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\"" prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\""
prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]" prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
checkSpacefulness params t = checkSpacefulness params t =
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params) doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
@@ -2148,6 +1854,7 @@ prop_checkUnassignedReferences24= verifyNotTree checkUnassignedReferences "decla
prop_checkUnassignedReferences25= verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;" prop_checkUnassignedReferences25= verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;"
prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b" prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b"
prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}" prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}"
prop_checkUnassignedReferences28= verifyNotTree checkUnassignedReferences "#!/bin/ksh\necho \"${.sh.version}\"\n"
checkUnassignedReferences params t = warnings checkUnassignedReferences params t = warnings
where where
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty) (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
@@ -2246,12 +1953,14 @@ prop_checkWhileReadPitfalls4 = verifyNot checkWhileReadPitfalls "while read foo;
prop_checkWhileReadPitfalls5 = verifyNot checkWhileReadPitfalls "while read foo; do echo ls | ssh $foo; done" prop_checkWhileReadPitfalls5 = verifyNot checkWhileReadPitfalls "while read foo; do echo ls | ssh $foo; done"
prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo" prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo"
prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file" prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file"
prop_checkWhileReadPitfalls8 = verifyNot checkWhileReadPitfalls "while read foo; do ssh -n $foo uptime; done < file"
checkWhileReadPitfalls _ (T_WhileExpression id [command] contents) checkWhileReadPitfalls _ (T_WhileExpression id [command] contents)
| isStdinReadCommand command = | isStdinReadCommand command =
mapM_ checkMuncher contents mapM_ checkMuncher contents
where where
munchers = [ "ssh", "ffmpeg", "mplayer" ] munchers = [ "ssh", "ffmpeg", "mplayer", "HandBrakeCLI" ]
preventionFlags = ["n", "noconsolecontrols" ]
isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) = isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) =
let plaintext = oversimplify cmd let plaintext = oversimplify cmd
@@ -2268,6 +1977,10 @@ checkWhileReadPitfalls _ (T_WhileExpression id [command] contents)
_ -> potentially $ do _ -> potentially $ do
name <- getCommandBasename cmd name <- getCommandBasename cmd
guard $ name `elem` munchers guard $ name `elem` munchers
-- Sloppily check if the command has a flag to prevent eating stdin.
let flags = getAllFlags cmd
guard . not $ any (`elem` preventionFlags) $ map snd flags
return $ do return $ do
info id 2095 $ info id 2095 $
name ++ " may swallow stdin, preventing this loop from working properly." name ++ " may swallow stdin, preventing this loop from working properly."
@@ -2386,14 +2099,6 @@ checkLoopKeywordScope params t |
checkLoopKeywordScope _ _ = return () checkLoopKeywordScope _ _ = return ()
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope params t | t `isCommand` "local" && not (isInFunction t) =
err (getId t) 2168 "'local' is only valid in functions."
where
isInFunction t = any isFunction $ getPath (parentMap params) t
checkLocalScope _ _ = return ()
prop_checkFunctionDeclarations1 = verify checkFunctionDeclarations "#!/bin/ksh\nfunction foo() { command foo --lol \"$@\"; }" prop_checkFunctionDeclarations1 = verify checkFunctionDeclarations "#!/bin/ksh\nfunction foo() { command foo --lol \"$@\"; }"
prop_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }" prop_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }"
prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }" prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }"
@@ -2677,7 +2382,7 @@ prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"
prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]" prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]"
checkShouldUseGrepQ params t = checkShouldUseGrepQ params t =
potentially $ case t of potentially $ case t of
TC_Noary id _ token -> check id True token TC_Nullary id _ token -> check id True token
TC_Unary id _ "-n" token -> check id True token TC_Unary id _ "-n" token -> check id True token
TC_Unary id _ "-z" token -> check id False token TC_Unary id _ "-z" token -> check id False token
_ -> fail "not check" _ -> fail "not check"
@@ -2705,12 +2410,67 @@ checkShouldUseGrepQ params t =
_ -> fail "unknown" _ -> fail "unknown"
isGrep = (`elem` ["grep", "egrep", "fgrep", "zgrep"]) isGrep = (`elem` ["grep", "egrep", "fgrep", "zgrep"])
prop_checkTestGlobs1 = verify checkTestGlobs "[ -e *.mp3 ]" prop_checkTestArgumentSplitting1 = verify checkTestArgumentSplitting "[ -e *.mp3 ]"
prop_checkTestGlobs2 = verifyNot checkTestGlobs "[[ $a == *b* ]]" prop_checkTestArgumentSplitting2 = verifyNot checkTestArgumentSplitting "[[ $a == *b* ]]"
checkTestGlobs params (TC_Unary _ _ op token) | isGlob token = prop_checkTestArgumentSplitting3 = verify checkTestArgumentSplitting "[[ *.png == '' ]]"
err (getId token) 2144 $ prop_checkTestArgumentSplitting4 = verify checkTestArgumentSplitting "[[ foo == f{o,oo,ooo} ]]"
op ++ " doesn't work with globs. Use a for loop." prop_checkTestArgumentSplitting5 = verify checkTestArgumentSplitting "[[ $@ ]]"
checkTestGlobs _ _ = return () prop_checkTestArgumentSplitting6 = verify checkTestArgumentSplitting "[ -e $@ ]"
prop_checkTestArgumentSplitting7 = verify checkTestArgumentSplitting "[ $@ == $@ ]"
prop_checkTestArgumentSplitting8 = verify checkTestArgumentSplitting "[[ $@ = $@ ]]"
prop_checkTestArgumentSplitting9 = verifyNot checkTestArgumentSplitting "[[ foo =~ bar{1,2} ]]"
prop_checkTestArgumentSplitting10 = verifyNot checkTestArgumentSplitting "[ \"$@\" ]"
prop_checkTestArgumentSplitting11 = verify checkTestArgumentSplitting "[[ \"$@\" ]]"
prop_checkTestArgumentSplitting12 = verify checkTestArgumentSplitting "[ *.png ]"
prop_checkTestArgumentSplitting13 = verify checkTestArgumentSplitting "[ \"$@\" == \"\" ]"
prop_checkTestArgumentSplitting14 = verify checkTestArgumentSplitting "[[ \"$@\" == \"\" ]]"
prop_checkTestArgumentSplitting15 = verifyNot checkTestArgumentSplitting "[[ \"$*\" == \"\" ]]"
checkTestArgumentSplitting :: Parameters -> Token -> Writer [TokenComment] ()
checkTestArgumentSplitting _ t =
case t of
(TC_Unary _ _ op token) | isGlob token ->
err (getId token) 2144 $
op ++ " doesn't work with globs. Use a for loop."
(TC_Nullary _ typ token) -> do
checkBraces typ token
checkGlobs typ token
when (typ == DoubleBracket) $
checkArrays typ token
(TC_Unary _ typ op token) -> checkAll typ token
(TC_Binary _ typ op lhs rhs) ->
if op `elem` ["=", "==", "!=", "=~"]
then do
checkAll typ lhs
checkArrays typ rhs
checkBraces typ rhs
else mapM_ (checkAll typ) [lhs, rhs]
_ -> return ()
where
checkAll typ token = do
checkArrays typ token
checkBraces typ token
checkGlobs typ token
checkArrays typ token =
when (any isArrayExpansion $ getWordParts token) $
if typ == SingleBracket
then warn (getId token) 2198 "Arrays don't work as operands in [ ]. Use a loop (or concatenate with * instead of @)."
else err (getId token) 2199 "Arrays implicitly concatenate in [[ ]]. Use a loop (or explicit * instead of @)."
checkBraces typ token =
when (any isBraceExpansion $ getWordParts token) $
if typ == SingleBracket
then warn (getId token) 2200 "Brace expansions don't work as operands in [ ]. Use a loop."
else err (getId token) 2201 "Brace expansion doesn't happen in [[ ]]. Use a loop."
checkGlobs typ token =
when (isGlob token) $
if typ == SingleBracket
then warn (getId token) 2202 "Globs don't work as operands in [ ]. Use a loop."
else err (getId token) 2203 "Globs are ignored in [[ ]] except right of =/!=. Use a loop."
prop_checkMaskedReturns1 = verify checkMaskedReturns "f() { local a=$(false); }" prop_checkMaskedReturns1 = verify checkMaskedReturns "f() { local a=$(false); }"
@@ -2757,7 +2517,7 @@ checkUncheckedCd params root =
when(t `isUnqualifiedCommand` "cd" when(t `isUnqualifiedCommand` "cd"
&& not (isCdDotDot t) && not (isCdDotDot t)
&& not (isCondition $ getPath (parentMap params) t)) $ && not (isCondition $ getPath (parentMap params) t)) $
warn (getId t) 2164 "Use cd ... || exit in case cd fails." warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
checkElement _ = return () checkElement _ = return ()
isCdDotDot t = oversimplify t == ["cd", ".."] isCdDotDot t = oversimplify t == ["cd", ".."]
hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root
@@ -2823,25 +2583,6 @@ checkTrailingBracket _ token =
"]" -> "[" "]" -> "["
x -> x x -> x
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 _ 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 ()
where
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
prop_checkReturnAgainstZero1 = verify checkReturnAgainstZero "[ $? -eq 0 ]" prop_checkReturnAgainstZero1 = verify checkReturnAgainstZero "[ $? -eq 0 ]"
prop_checkReturnAgainstZero2 = verify checkReturnAgainstZero "[[ \"$?\" -gt 0 ]]" prop_checkReturnAgainstZero2 = verify checkReturnAgainstZero "[[ \"$?\" -gt 0 ]]"
prop_checkReturnAgainstZero3 = verify checkReturnAgainstZero "[[ 0 -ne $? ]]" prop_checkReturnAgainstZero3 = verify checkReturnAgainstZero "[[ 0 -ne $? ]]"
@@ -2872,5 +2613,129 @@ checkReturnAgainstZero _ token =
otherwise -> False otherwise -> False
message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?." message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?."
prop_checkRedirectedNowhere1 = verify checkRedirectedNowhere "> file"
prop_checkRedirectedNowhere2 = verify checkRedirectedNowhere "> file | grep foo"
prop_checkRedirectedNowhere3 = verify checkRedirectedNowhere "grep foo | > bar"
prop_checkRedirectedNowhere4 = verifyNot checkRedirectedNowhere "grep foo > bar"
prop_checkRedirectedNowhere5 = verifyNot checkRedirectedNowhere "foo | grep bar > baz"
prop_checkRedirectedNowhere6 = verifyNot checkRedirectedNowhere "var=$(value) 2> /dev/null"
prop_checkRedirectedNowhere7 = verifyNot checkRedirectedNowhere "var=$(< file)"
prop_checkRedirectedNowhere8 = verifyNot checkRedirectedNowhere "var=`< file`"
checkRedirectedNowhere params token =
case token of
T_Pipeline _ _ [single] -> potentially $ do
redir <- getDanglingRedirect single
guard . not $ isInExpansion token
return $ warn (getId redir) 2188 "This redirection doesn't have a command. Move to its command (or use 'true' as no-op)."
T_Pipeline _ _ list -> forM_ list $ \x -> potentially $ do
redir <- getDanglingRedirect x
return $ err (getId redir) 2189 "You can't have | between this redirection and the command it should apply to."
_ -> return ()
where
isInExpansion t =
case drop 1 $ getPath (parentMap params) t of
T_DollarExpansion _ [_] : _ -> True
T_Backticked _ [_] : _ -> True
T_Annotation _ _ u : _ -> isInExpansion u
_ -> False
getDanglingRedirect token =
case token of
T_Redirecting _ (first:_) (T_SimpleCommand _ [] []) -> return first
_ -> Nothing
prop_checkArrayAssignmentIndices1 = verifyTree checkArrayAssignmentIndices "declare -A foo; foo=(bar)"
prop_checkArrayAssignmentIndices2 = verifyNotTree checkArrayAssignmentIndices "declare -a foo; foo=(bar)"
prop_checkArrayAssignmentIndices3 = verifyNotTree checkArrayAssignmentIndices "declare -A foo; foo=([i]=bar)"
prop_checkArrayAssignmentIndices4 = verifyTree checkArrayAssignmentIndices "typeset -A foo; foo+=(bar)"
prop_checkArrayAssignmentIndices5 = verifyTree checkArrayAssignmentIndices "arr=( [foo]= bar )"
prop_checkArrayAssignmentIndices6 = verifyTree checkArrayAssignmentIndices "arr=( [foo] = bar )"
prop_checkArrayAssignmentIndices7 = verifyTree checkArrayAssignmentIndices "arr=( var=value )"
prop_checkArrayAssignmentIndices8 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=bar )"
prop_checkArrayAssignmentIndices9 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=\"\" )"
checkArrayAssignmentIndices params root =
runNodeAnalysis check params root
where
assocs = getAssociativeArrays root
check _ t =
case t of
T_Assignment _ _ name [] (T_Array _ list) ->
let isAssoc = name `elem` assocs in
mapM_ (checkElement isAssoc) list
_ -> return ()
checkElement isAssociative t =
case t of
T_IndexedElement _ _ (T_Literal id "") ->
warn id 2192 "This array element has no value. Remove spaces after = or use \"\" for empty string."
T_IndexedElement {} ->
return ()
T_NormalWord _ parts ->
let literalEquals = do
part <- parts
(id, str) <- case part of
T_Literal id str -> [(id,str)]
_ -> []
guard $ '=' `elem` str
return $ warn id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it."
in
if (null literalEquals && isAssociative)
then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ."
else sequence_ literalEquals
_ -> return ()
prop_checkUnmatchableCases1 = verify checkUnmatchableCases "case foo in bar) true; esac"
prop_checkUnmatchableCases2 = verify checkUnmatchableCases "case foo-$bar in ??|*) true; esac"
prop_checkUnmatchableCases3 = verify checkUnmatchableCases "case foo in foo) true; esac"
prop_checkUnmatchableCases4 = verifyNot checkUnmatchableCases "case foo-$bar in foo*|*bar|*baz*) true; esac"
checkUnmatchableCases _ t =
case t of
T_CaseExpression _ word list ->
if isConstant word
then warn (getId word) 2194
"This word is constant. Did you forget the $ on a variable?"
else potentially $ do
pg <- wordToPseudoGlob word
return $ mapM_ (check pg) (concatMap (\(_,x,_) -> x) list)
_ -> return ()
where
check target candidate = potentially $ do
candidateGlob <- wordToPseudoGlob candidate
guard . not $ pseudoGlobsCanOverlap target candidateGlob
return $ warn (getId candidate) 2195
"This pattern will never match the case statement's word. Double check them."
prop_checkSubshellAsTest1 = verify checkSubshellAsTest "( -e file )"
prop_checkSubshellAsTest2 = verify checkSubshellAsTest "( 1 -gt 2 )"
prop_checkSubshellAsTest3 = verifyNot checkSubshellAsTest "( grep -c foo bar )"
prop_checkSubshellAsTest4 = verifyNot checkSubshellAsTest "[ 1 -gt 2 ]"
prop_checkSubshellAsTest5 = verify checkSubshellAsTest "( -e file && -x file )"
prop_checkSubshellAsTest6 = verify checkSubshellAsTest "( -e file || -x file && -t 1 )"
prop_checkSubshellAsTest7 = verify checkSubshellAsTest "( ! -d file )"
checkSubshellAsTest _ t =
case t of
T_Subshell id [w] -> check id w
_ -> return ()
where
check id t = case t of
(T_Banged _ w) -> check id w
(T_AndIf _ w _) -> check id w
(T_OrIf _ w _) -> check id w
(T_Pipeline _ _ [T_Redirecting _ _ (T_SimpleCommand _ [] (first:second:_))]) ->
checkParams id first second
_ -> return ()
checkParams id first second = do
when (fromMaybe False $ (`elem` unaryTestOps) <$> getLiteralString first) $
err id 2204 "(..) is a subshell. Did you mean [ .. ], a test expression?"
when (fromMaybe False $ (`elem` binaryTestOps) <$> getLiteralString second) $
warn id 2205 "(..) is a subshell. Did you mean [ .. ], a test expression?"
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

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

View File

@@ -29,7 +29,7 @@ import ShellCheck.Regex
import Control.Arrow (first) import Control.Arrow (first)
import Control.Monad.Identity import Control.Monad.Identity
import Control.Monad.Reader import Control.Monad.RWS
import Control.Monad.State import Control.Monad.State
import Control.Monad.Writer import Control.Monad.Writer
import Data.Char import Data.Char
@@ -40,16 +40,48 @@ import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
type Analysis = ReaderT Parameters (Writer [TokenComment]) () 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 { data Parameters = Parameters {
variableFlow :: [StackData], variableFlow :: [StackData],
parentMap :: Map.Map Id Token, parentMap :: Map.Map Id Token,
shellType :: Shell, shellType :: Shell,
shellTypeSpecified :: Bool shellTypeSpecified :: Bool,
rootNode :: Token
} }
-- TODO: Cache results of common AST ops here
data Cache = Cache {}
data Scope = SubshellScope String | NoneScope deriving (Show, Eq) data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
data StackData = data StackData =
StackScope Scope StackScope Scope
@@ -81,6 +113,14 @@ pScript s =
} }
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec 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 -> String -> TokenComment
makeComment severity id code note = makeComment severity id code note =
TokenComment id $ Comment severity code note TokenComment id $ Comment severity code note
@@ -95,6 +135,7 @@ style id code str = addComment $ makeComment StyleC id code str
makeParameters spec = makeParameters spec =
let params = Parameters { let params = Parameters {
rootNode = root,
shellType = fromMaybe (determineShell root) $ asShellType spec, shellType = fromMaybe (determineShell root) $ asShellType spec,
shellTypeSpecified = isJust $ asShellType spec, shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root, parentMap = getParentTree root,
@@ -112,6 +153,7 @@ prop_determineShell4 = determineShell (fromJust $ pScript
prop_determineShell5 = determineShell (fromJust $ pScript prop_determineShell5 = determineShell (fromJust $ pScript
"#shellcheck shell=sh\nfoo") == Sh "#shellcheck shell=sh\nfoo") == Sh
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
prop_determineShell7 = determineShell (fromJust $ pScript "#! /bin/ash") == Dash
determineShell t = fromMaybe Bash $ do determineShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString shellForExecutable shellString
@@ -125,8 +167,13 @@ determineShell t = fromMaybe Bash $ do
getCandidates (T_Annotation _ annotations s) = getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++ map forAnnotation annotations ++
[Just $ fromShebang s] [Just $ fromShebang s]
fromShebang (T_Script _ s t) = shellFor 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 | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
shellFor s = reverse . takeWhile (/= '/') . reverse $ s shellFor s = reverse . takeWhile (/= '/') . reverse $ s
@@ -170,16 +217,13 @@ isQuoteFreeNode strict tree t =
-- Are any subnodes inherently self-quoting? -- Are any subnodes inherently self-quoting?
isQuoteFreeContext t = isQuoteFreeContext t =
case t of case t of
TC_Noary _ DoubleBracket _ -> return True TC_Nullary _ DoubleBracket _ -> return True
TC_Unary _ DoubleBracket _ _ -> return True TC_Unary _ DoubleBracket _ _ -> return True
TC_Binary _ DoubleBracket _ _ _ -> return True TC_Binary _ DoubleBracket _ _ _ -> return True
TA_Sequence {} -> return True TA_Sequence {} -> return True
T_Arithmetic {} -> return True T_Arithmetic {} -> return True
T_Assignment {} -> return True T_Assignment {} -> return True
T_Redirecting {} -> return $ T_Redirecting {} -> return False
if strict then False else
-- Not true, just a hack to prevent warning about non-expansion refs
any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]
T_DoubleQuoted _ _ -> return True T_DoubleQuoted _ _ -> return True
T_DollarDoubleQuoted _ _ -> return True T_DollarDoubleQuoted _ _ -> return True
T_CaseExpression {} -> return True T_CaseExpression {} -> return True
@@ -211,6 +255,10 @@ getClosestCommand tree t =
getCommand t@(T_Redirecting {}) = return t getCommand t@(T_Redirecting {}) = return t
getCommand _ = Nothing getCommand _ = Nothing
getClosestCommandM t = do
tree <- asks parentMap
return $ getClosestCommand tree t
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token) usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
where where
go currentId (T_NormalWord id [word]:rest) go currentId (T_NormalWord id [word]:rest)
@@ -227,6 +275,12 @@ getPath tree t = t :
Nothing -> [] Nothing -> []
Just parent -> getPath tree parent 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 = isParentOf tree parent child =
elem (getId parent) . map getId $ getPath tree child elem (getId parent) . map getId $ getPath tree child
@@ -347,6 +401,7 @@ getModifiedVariables t =
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)] [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
--Points to 'for' rather than variable --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_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)] T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
_ -> [] _ -> []
@@ -644,6 +699,10 @@ headOrDefault def _ = def
[] -> Nothing [] -> Nothing
(r:_) -> Just r (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 = filterByAnnotation token =

View File

@@ -158,5 +158,25 @@ prop_sourceDirectiveDoesntFollowFile =
[("foo", "source bar"), ("bar", "baz=3")] [("foo", "source bar"), ("bar", "baz=3")]
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\"" "#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 [] return []
runTests = $quickCheckAll runTests = $quickCheckAll

View File

@@ -21,7 +21,7 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
-- This module contains checks that examine specific commands by name. -- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (runChecks module ShellCheck.Checks.Commands (checker
, ShellCheck.Checks.Commands.runTests , ShellCheck.Checks.Commands.runTests
) where ) where
@@ -34,8 +34,7 @@ import ShellCheck.Parser
import ShellCheck.Regex import ShellCheck.Regex
import Control.Monad import Control.Monad
import Control.Monad.Reader import Control.Monad.RWS
import Control.Monad.Writer
import Data.Char import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
@@ -49,22 +48,10 @@ data CommandName = Exactly String | Basename String
data CommandCheck = data CommandCheck =
CommandCheck CommandName (Token -> Analysis) CommandCheck CommandName (Token -> Analysis)
nullCheck :: Token -> Analysis
nullCheck _ = return ()
verify :: CommandCheck -> String -> Bool verify :: CommandCheck -> String -> Bool
verify f s = producesComments f s == Just True verify f s = producesComments (getChecker [f]) s == Just True
verifyNot f s = producesComments f s == Just False verifyNot f s = producesComments (getChecker [f]) s == Just False
producesComments :: CommandCheck -> String -> Maybe Bool
producesComments f s = do
root <- pScript s
return . not . null $ runList (defaultSpec root) [f]
composeChecks f g t = do
f t
g t
arguments (T_SimpleCommand _ _ (cmd:args)) = args arguments (T_SimpleCommand _ _ (cmd:args)) = args
@@ -92,13 +79,19 @@ commandChecks = [
,checkAliasesExpandEarly ,checkAliasesExpandEarly
,checkUnsetGlobs ,checkUnsetGlobs
,checkFindWithoutPath ,checkFindWithoutPath
,checkTimeParameters
,checkTimedCommand
,checkLocalScope
,checkDeprecatedTempfile
,checkDeprecatedEgrep
,checkDeprecatedFgrep
] ]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
buildCommandMap = foldl' addCheck Map.empty buildCommandMap = foldl' addCheck Map.empty
where where
addCheck map (CommandCheck name function) = addCheck map (CommandCheck name function) =
Map.insertWith' composeChecks name function map Map.insertWith' composeAnalyzers name function map
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
@@ -116,15 +109,17 @@ checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ d
basename = reverse . takeWhile (/= '/') . reverse basename = reverse . takeWhile (/= '/') . reverse
checkCommand _ _ = return () checkCommand _ _ = return ()
runList spec list = notes getChecker :: [CommandCheck] -> Checker
where getChecker list = Checker {
root = asScript spec perScript = const $ return (),
params = makeParameters spec perToken = checkCommand map
notes = execWriter $ runReaderT (doAnalysis (checkCommand map) root) params }
map = buildCommandMap list where
map = buildCommandMap list
runChecks spec = runList spec commandChecks
checker :: Parameters -> Checker
checker params = getChecker commandChecks
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'" prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
@@ -199,23 +194,27 @@ prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file" prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file" prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo" prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
checkGrepRe = CommandCheck (Basename "grep") (f . arguments) where checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd)
-- --regex=*(extglob) doesn't work. Fixme? -- --regex=*(extglob) doesn't work. Fixme?
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
skippable _ = False skippable _ = False
f [] = return () f _ [] = return ()
f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r f cmd (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f cmd r
f (re:_) = do f cmd (re:_) = do
when (isGlob re) $ when (isGlob re) $
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it." warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
let string = concat $ oversimplify re
if isConfusedGlobRegex string then unless (cmd `hasFlag` "F") $ do
warn (getId re) 2063 "Grep uses regex, but this looks like a glob." let string = concat $ oversimplify re
else potentially $ do if isConfusedGlobRegex string then
char <- getSuspiciousRegexWildcard string warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
return $ info (getId re) 2022 $ else potentially $ do
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'." char <- getSuspiciousRegexWildcard string
return $ info (getId re) 2022 $
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
wordStartingWith c = wordStartingWith c =
head . filter ([c] `isPrefixOf`) $ candidates head . filter ([c] `isPrefixOf`) $ candidates
@@ -344,7 +343,7 @@ checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
pattern = [ pattern = [
(`elem` ["-exec", "-execdir"]), (`elem` ["-exec", "-execdir"]),
(`elem` ["sh", "bash", "ksh"]), (`elem` ["sh", "bash", "dash", "ksh"]),
(== "-c") (== "-c")
] ]
action (id, arg) = action (id, arg) =
@@ -364,7 +363,7 @@ checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
then warnFor (list !! (length pattern - 1)) then warnFor (list !! (length pattern - 1))
else f rest else f rest
isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ] isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ] isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0", "-fls", "-fprint", "-fprint0", "-fprintf", "-ls", "-ok", "-okdir", "-printf" ]
isParam strs t = fromMaybe False $ do isParam strs t = fromMaybe False $ do
param <- getLiteralString t param <- getLiteralString t
return $ param `elem` strs return $ param `elem` strs
@@ -478,6 +477,7 @@ prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"" prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png" prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz" prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
@@ -487,6 +487,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
countFormats string = countFormats string =
case string of case string of
'%':'%':rest -> countFormats rest '%':'%':rest -> countFormats rest
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
'%':rest -> 1 + countFormats rest '%':rest -> 1 + countFormats rest
_:rest -> countFormats rest _:rest -> countFormats rest
[] -> 0 [] -> 0
@@ -619,5 +620,66 @@ checkFindWithoutPath = CommandCheck (Basename "find") f
hasPath [] = False 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 [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) 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 -- Other
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY", "USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY" "HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
-- Ksh
, ".sh.version"
] ]
variablesWithoutSpaces = [ variablesWithoutSpaces = [
@@ -82,12 +85,24 @@ sampleWords = [
"zulu" "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 :: String -> Maybe Shell
shellForExecutable name = shellForExecutable name =
case name of case name of
"sh" -> return Sh "sh" -> return Sh
"bash" -> return Bash "bash" -> return Bash
"dash" -> return Dash "dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh "ksh" -> return Ksh
"ksh88" -> return Ksh "ksh88" -> return Ksh
"ksh93" -> return Ksh "ksh93" -> return Ksh

View File

@@ -58,17 +58,18 @@ linefeed = do
c <- char '\n' c <- char '\n'
readPendingHereDocs readPendingHereDocs
return c return c
singleQuote = char '\'' <|> unicodeSingleQuote singleQuote = char '\''
doubleQuote = char '"' <|> unicodeDoubleQuote doubleQuote = char '"'
variableStart = upper <|> lower <|> oneOf "_" variableStart = upper <|> lower <|> oneOf "_"
variableChars = upper <|> lower <|> digit <|> oneOf "_" variableChars = upper <|> lower <|> digit <|> oneOf "_"
functionChars = variableChars <|> oneOf ":+-.?" functionChars = variableChars <|> oneOf ":+-.?"
specialVariable = oneOf "@*#?-$!" specialVariable = oneOf "@*#?-$!"
paramSubSpecialChars = oneOf "/:+-=%"
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
quotable = almostSpace <|> unicodeDoubleQuote <|> oneOf quotableChars quotable = almostSpace <|> oneOf quotableChars
bracedQuotable = oneOf "}\"$`'" bracedQuotable = oneOf "}\"$`'"
doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars doubleQuotableChars = "\"$`"
doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars doubleQuotable = oneOf doubleQuotableChars
whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed
linewhitespace = oneOf " \t" <|> almostSpace linewhitespace = oneOf " \t" <|> almostSpace
@@ -77,7 +78,8 @@ suspectCharAfterQuotes = variableChars <|> char '%'
extglobStartChars = "?*@!+" extglobStartChars = "?*@!+"
extglobStart = oneOf extglobStartChars extglobStart = oneOf extglobStartChars
unicodeDoubleQuoteChars = "\x201C\x201D\x2033\x2036" unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036"
unicodeSingleQuotes = "\x2018\x2019"
prop_spacing = isOk spacing " \\\n # Comment" prop_spacing = isOk spacing " \\\n # Comment"
spacing = do spacing = do
@@ -106,17 +108,12 @@ allspacingOrFail = do
s <- allspacing s <- allspacing
when (null s) $ fail "Expected whitespace" when (null s) $ fail "Expected whitespace"
unicodeDoubleQuote = do readUnicodeQuote = do
pos <- getPosition pos <- getPosition
oneOf unicodeDoubleQuoteChars c <- oneOf (unicodeSingleQuotes ++ unicodeDoubleQuotes)
parseProblemAt pos WarningC 1015 "This is a unicode double quote. Delete and retype it." parseProblemAt pos WarningC 1110 "This is a unicode quote. Delete and retype it (or quote to make literal)."
return '"' id <- getNextIdAt pos
return $ T_Literal id [c]
unicodeSingleQuote = do
pos <- getPosition
char '\x2018' <|> char '\x2019'
parseProblemAt pos WarningC 1016 "This is a unicode single quote. Delete and retype it."
return '"'
carriageReturn = do carriageReturn = do
parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ." parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
@@ -332,6 +329,13 @@ parseProblemAtWithEnd start end level code msg = do
parseProblemAt pos = parseProblemAtWithEnd pos pos parseProblemAt pos = parseProblemAtWithEnd pos pos
parseProblemAtId :: Monad m => Id -> Severity -> Integer -> String -> SCParser m ()
parseProblemAtId id level code msg = do
map <- getMap
let pos = Map.findWithDefault
(error "Internal error (no position for id). Please report.") id map
parseProblemAt pos level code msg
-- Store non-parse problems inside -- Store non-parse problems inside
parseNote c l a = do parseNote c l a = do
@@ -527,7 +531,7 @@ readConditionContents single =
condSpacing requiresSpacing condSpacing requiresSpacing
return x return x
readCondNoaryOrBinary = do readCondNullaryOrBinary = do
id <- getNextId id <- getNextId
x <- readCondWord `attempting` (do x <- readCondWord `attempting` (do
pos <- getPosition pos <- getPosition
@@ -544,7 +548,16 @@ readConditionContents single =
then readRegex then readRegex
else readCondWord <|> (parseProblemAt pos ErrorC 1027 "Expected another argument for this operator." >> mzero) else readCondWord <|> (parseProblemAt pos ErrorC 1027 "Expected another argument for this operator." >> mzero)
return (x `op` y) return (x `op` y)
) <|> return (TC_Noary id typ x) ) <|> ( do
checkTrailingOp x
return $ TC_Nullary id typ x
)
checkTrailingOp x = fromMaybe (return ()) $ do
(T_Literal id str) <- getTrailingUnquotedLiteral x
trailingOp <- listToMaybe (filter (`isSuffixOf` str) binaryTestOps)
return $ parseProblemAtId id ErrorC 1108 $
"You need a space before and after the " ++ trailingOp ++ " ."
readCondGroup = do readCondGroup = do
id <- getNextId id <- getNextId
@@ -621,7 +634,7 @@ readConditionContents single =
return $ TC_Unary id typ "!" expr return $ TC_Unary id typ "!" expr
readCondExpr = readCondExpr =
readCondGroup <|> readCondUnaryExp <|> readCondNoaryOrBinary readCondGroup <|> readCondUnaryExp <|> readCondNullaryOrBinary
readCondOr = chainl1 readCondAnd readCondAndOp readCondOr = chainl1 readCondAnd readCondAndOp
readCondAnd = chainl1 readCondTerm readCondOrOp readCondAnd = chainl1 readCondTerm readCondOrOp
@@ -930,6 +943,9 @@ prop_readNormalWord6 = isOk readNormalWord "foo/{}"
prop_readNormalWord7 = isOk readNormalWord "foo\\\nbar" prop_readNormalWord7 = isOk readNormalWord "foo\\\nbar"
prop_readNormalWord8 = isWarning readSubshell "(foo\\ \nbar)" prop_readNormalWord8 = isWarning readSubshell "(foo\\ \nbar)"
prop_readNormalWord9 = isOk readSubshell "(foo\\ ;\nbar)" prop_readNormalWord9 = isOk readSubshell "(foo\\ ;\nbar)"
prop_readNormalWord10 = isWarning readNormalWord "\x201Chello\x201D"
prop_readNormalWord11 = isWarning readNormalWord "\x2018hello\x2019"
prop_readNormalWord12 = isWarning readNormalWord "hello\x2018"
readNormalWord = readNormalishWord "" readNormalWord = readNormalishWord ""
readNormalishWord end = do readNormalishWord end = do
@@ -969,6 +985,7 @@ readNormalWordPart end = do
readBraced, readBraced,
readUnquotedBackTicked, readUnquotedBackTicked,
readProcSub, readProcSub,
readUnicodeQuote,
readNormalLiteral end, readNormalLiteral end,
readLiteralCurlyBraces readLiteralCurlyBraces
] ]
@@ -1003,13 +1020,19 @@ readDollarBracedWord = do
list <- many readDollarBracedPart list <- many readDollarBracedPart
return $ T_NormalWord id list return $ T_NormalWord id list
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readUnquotedBackTicked <|> readDollarBracedLiteral readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
readParamSubSpecialChar <|> readExtglob <|> readNormalDollar <|>
readUnquotedBackTicked <|> readDollarBracedLiteral
readDollarBracedLiteral = do readDollarBracedLiteral = do
id <- getNextId id <- getNextId
vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable
return $ T_Literal id $ concat vars return $ T_Literal id $ concat vars
readParamSubSpecialChar = do
id <- getNextId
T_ParamSubSpecialChar id <$> many1 paramSubSpecialChars
prop_readProcSub1 = isOk readProcSub "<(echo test | wc -l)" prop_readProcSub1 = isOk readProcSub "<(echo test | wc -l)"
prop_readProcSub2 = isOk readProcSub "<( if true; then true; fi )" prop_readProcSub2 = isOk readProcSub "<( if true; then true; fi )"
prop_readProcSub3 = isOk readProcSub "<( # nothing here \n)" prop_readProcSub3 = isOk readProcSub "<( # nothing here \n)"
@@ -1026,15 +1049,16 @@ readProcSub = called "process substitution" $ do
prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'" prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'"
prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'" prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'"
prop_readsingleQuoted3 = isWarning readSingleQuoted "\x2018hello\x2019"
prop_readSingleQuoted4 = isWarning readNormalWord "'it's" prop_readSingleQuoted4 = isWarning readNormalWord "'it's"
prop_readSingleQuoted5 = isWarning readSimpleCommand "foo='bar\ncow 'arg" prop_readSingleQuoted5 = isWarning readSimpleCommand "foo='bar\ncow 'arg"
prop_readSingleQuoted6 = isOk readSimpleCommand "foo='bar cow 'arg" prop_readSingleQuoted6 = isOk readSimpleCommand "foo='bar cow 'arg"
prop_readSingleQuoted7 = isOk readSingleQuoted "'foo\x201C\&bar'"
prop_readSingleQuoted8 = isWarning readSingleQuoted "'foo\x2018\&bar'"
readSingleQuoted = called "single quoted string" $ do readSingleQuoted = called "single quoted string" $ do
id <- getNextId id <- getNextId
startPos <- getPosition startPos <- getPosition
singleQuote singleQuote
s <- readSingleQuotedPart `reluctantlyTill` singleQuote s <- many readSingleQuotedPart
let string = concat s let string = concat s
endPos <- getPosition endPos <- getPosition
singleQuote <|> fail "Expected end of single quoted string" singleQuote <|> fail "Expected end of single quoted string"
@@ -1059,7 +1083,15 @@ readSingleQuotedLiteral = do
readSingleQuotedPart = readSingleQuotedPart =
readSingleEscaped readSingleEscaped
<|> many1 (noneOf "'\\\x2018\x2019") <|> many1 (noneOf $ "'\\" ++ unicodeSingleQuotes)
<|> readUnicodeQuote
where
readUnicodeQuote = do
pos <- getPosition
x <- oneOf unicodeSingleQuotes
parseProblemAt pos WarningC 1112
"This is a unicode quote. Delete and retype it (or ignore/doublequote for literal)."
return [x]
prop_readBackTicked = isOk (readBackTicked False) "`ls *.mp3`" prop_readBackTicked = isOk (readBackTicked False) "`ls *.mp3`"
@@ -1135,11 +1167,12 @@ parseForgettingContext alsoOnSuccess parser = do
prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\"" prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\""
prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\"" prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\""
prop_readDoubleQuoted3 = isWarning readDoubleQuoted "\x201Chello\x201D" prop_readDoubleQuoted3 = isOk readDoubleQuoted "\"\x2018hello\x2019\""
prop_readDoubleQuoted4 = isWarning readSimpleCommand "\"foo\nbar\"foo" prop_readDoubleQuoted4 = isWarning readSimpleCommand "\"foo\nbar\"foo"
prop_readDoubleQuoted5 = isOk readSimpleCommand "lol \"foo\nbar\" etc" prop_readDoubleQuoted5 = isOk readSimpleCommand "lol \"foo\nbar\" etc"
prop_readDoubleQuoted6 = isOk readSimpleCommand "echo \"${ ls; }\"" prop_readDoubleQuoted6 = isOk readSimpleCommand "echo \"${ ls; }\""
prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\"" prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\""
prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\""
readDoubleQuoted = called "double quoted string" $ do readDoubleQuoted = called "double quoted string" $ do
id <- getNextId id <- getNextId
startPos <- getPosition startPos <- getPosition
@@ -1164,7 +1197,15 @@ suggestForgotClosingQuote startPos endPos name = do
parseProblemAt endPos InfoC 1079 parseProblemAt endPos InfoC 1079
"This is actually an end quote, but due to next char it looks suspect." "This is actually an end quote, but due to next char it looks suspect."
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBackTicked doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBackTicked <|> readUnicodeQuote
where
readUnicodeQuote = do
pos <- getPosition
id <- getNextId
c <- oneOf unicodeDoubleQuotes
parseProblemAt pos WarningC 1111
"This is a unicode quote. Delete and retype it (or ignore/singlequote for literal)."
return $ T_Literal id [c]
readDoubleQuotedLiteral = do readDoubleQuotedLiteral = do
doubleQuote doubleQuote
@@ -1178,7 +1219,7 @@ readDoubleLiteral = do
return $ T_Literal id (concat s) return $ T_Literal id (concat s)
readDoubleLiteralPart = do readDoubleLiteralPart = do
x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars))) x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars ++ unicodeDoubleQuotes)))
return $ concat x return $ concat x
readNormalLiteral end = do readNormalLiteral end = do
@@ -1220,8 +1261,15 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
c <- extglobStart <|> char '[' c <- extglobStart <|> char '['
return $ T_Literal id [c] return $ T_Literal id [c]
readNormalLiteralPart end = readNormalLiteralPart customEnd =
readNormalEscaped <|> many1 (noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}")) readNormalEscaped <|>
many1 (noneOf (customEnd ++ standardEnd))
where
standardEnd = "[{}"
++ quotableChars
++ extglobStartChars
++ unicodeDoubleQuotes
++ unicodeSingleQuotes
readNormalEscaped = called "escaped char" $ do readNormalEscaped = called "escaped char" $ do
pos <- getPosition pos <- getPosition
@@ -1286,19 +1334,16 @@ readExtglobPart = do
readSingleEscaped = do readSingleEscaped = do
pos <- getPosition
s <- backslash s <- backslash
let attempt level code p msg = do { try $ parseNote level code msg; x <- p; return [s,x]; } x <- lookAhead anyChar
do { case x of
x <- lookAhead singleQuote; '\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\''s done'.";
parseProblem InfoC 1003 "Are you trying to escape that single quote? echo 'You'\\''re doing it wrong'."; '\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line."
return [s]; _ -> return ()
}
<|> attempt InfoC 1004 linefeed "You don't break lines with \\ in single quotes, it results in literal backslash-linefeed."
<|> do
x <- anyChar
return [s,x]
return [s]
readDoubleEscaped = do readDoubleEscaped = do
bs <- backslash bs <- backslash
@@ -1359,19 +1404,29 @@ readBraced = try braceExpansion
braceLiteral = braceLiteral =
T_Literal `withParser` readGenericLiteral1 (oneOf "{}\"$'," <|> whitespace) T_Literal `withParser` readGenericLiteral1 (oneOf "{}\"$'," <|> whitespace)
readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely ensureDollar =
readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely -- The grammar should have been designed along the lines of readDollarExpr = char '$' >> stuff, but
-- instead, each subunit parses its own $. This results in ~7 1-3 char lookaheads instead of one 1-char.
-- Instead of optimizing the grammar, here's a green cut that decreases shellcheck runtime by 10%:
lookAhead $ char '$'
readNormalDollar = do
ensureDollar
readDollarExp <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
readDoubleQuotedDollar = do
ensureDollar
readDollarExp <|> readDollarLonely
prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))" prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))"
prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)" prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)"
prop_readDollarExpression3 = isWarning readDollarExpression "$((\"$@\" &); foo;)" prop_readDollarExpression3 = isWarning readDollarExpression "$((\"$@\" &); foo;)"
readDollarExpression :: Monad m => SCParser m Token readDollarExpression :: Monad m => SCParser m Token
readDollarExpression = do readDollarExpression = do
-- The grammar should have been designed along the lines of readDollarExpr = char '$' >> stuff, but ensureDollar
-- instead, each subunit parses its own $. This results in ~7 1-3 char lookaheads instead of one 1-char. readDollarExp
-- Instead of optimizing the grammar, here's a green cut that decreases shellcheck runtime by 10%:
lookAhead $ char '$' readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
where where
arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos -> arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.") parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")
@@ -1527,40 +1582,32 @@ prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo"
prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n" prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"
prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n" prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n" prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"
readHereDoc = called "here document" $ do readHereDoc = called "here document" $ do
fid <- getNextId fid <- getNextId
pos <- getPosition pos <- getPosition
try $ string "<<" try $ string "<<"
dashed <- (char '-' >> return Dashed) <|> return Undashed dashed <- (char '-' >> return Dashed) <|> return Undashed
tokenPosition <- getPosition
sp <- spacing sp <- spacing
optional $ do optional $ do
try . lookAhead $ char '(' try . lookAhead $ char '('
let message = "Shells are space sensitive. Use '< <(cmd)', not '<<" ++ sp ++ "(cmd)'." let message = "Shells are space sensitive. Use '< <(cmd)', not '<<" ++ sp ++ "(cmd)'."
parseProblemAt pos ErrorC 1038 message parseProblemAt pos ErrorC 1038 message
hid <- getNextId hid <- getNextId
(quoted, endToken) <- (quoted, endToken) <- readToken
liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral
<|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral
<|> (readToken >>= (\x -> return (Unquoted, x)))
-- add empty tokens for now, read the rest in readPendingHereDocs -- add empty tokens for now, read the rest in readPendingHereDocs
let doc = T_HereDoc hid dashed quoted endToken [] let doc = T_HereDoc hid dashed quoted endToken []
addPendingHereDoc doc addPendingHereDoc doc
return doc return doc
where where
stripLiteral (T_Literal _ x) = x quotes = "\"'\\"
stripLiteral (T_SingleQuoted _ x) = x -- Fun fact: bash considers << foo"" quoted, but not << <("foo").
-- Instead of replicating this, just read a token and strip quotes.
readToken = readToken = do
liftM concat $ many1 (escaped <|> quoted <|> normal) str <- readStringForParser readNormalWord
where return (if any (`elem` quotes) str then Quoted else Unquoted,
quoted = liftM stripLiteral readDoubleQuotedLiteral <|> readSingleQuotedLiteral filter (not . (`elem` quotes)) str)
normal = anyChar `reluctantlyTill1` (whitespace <|> oneOf "<>;&)'\"\\")
escaped = do -- surely the user must be doing something wrong at this point
char '\\'
c <- anyChar
return [c]
readPendingHereDocs = do readPendingHereDocs = do
@@ -1674,11 +1721,19 @@ readLineBreak = optional readNewlineList
prop_readSeparator1 = isWarning readScript "a &; b" prop_readSeparator1 = isWarning readScript "a &; b"
prop_readSeparator2 = isOk readScript "a & b" prop_readSeparator2 = isOk readScript "a & b"
prop_readSeparator3 = isWarning readScript "a &amp; b"
prop_readSeparator4 = isWarning readScript "a &gt; file; b"
readSeparatorOp = do readSeparatorOp = do
notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator) notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator)
notFollowedBy2 (string "&>") notFollowedBy2 (string "&>")
f <- try (do f <- try (do
pos <- getPosition
char '&' char '&'
optional $ do
s <- lookAhead . choice . map (try . string) $
["amp;", "gt;", "lt;"]
parseProblemAt pos ErrorC 1109 "This is an unquoted HTML entity. Replace with corresponding character."
spacing spacing
pos <- getPosition pos <- getPosition
char ';' char ';'
@@ -2273,7 +2328,7 @@ readCompoundListOrEmpty = do
readCmdPrefix = many1 (readIoRedirect <|> readAssignmentWord) readCmdPrefix = many1 (readIoRedirect <|> readAssignmentWord)
readCmdSuffix = many1 (readIoRedirect <|> readCmdWord) readCmdSuffix = many1 (readIoRedirect <|> readCmdWord)
readModifierSuffix = many1 (readIoRedirect <|> readAssignmentWord <|> readCmdWord) readModifierSuffix = many1 (readIoRedirect <|> readWellFormedAssignment <|> readCmdWord)
readTimeSuffix = do readTimeSuffix = do
flags <- many readFlag flags <- many readFlag
pipeline <- readPipeline pipeline <- readPipeline
@@ -2339,12 +2394,16 @@ prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42" prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )" prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'" prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
readAssignmentWord = try $ do readAssignmentWord = readAssignmentWordExt True
readWellFormedAssignment = readAssignmentWordExt False
readAssignmentWordExt lenient = try $ do
id <- getNextId id <- getNextId
pos <- getPosition pos <- getPosition
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.") when lenient $
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")
variable <- readVariableName variable <- readVariableName
optional (readNormalDollar >> parseNoteAt pos ErrorC when lenient $
optional (readNormalDollar >> parseNoteAt pos ErrorC
1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'") 1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")
indices <- many readArrayIndex indices <- many readArrayIndex
hasLeftSpace <- liftM (not . null) spacing hasLeftSpace <- liftM (not . null) spacing
@@ -2563,6 +2622,7 @@ readScriptFile = do
verifyShell pos (getShell sb) verifyShell pos (getShell sb)
if isValidShell (getShell sb) /= Just False if isValidShell (getShell sb) /= Just False
then do then do
allspacing
annotationId <- getNextId annotationId <- getNextId
annotations <- readAnnotations annotations <- readAnnotations
commands <- withAnnotations annotations readCompoundListOrEmpty commands <- withAnnotations annotations readCompoundListOrEmpty
@@ -2587,8 +2647,8 @@ readScriptFile = do
verifyShell pos s = verifyShell pos s =
case isValidShell s of case isValidShell s of
Just True -> return () Just True -> return ()
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/ksh scripts. Sorry!" Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!"
Nothing -> parseProblemAt pos InfoC 1008 "This shebang was unrecognized. Note that ShellCheck only handles sh/bash/ksh." Nothing -> parseProblemAt pos InfoC 1008 "This shebang was unrecognized. Note that ShellCheck only handles sh/bash/dash/ksh."
isValidShell s = isValidShell s =
let good = s == "" || any (`isPrefixOf` s) goodShells let good = s == "" || any (`isPrefixOf` s) goodShells

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

@@ -9,6 +9,7 @@
,ShellCheck.Parser.runTests ,ShellCheck.Parser.runTests
,ShellCheck.Checker.runTests ,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests ,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.AnalyzerLib.runTests ,ShellCheck.AnalyzerLib.runTests
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr) ]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]] if [[ $var == *$'\nTrue'* ]]

View File

@@ -175,6 +175,15 @@ ShellCheck uses the follow exit codes:
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag). + 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter). + 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 # AUTHOR
ShellCheck is written and maintained by Vidar Holen. ShellCheck is written and maintained by Vidar Holen.

View File

@@ -31,6 +31,7 @@ import qualified ShellCheck.Formatter.TTY
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Data.Bits
import Data.Char import Data.Char
import Data.Functor import Data.Functor
import Data.Either import Data.Either
@@ -288,14 +289,47 @@ ioInterface options files = do
fallback path _ = return path fallback path _ = return path
inputFile file = do inputFile file = do
contents <- handle <-
if file == "-" if file == "-"
then getContents then return stdin
else readFile file else openBinaryFile file ReadMode
hSetBinaryMode handle True
contents <- decodeString <$> hGetContents handle -- closes handle
seq (length contents) $ seq (length contents) $
return 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 = verifyFiles files =
when (null files) $ do when (null files) $ do
printErr "No files specified.\n" printErr "No files specified.\n"

View File

@@ -7,12 +7,14 @@ import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.Parser import qualified ShellCheck.Parser
import qualified ShellCheck.Checks.Commands import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.ShellSupport
main = do main = do
putStrLn "Running ShellCheck tests..." putStrLn "Running ShellCheck tests..."
results <- sequence [ results <- sequence [
ShellCheck.Checker.runTests, ShellCheck.Checker.runTests,
ShellCheck.Checks.Commands.runTests, ShellCheck.Checks.Commands.runTests,
ShellCheck.Checks.ShellSupport.runTests,
ShellCheck.Analytics.runTests, ShellCheck.Analytics.runTests,
ShellCheck.AnalyzerLib.runTests, ShellCheck.AnalyzerLib.runTests,
ShellCheck.Parser.runTests ShellCheck.Parser.runTests