65 Commits

Author SHA1 Message Date
Vidar Holen
a21df2d88f Stable version 0.4.3
This is purely a bugfix bump that works on GHC 7.6.3,
as currently found in Debian stable and Ubuntu LTS.
2016-01-13 16:54:54 -08:00
Vidar Holen
d473fb8867 Use system over callCommand to allow linking on ghc 7.6.3. 2016-01-13 14:10:21 -08:00
Vidar Holen
f754363733 Add >= process-1.2.0.0 as dependency due to custom build. 2016-01-11 10:56:40 -08:00
Vidar Holen
ef1f8f535e Stable version 0.4.2
This release is dedicated to Tom Lehrer, who lives
the dream of having quit just to loaf about indefinitely.
2016-01-09 15:30:07 -08:00
Vidar Holen
f9909504dd Make SC2174 only trigger for nested directories. 2016-01-09 14:24:31 -08:00
koalaman
fa4cefda9d Merge pull request #572 from eatnumber1/mkdir
Add a warning when you mkdir with both -p and -m.
2016-01-09 13:58:53 -08:00
Russell Harmon
f2f6c66902 Add a warning when you mkdir with both -p and -m.
When using -p, parent directories will not be created with the mode
specified with -m and will instead be created using the default behavior
controlled by umask.
2016-01-08 14:15:11 -08:00
Vidar Holen
1f4dd85548 Change cabal Build-Type to Custom to actually use Setup.hs. 2016-01-05 14:10:45 -08:00
Vidar Holen
528381796e Allow escaped characters in [..] globs 2015-12-13 10:19:48 -08:00
Vidar Holen
ad7ad28246 Merge branch 'master' of github.com:koalaman/shellcheck 2015-12-12 15:53:18 -08:00
Vidar Holen
33ab998b02 Don't warn about quoting for [ -v var ] 2015-12-12 15:47:35 -08:00
koalaman
9c28237d52 Merge pull request #557 from eatnumber1/ignvars
Add more variables which are assumed to be set.
2015-12-10 20:26:00 -08:00
Russell Harmon
e0e5ba3a90 Add more variables which are assumed to be set.
This commit adds LC_MONETARY, LOGNAME, LD_LIBRARY_PATH, LANGUAGE,
DISPLAY, HOSTNAME, KRB5CCNAME, XAUTHORITY to be ignored when used
without initialization.
2015-12-10 20:05:50 -08:00
Vidar Holen
b4390414ef Fix remaining FIXME for tty error color output. 2015-12-06 12:48:53 -08:00
Vidar Holen
8acd5b13cd Add scripts for running shellcheck/tests interpreted. 2015-12-05 17:47:43 -08:00
Vidar Holen
d00ca0c283 Fix/generalize message for SC2070. 2015-12-05 17:29:06 -08:00
Vidar Holen
8bc98d89a7 Let SC2094 ignore echo/printf/sponge. 2015-12-05 16:50:11 -08:00
Vidar Holen
c7964a7a78 Warn about missing space in 'function foo{'. 2015-12-05 16:09:44 -08:00
Vidar Holen
8ec87d6655 Mention -- as alternative to ./* in SC2035 2015-12-05 13:45:56 -08:00
Vidar Holen
c3df2bf761 Don't warn about deprecated `` when just used for comments. 2015-12-05 13:33:39 -08:00
Vidar Holen
d1df3713ca Document --color in the man page. 2015-12-05 13:16:31 -08:00
Vidar Holen
23496e93b0 Fix compiler warning about missing field 2015-12-05 13:08:02 -08:00
koalaman
437e69fbba Merge pull request #553 from haguenau/add-color-switch
Add --color switch
2015-12-05 12:33:04 -08:00
David Haguenauer
63ad3f99ad Shorten long help line 2015-12-04 11:40:52 -05:00
David Haguenauer
0044c3dd6e Make use of --color with no argument equivalent to --color=always 2015-12-04 10:57:54 -05:00
David Haguenauer
a3d4101d6c Add initial support for --color 2015-12-03 17:55:56 -05:00
David Haguenauer
bd359c5c0f Delete trailing whitespace 2015-12-03 17:55:56 -05:00
Vidar Holen
498de63337 Mention that cabal installs to ~/.cabal/bin 2015-11-28 13:42:31 -08:00
Vidar Holen
52ab7dee2d Updating README.md to take over for shellcheck.net/about.html 2015-11-27 17:32:48 -08:00
Vidar Holen
1a5296659b Properly handle escaped double quotes in quoted backtick expressions. 2015-11-01 12:30:33 -08:00
Vidar Holen
a66ee2967c Trap warnings for kill/stop, non-XSI ints, "sig"-prefix and casing. 2015-10-31 17:36:24 -07:00
Vidar Holen
d985380f48 Consider "echo $(<file)" a bashism, and don't warn about UUOE. 2015-10-31 14:48:08 -07:00
Vidar Holen
6739c4a729 Count declare -p as a variable reference. 2015-10-31 14:16:45 -07:00
Vidar Holen
7415c9dcb7 Warn about non-posix function names like x-y 2015-10-31 13:53:17 -07:00
Vidar Holen
d3fc1f355d Merge branch 'master' of github.com:koalaman/shellcheck 2015-10-31 13:36:52 -07:00
Vidar Holen
48fd793581 Update getFlag function to also return non-flags. 2015-10-27 22:07:29 -07:00
koalaman
e5842e2e2b Merge pull request #516 from Fusl/patch-01
Fix typo "zyper" -> "zypper"
2015-10-22 13:59:50 -07:00
Fusl
cf445c7d20 Fix typo "zyper" -> "zypper" 2015-10-21 11:29:01 +02:00
Vidar Holen
ffb9578a98 Support parsing quoted test operators, and also warn about unicode dashes. 2015-10-17 16:33:21 -07:00
Vidar Holen
630f20e888 Count ~/ as dynamic for resolving source paths. 2015-10-17 10:50:07 -07:00
Vidar Holen
8f5f91f041 Warn about ]] with no corresponding [[. 2015-10-17 10:31:14 -07:00
Vidar Holen
8d9d4533c3 Don't trigger constant checks for -ot/-ef/-nt 2015-10-17 09:28:29 -07:00
koalaman
a4b4954a23 Merge pull request #512 from mimi1vx/opensuse-docu
install instructions for openSUSE
2015-10-17 09:10:12 -07:00
Ondřej Súkup
38cea9201d install instructions for openSUSE 2015-10-17 17:17:32 +02:00
Vidar Holen
4ce916ec1d Include bash builtin arrays in SC2128. 2015-10-14 12:31:09 -07:00
Vidar Holen
b9cb040128 Account for dereferencing for numerical ops in [[ ]]. 2015-10-14 11:11:17 -07:00
Vidar Holen
2488be7298 Don't warn about undefined HOSTNAME if it's being assigned. 2015-10-14 09:21:21 -07:00
Vidar Holen
d01b59a827 Don't warn about empty assignments when at end of command. 2015-10-13 12:16:39 -07:00
Vidar Holen
f77821625c Add dash as a first class supported shell. 2015-10-13 11:37:50 -07:00
Vidar Holen
1eece5b2ee Add warning about local in sh or not in bash functions. 2015-10-10 20:48:52 -07:00
Vidar Holen
58d45e3fa4 Warn about non-posix %q and flags for printf. 2015-10-10 20:14:17 -07:00
Vidar Holen
5aaa1a7d9a Split SC2165 messages into separate codes (2167). 2015-10-10 19:26:14 -07:00
Vidar Holen
3b36c2c820 Document exit codes in man page. 2015-10-10 19:19:38 -07:00
Vidar Holen
55692926b9 Don't consider {} and {a} brace expansions. 2015-10-03 21:12:28 -07:00
Vidar Holen
4172722167 Don't warn about quoted rhs of =~ when not a regex. 2015-10-03 15:21:57 -07:00
Vidar Holen
485593da2c Don't warn about disrespected quotes in ${#var}. 2015-10-01 18:56:45 -07:00
Vidar Holen
1181c6b3af Warn about ${!var} in POSIX mode. 2015-10-01 18:34:09 -07:00
Vidar Holen
ee181cfc43 Don't warn about comparisons when redirecting stderr for test. 2015-10-01 18:19:47 -07:00
Vidar Holen
c72667407b Merge branch 'master' of github.com:koalaman/shellcheck 2015-09-28 18:53:51 -07:00
Vidar Holen
5467a0f1d9 Account for set -o errexit and #!/bin/bash -e for unchecked cd. 2015-09-28 18:52:03 -07:00
Vidar Holen
3fc77d94ec Warn about [ -n foo ] when foo is entirely constant. 2015-09-28 18:46:32 -07:00
koalaman
23e0420cb1 Merge pull request #469 from spartacus06/fedora-readme
Add Fedora install procedure to README.md
2015-09-21 08:29:44 -07:00
Seth Jennings
a898165ac7 Add Fedora install procedure to README.md 2015-09-21 09:12:19 -05:00
Vidar Holen
ba5e3db31a Stable version 0.4.1
This is purely a bugfix bump because I botched the
0.4.0 cabal file and subsequent Hackage release :|
2015-09-05 19:32:37 -07:00
Vidar Holen
56145217fe Fixed the cabal file, which didn't include formatters :| 2015-09-05 19:17:22 -07:00
17 changed files with 795 additions and 196 deletions

270
README.md
View File

@@ -1,26 +1,67 @@
# ShellCheck - A shell script static analysis tool
http://www.shellcheck.net
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
Copyright 2012-2015, Vidar 'koala_man' Holen
Licensed under the GNU General Public License, v3
![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png).
The goals of ShellCheck are:
The goals of ShellCheck are
- To point out and clarify typical beginner's syntax issues,
- To point out and clarify typical beginner's syntax issues
that causes a shell to give cryptic error messages.
- To point out and clarify typical intermediate level semantic problems,
- To point out and clarify typical intermediate level semantic problems
that causes a shell to behave strangely and counter-intuitively.
- To point out subtle caveats, corner cases and pitfalls, that may cause an
- To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances.
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
## How to use
There are a variety of ways to use ShellCheck!
#### On the web
Paste a shell script on http://www.shellcheck.net for instant feedback.
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the simplest way to give ShellCheck a go. Tell your friends!
#### From your terminal
Run `shellcheck yourscript` in your terminal for instant output, as seen above.
#### In your editor
You can see ShellCheck suggestions directly in a variety of editors.
* Vim, through [Syntastic](https://github.com/scrooloose/syntastic):
![Screenshot of vim showing inlined shellcheck feedback](doc/vim-syntastic.png).
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
![Screenshot of emacs showing inlined shellcheck feedback](doc/emacs-flycheck.png).
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
* Most other editors, through [GCC error compatibility](blob/master/shellcheck.1.md#user-content-formats).
#### In your build or test suites
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
Use ShellCheck's exit code, or it's [CheckStyle compatible XML output](blob/master/shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
ShellCheck is written in Haskell, and requires 2 GB of memory to compile.
## Installing
On systems with Cabal:
The easiest way to install ShellCheck locally is through your package manager.
On systems with Cabal (installs to `~/.cabal/bin`):
cabal update
cabal install shellcheck
@@ -29,54 +70,60 @@ On Debian based distros:
apt-get install shellcheck
On Fedora based distros:
dnf install ShellCheck
On OS X with homebrew:
brew install shellcheck
ShellCheck is also available as an online service:
On OS X with MacPorts:
http://www.shellcheck.net
port install shellcheck
## Building with Cabal
On openSUSE:Tumbleweed:
This sections describes how to build ShellCheck from a source directory.
zypper in ShellCheck
First, make sure cabal is installed. On Debian based distros:
On other openSUSE distributions:
apt-get install cabal-install
add OBS devel:languages:haskell repository from https://build.opensuse.org/project/repositories/devel:languages:haskell
On Fedora:
zypper ar http://download.opensuse.org/repositories/devel:/languages:/haskell/openSUSE_$(version)/devel:languages:haskell.repo
zypper in ShellCheck
yum install cabal-install
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
On Mac OS X with homebrew (http://brew.sh/):
brew install cabal-install
## Compiling from source
On Mac OS X with MacPorts (http://www.macports.org/):
This sections describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
port install hs-cabal-install
On native Windows (https://www.haskell.org/platform/):
#### Installing Cabal
Download and install the latest version of the Haskell Platform.
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`).
Let cabal update itself, in case your distro version is outdated:
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/
Verify that `cabal` is installed and update its dependency list with
$ cabal update
$ cabal install cabal-install
With cabal installed, cd to the ShellCheck source directory and:
#### Compiling ShellCheck
`git clone` this repository, and `cd` to the ShellCheck source directory to build/install:
$ cabal install
This will install ShellCheck to your `~/.cabal/bin` directory.
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
Add the directory to your `PATH` (for bash, add this to your `~/.bashrc`):
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
export PATH="$HOME/.cabal/bin:$PATH"
Verify that your PATH is set up correctly:
Log out and in again, and verify that your PATH is set up correctly:
$ which shellcheck
~/.cabal/bin/shellcheck
@@ -93,13 +140,155 @@ In Powershell ISE, you may need to additionally update the output encoding:
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
## Running tests
#### Running tests
To run the unit test suite:
cabal configure --enable-tests
cabal build
cabal test
$ cabal test
## Gallery of bad code
So what kind of things does ShellCheck look for? Here is an incomplete list of detected issues.
#### Quoting
ShellCheck can recognize several types of incorrect quoting:
echo $1 # Unquoted variables
find . -name *.ogg # Unquoted find/grep patterns
rm "~/my file.txt" # Quoted tilde expansion
v='--verbose="true"'; cmd $v # Literal quotes in variables
for f in "*.ogg" # Incorrectly quoted 'for' loops
touch $@ # Unquoted $@
echo 'Don't forget to restart!' # Singlequote closed by apostrophe
echo 'Don\'t try this at home' # Attempting to escape ' in ''
echo 'Path is $PATH' # Variables in single quotes
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
#### Conditionals
ShellCheck can recognize many types of incorrect test statements.
[[ n != 0 ]] # Constant test expressions
[[ -e *.mpg ]] # Existence checks of globs
[[ $foo==0 ]] # Always true due to missing spaces
[[ -n "$foo " ]] # Always true due to literals
[[ $foo =~ "fo+" ]] # Quoted regex in =~
[ foo =~ re ] # Unsupported [ ] operators
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
[ $n && $m ] # && in [ .. ]
[ grep -q foo file ] # Command without $(..)
#### Frequently misused commands
ShellCheck can recognize instances where commands are used incorrectly:
grep '*foo*' file # Globs in regex contexts
find . -exec foo {} && bar {} \; # Prematurely terminated find -exec
sudo echo 'Var=42' > /etc/profile # Redirecting sudo
time --format=%s sleep 10 # Passing time(1) flags to time builtin
while read h; do ssh "$h" uptime # Commands eating while loop input
alias archive='mv $1 /backup' # Defining aliases with arguments
tr -cd '[a-zA-Z0-9]' # [] around ranges in tr
exec foo; echo "Done!" # Misused 'exec'
find -name \*.bak -o -name \*~ -delete # Implicit precedence in find
f() { whoami; }; sudo f # External use of internal functions
#### Common beginner's mistakes
ShellCheck recognizes many common beginner's syntax errors:
var = 42 # Spaces around = in assignments
$foo=42 # $ in assignments
for $var in *; do ... # $ in for loop variables
var$n="Hello" # Wrong indirect assignment
echo ${var$n} # Wrong indirect reference
var=(1, 2, 3) # Comma separated arrays
echo "Argument 10 is $10" # Positional parameter misreference
if $(myfunction); then ..; fi # Wrapping commands in $()
else if othercondition; then .. # Using 'else if'
#### Style
ShellCheck can make suggestions to improve style:
[[ -z $(find /tmp | grep mpg) ]] # Use grep -q instead
a >> log; b >> log; c >> log # Use a redirection block instead
echo "The time is `date`" # Use $() instead
cd dir; process *; cd ..; # Use subshells instead
echo $[1+2] # Use standard $((..)) instead of old $[]
echo $(($RANDOM % 6)) # Don't use $ on variables in $((..))
echo "$(date)" # Useless use of echo
cat file | grep foo # Useless use of cat
#### Data and typing errors
ShellCheck can recognize issues related to data and typing:
args="$@" # Assigning arrays to strings
files=(foo bar); echo "$files" # Referencing arrays as strings
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays.
[[ $# > 2 ]] # Comparing numbers as strings
var=World; echo "Hello " var # Unused lowercase variables
echo "Hello $name" # Unassigned lowercase variables
cmd | read bar; echo $bar # Assignments in subshells
#### Robustness
ShellCheck can make suggestions for improving the robustness of a script:
rm -rf "$STEAMROOT/"* # Catastrophic rm
touch ./-l; ls * # Globs that could become options
find . -exec sh -c 'a && b {}' \; # Find -exec shell injection
printf "Hello $name" # Variables in printf format
for f in $(ls *.txt); do # Iterating over ls output
export MYVAR=$(cmd) # Masked exit codes
#### Portability
ShellCheck will warn when using features not supported by the shebang. For example, if you set the shebang to `#!/bin/sh`, ShellCheck will warn about portability issues similar to `checkbashisms`:
echo {1..$n} # Works in ksh, but not bash/dash/sh
echo {1..10} # Works in ksh and bash, but not dash/sh
echo -n 42 # Works in ksh, bash and dash, undefined in sh
trap 'exit 42' sigint # Unportable signal spec
cmd &> file # Unportable redirection operator
read foo < /dev/tcp/host/22 # Unportable intercepted files
foo-bar() { ..; } # Undefined/unsupported function name
[ $UID = 0 ] # Variable undefined in dash/sh
local var=value # local is undefined in sh
#### Miscellaneous
ShellCheck recognizes a menagerie of other issues:
PS1='\e[0;32m\$\e[0m ' # PS1 colors not in \[..\]
PATH="$PATH:~/bin" # Literal tilde in $PATH
rm “file” # Unicode quotes
echo "Hello world" # Carriage return / DOS line endings
var=42 echo $var # Expansion of inlined environment
#!/bin/bash -x -e # Common shebang errors
echo $((n/180*100)) # Unnecessary loss of precision
ls *[:digit:].txt # Bad character class globs
sed 's/foo/bar/ file > file # Redirecting to input
## Testimonials
> At first you're like "shellcheck is awesome" but then you're like "wtf are we still using bash"
Alexander Tarasikov,
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)
## Reporting bugs
@@ -107,4 +296,19 @@ Please use the Github issue tracker for any bugs or feature suggestions:
https://github.com/koalaman/shellcheck/issues
## Contributing
Please submit patches to code or documentation as Github pull requests!
Contributions must be licensed under the GNU GPLv3.
The contributor retains the copyright.
## Copyright
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2015, Vidar 'koala_man' Holen and contributors.
Happy ShellChecking!

View File

@@ -8,21 +8,13 @@ import Distribution.Simple (
simpleUserHooks )
import Distribution.Simple.Setup ( SDistFlags )
-- | This requires the process package from,
--
-- https://hackage.haskell.org/package/process
--
import System.Process ( callCommand )
import System.Process ( system )
-- | This will use almost the default implementation, except we switch
-- out the default pre-sdist hook with our own, 'myPreSDist'.
--
main = defaultMainWithHooks myHooks
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
-- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message:
@@ -35,9 +27,10 @@ main = defaultMainWithHooks myHooks
--
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do
putStrLn "Building the man page..."
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
callCommand pandoc_cmd
result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
return emptyHookedBuildInfo
where
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,5 +1,5 @@
Name: ShellCheck
Version: 0.4.0
Version: 0.4.3
Synopsis: Shell script analysis tool
License: GPL-3
License-file: LICENSE
@@ -7,7 +7,7 @@ Category: Static Analysis
Author: Vidar Holen
Maintainer: vidar@vidarholen.net
Homepage: http://www.shellcheck.net/
Build-Type: Simple
Build-Type: Custom
Cabal-Version: >= 1.8
Bug-reports: https://github.com/koalaman/shellcheck/issues
Description:
@@ -44,7 +44,9 @@ library
mtl >= 2.2.1,
parsec,
regex-tdfa,
QuickCheck >= 2.7.4
QuickCheck >= 2.7.4,
-- When cabal supports it, move this to setup-depends:
process
exposed-modules:
ShellCheck.AST
ShellCheck.ASTLib
@@ -53,6 +55,10 @@ library
ShellCheck.Checker
ShellCheck.Data
ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle
ShellCheck.Formatter.GCC
ShellCheck.Formatter.JSON
ShellCheck.Formatter.TTY
ShellCheck.Interface
ShellCheck.Parser
ShellCheck.Regex

View File

@@ -93,14 +93,17 @@ oversimplify token =
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
-- each in a tuple of (token, stringFlag).
-- each in a tuple of (token, stringFlag). Non-flag arguments are added with
-- stringFlag == "".
getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
let textArgs = takeWhile (not . stopCondition . snd) $ map (\x -> (x, concat $ oversimplify x)) args in
concatMap flag textArgs
let tokenAndText = map (\x -> (x, concat $ oversimplify x)) args
(flagArgs, rest) = break (stopCondition . snd) tokenAndText
in
concatMap flag flagArgs ++ map (\(t, _) -> (t, "")) rest
where
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
flag (x, '-':args) = map (\v -> (x, [v])) args
flag _ = []
flag (x, _) = [ (x, "") ]
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
-- Get all flags in a GNU way, up until --
@@ -224,6 +227,16 @@ isAssignment t =
T_Annotation _ _ w -> isAssignment w
otherwise -> False
isOnlyRedirection t =
case t of
T_Pipeline _ _ [x] -> isOnlyRedirection x
T_Annotation _ _ w -> isOnlyRedirection w
T_Redirecting _ (_:_) c -> isOnlyRedirection c
T_SimpleCommand _ [] [] -> True
otherwise -> False
isFunction t = case t of T_Function {} -> True; _ -> False
-- Get the list of commands from tokens that contain them, such as
-- the body of while loops and if statements.
getCommandSequences t =

View File

@@ -74,6 +74,11 @@ checksFor Sh = [
,checkTimeParameters
,checkForDecimals
]
checksFor Dash = [
checkBashisms
,checkForDecimals
,checkLocalScope
]
checksFor Ksh = [
checkEchoSed
]
@@ -82,6 +87,7 @@ checksFor Bash = [
,checkBraceExpansionVars
,checkEchoSed
,checkForDecimals
,checkLocalScope
]
runAnalytics :: AnalysisSpec -> AnalysisResult
@@ -210,9 +216,11 @@ nodeChecks = [
,checkReadWithoutR
,checkExportedExpansions
,checkLoopVariableReassignment
,checkTrailingBracket
,checkNonportableSignals
,checkMkdirDashPM
]
filterByAnnotation token =
filter (not . shouldIgnore)
where
@@ -224,6 +232,7 @@ filterByAnnotation token =
any hasNum anns
where
hasNum (DisableComment ts) = num == ts
hasNum _ = False
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
shouldIgnoreFor _ _ = False
parents = getParentTree token
@@ -577,17 +586,39 @@ 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_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"
checkBashisms _ = bashism
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"
checkBashisms params = bashism
where
errMsg id s = err id 2040 $ "In sh, " ++ s ++ " not supported, even when sh is actually bash."
warnMsg id s = warn id 2039 $ "In POSIX sh, " ++ s ++ " not supported."
bashism (T_ProcSub id _ _) = errMsg id "process substitution is"
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"
@@ -599,8 +630,10 @@ checkBashisms _ = bashism
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", "\\<", "\\>", "==" ] =
warnMsg id $ op ++ " is"
| 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 _)
@@ -621,10 +654,11 @@ checkBashisms _ = bashism
warnMsg id $ fromJust str ++ " is"
where
str = getLiteralString t
isBashism = isJust str && fromJust str `elem` bashVars
isBashism = isJust str && isBashVariable (fromJust str)
bashism t@(T_DollarBraced id token) = do
mapM_ check expansion
when (var `elem` bashVars) $ warnMsg id $ var ++ " is"
when (isBashVariable var) $
warnMsg id $ var ++ " is"
where
str = bracedString t
var = getBracedReference str
@@ -640,10 +674,23 @@ checkBashisms _ = bashism
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 "-------"
warnMsg (getId arg) "echo flags are"
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) =
@@ -655,31 +702,47 @@ checkBashisms _ = bashism
let name = fromMaybe "" $ getCommandName t
flags = getLeadingFlags t
in do
when (name `elem` bashCommands) $ warnMsg id $ "'" ++ name ++ "' is"
when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is"
potentially $ do
allowed <- Map.lookup name allowedFlags
(word, flag) <- listToMaybe $ filter (\x -> snd x `notElem` allowed) flags
(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
word <- liftM (map toLower) $ getLiteralString token
guard $ word `elem` ["err", "debug", "return"]
return $ warnMsg (getId token) $ "trapping " ++ word ++ " is"
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 (reverse rest)
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
bashCommands = [
unsupportedCommands = [
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend", "type",
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
"typeset"
]
] ++ if not isDash then ["local", "type"] else []
allowedFlags = Map.fromList [
("read", ["r"]),
("read", if isDash then ["r", "p"] else ["r"]),
("ulimit", ["f"]),
("echo", []),
("printf", []),
("exec", [])
]
@@ -687,6 +750,7 @@ checkBashisms _ = bashism
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"),
@@ -694,9 +758,19 @@ checkBashisms _ = bashism
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
]
bashVars = [
"RANDOM", "LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SECONDS", "SHLVL", "PIPESTATUS", "SHELLOPTS"
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
]
bashDynamicVars = [ "RANDOM", "SECONDS" ]
isBashVariable var =
var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var)
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_checkForInQuoted2 = verifyNot checkForInQuoted "for f in \"$@\"; do echo foo; done"
@@ -822,6 +896,8 @@ prop_checkRedirectToSame2 = verify checkRedirectToSame "cat lol | sed -e 's/a/b/
prop_checkRedirectToSame3 = verifyNot checkRedirectToSame "cat lol | sed -e 's/a/b/g' > foo.bar && mv foo.bar lol"
prop_checkRedirectToSame4 = verifyNot checkRedirectToSame "foo /dev/null > /dev/null"
prop_checkRedirectToSame5 = verifyNot checkRedirectToSame "foo > bar 2> bar"
prop_checkRedirectToSame6 = verifyNot checkRedirectToSame "echo foo > foo"
prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file"
checkRedirectToSame params s@(T_Pipeline _ _ list) =
mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list
where
@@ -831,7 +907,8 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
when (exceptId /= newId
&& x == y
&& not (isOutput t && isOutput u)
&& not (special t)) $ do
&& not (special t)
&& not (any isHarmlessCommand [t,u])) $ do
addComment $ note newId
addComment $ note exceptId
checkOccurrences _ _ = return ()
@@ -854,6 +931,11 @@ checkRedirectToSame params s@(T_Pipeline _ _ list) =
T_DGREAT _ -> True
_ -> False
_ -> False
isHarmlessCommand arg = fromMaybe False $ do
cmd <- getClosestCommand (parentMap params) arg
name <- getCommandBasename cmd
return $ name `elem` ["echo", "printf", "sponge"]
checkRedirectToSame _ _ = return ()
@@ -901,7 +983,7 @@ checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree
err (getId x) 2068
"Double quote array expansions to avoid re-splitting elements."
where
-- Fixme: should detect whether the alterantive is quoted
-- Fixme: should detect whether the alternative is quoted
isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b
isAlternative _ = False
checkUnquotedDollarAt _ _ = return ()
@@ -944,24 +1026,26 @@ prop_checkArrayWithoutIndex2 = verifyNotTree checkArrayWithoutIndex "foo='bar ba
prop_checkArrayWithoutIndex3 = verifyTree checkArrayWithoutIndex "coproc foo while true; do echo cow; done; echo $foo"
prop_checkArrayWithoutIndex4 = verifyTree checkArrayWithoutIndex "coproc tail -f log; echo $COPROC"
prop_checkArrayWithoutIndex5 = verifyTree checkArrayWithoutIndex "a[0]=foo; echo $a"
prop_checkArrayWithoutIndex6 = verifyTree checkArrayWithoutIndex "echo $PIPESTATUS"
checkArrayWithoutIndex params _ =
concat $ doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)
concat $ doVariableFlowAnalysis readF writeF defaultMap (variableFlow params)
where
defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables
readF _ (T_DollarBraced id token) _ = do
map <- get
return . maybeToList $ do
name <- getLiteralString token
assignment <- Map.lookup name map
assigned <- Map.lookup name map
return [makeComment WarningC id 2128
"Expanding an array without an index only gives the first element."]
readF _ _ _ = return []
writeF _ t name (DataArray _) = do
modify (Map.insert name t)
modify (Map.insert name ())
return []
writeF _ expr name _ = do
if isIndexed expr
then modify (Map.insert name expr)
then modify (Map.insert name ())
else modify (Map.delete name)
return []
@@ -1028,9 +1112,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
commandName = fromMaybe "" $ do
cmd <- getClosestCommand parents t
name <- getCommandBasename cmd
if name == "find"
then return $ getFindCommand cmd
else return name
return $ if name == "find" then getFindCommand cmd else name
isProbablyOk =
any isOkAssignment (take 3 $ getPath parents t)
@@ -1071,8 +1153,9 @@ checkSingleQuotedVariables _ _ = return ()
prop_checkUnquotedN = verify checkUnquotedN "if [ -n $foo ]; then echo cow; fi"
prop_checkUnquotedN2 = verify checkUnquotedN "[ -n $cow ]"
prop_checkUnquotedN3 = verifyNot checkUnquotedN "[[ -n $foo ]] && echo cow"
checkUnquotedN _ (T_Condition _ SingleBracket (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t]))) | willSplit t =
err id 2070 "Always true because you failed to quote. Use [[ ]] instead."
prop_checkUnquotedN4 = verify checkUnquotedN "[ -n $cow -o -t 1 ]"
checkUnquotedN _ (TC_Unary _ SingleBracket "-n" (T_NormalWord id [t])) | willSplit t =
err id 2070 "-n doesn't work with unquoted arguments. Quote or use [[ ]]."
checkUnquotedN _ _ = return ()
prop_checkNumberComparisons1 = verify checkNumberComparisons "[[ $foo < 3 ]]"
@@ -1082,9 +1165,9 @@ prop_checkNumberComparisons4 = verify checkNumberComparisons "[[ $foo > 2.72 ]]"
prop_checkNumberComparisons5 = verify checkNumberComparisons "[[ $foo -le 2.72 ]]"
prop_checkNumberComparisons6 = verify checkNumberComparisons "[[ 3.14 -eq $foo ]]"
prop_checkNumberComparisons7 = verifyNot checkNumberComparisons "[[ 3.14 == $foo ]]"
prop_checkNumberComparisons8 = verify checkNumberComparisons "[[ foo <= bar ]]"
prop_checkNumberComparisons8 = verify checkNumberComparisons "[ foo <= bar ]"
prop_checkNumberComparisons9 = verify checkNumberComparisons "[ foo \\>= bar ]"
prop_checkNumberComparisons11= verify checkNumberComparisons "[[ $foo -eq 'N' ]]"
prop_checkNumberComparisons11= verify checkNumberComparisons "[ $foo -eq 'N' ]"
prop_checkNumberComparisons12= verify checkNumberComparisons "[ x$foo -gt x${N} ]"
checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
if isNum lhs && not (isNonNum rhs)
@@ -1106,15 +1189,15 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
when (op `elem` ["-lt", "-gt", "-le", "-ge", "-eq"]) $ do
mapM_ checkDecimals [lhs, rhs]
checkStrings [lhs, rhs]
when (typ == SingleBracket) $
checkStrings [lhs, rhs]
where
isLtGt = flip elem ["<", "\\<", ">", "\\>"]
isLeGe = flip elem ["<=", "\\<=", ">=", "\\>="]
supportsDecimals = (shellType params) == Ksh
checkDecimals hs =
when (isFraction hs && not supportsDecimals) $
when (isFraction hs && not (hasFloatingPoint params)) $
err (getId hs) 2072 decimalError
decimalError = "Decimals are not supported. " ++
"Either use integers only, or use bc or awk to compare."
@@ -1127,8 +1210,8 @@ checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do
return . not . all numChar $ s
numChar x = isDigit x || x `elem` "+-. "
stringError t = err (getId t) 2130 $
op ++ " is for integer comparisons. Use " ++ seqv op ++ " instead."
stringError t = err (getId t) 2170 $
"Numerical " ++ op ++ " does not dereference in [..]. Expand or use string operator."
isNum t =
case oversimplify t of
@@ -1204,16 +1287,26 @@ checkConditionalAndOrs _ t =
otherwise -> return ()
prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"
prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ 'cow' ]]"
prop_checkQuotedCondRegex1 = verify checkQuotedCondRegex "[[ $foo =~ \"bar.*\" ]]"
prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)' ]]"
prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]"
prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"
prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]"
checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
case rhs of
T_NormalWord id [T_DoubleQuoted _ _] -> error id
T_NormalWord id [T_SingleQuoted _ _] -> error id
T_NormalWord id [T_DoubleQuoted _ _] -> error rhs
T_NormalWord id [T_SingleQuoted _ _] -> error rhs
_ -> return ()
where
error id = err id 2076 "Don't quote rhs of =~, it'll match literally rather than as a regex."
error t =
unless (isConstantNonRe t) $
err (getId t) 2076
"Don't quote rhs of =~, it'll match literally rather than as a regex."
re = mkRegex "[][*.+()]"
hasMetachars s = s `matches` re
isConstantNonRe t = fromMaybe False $ do
s <- getLiteralString t
return . not $ hasMetachars s
checkQuotedCondRegex _ _ = return ()
prop_checkGlobbedRegex1 = verify checkGlobbedRegex "[[ $foo =~ *foo* ]]"
@@ -1229,16 +1322,23 @@ checkGlobbedRegex _ _ = return ()
prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]"
prop_checkConstantIfs2 = verify checkConstantIfs "[[ n -le 4 ]]"
prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n -ge 2 ]]"
prop_checkConstantIfs2a= verify checkConstantIfs "[ n -le 4 ]"
prop_checkConstantIfs2b= verifyNot checkConstantIfs "[[ n -le 4 ]]"
prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n != 2 ]]"
prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]"
prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"
checkConstantIfs _ (TC_Binary id typ op lhs rhs)
| op `elem` [ "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "="] =
when (isJust lLit && isJust rLit) $ warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
where
lLit = getLiteralString lhs
rLit = getLiteralString rhs
prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]"
prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"
checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =
when (isJust lLit && isJust rLit) $
warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
where
lLit = getLiteralString lhs
rLit = getLiteralString rhs
isDynamic =
op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]
&& typ == DoubleBracket
|| op `elem` [ "-nt", "-ot", "-ef"]
checkConstantIfs _ _ = return ()
prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]"
@@ -1249,13 +1349,13 @@ prop_checkLiteralBreakingTest5 = verify checkLiteralBreakingTest "[ -n \"$(true)
prop_checkLiteralBreakingTest6 = verify checkLiteralBreakingTest "[ -z $(true)z ]"
prop_checkLiteralBreakingTest7 = verifyNot checkLiteralBreakingTest "[ -z $(true) ]"
prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(true) ]"
prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]"
checkLiteralBreakingTest _ t = potentially $
case t of
(TC_Noary _ _ w@(T_NormalWord _ l)) -> do
guard . not $ isConstant w
guard . not $ isConstant w -- Covered by SC2078
comparisonWarning l `mplus` tautologyWarning w "Argument to implicit -n is always true due to literal strings."
(TC_Unary _ _ op w@(T_NormalWord _ l)) -> do
guard $ not $ isConstant w
(TC_Unary _ _ op w@(T_NormalWord _ l)) ->
case op of
"-n" -> tautologyWarning w "Argument to -n is always true due to literal strings."
"-z" -> tautologyWarning w "Argument to -z is always false due to literal strings."
@@ -1312,7 +1412,8 @@ checkBraceExpansionVars _ (T_BraceExpansion id list) = mapM_ check list
checkBraceExpansionVars _ _ = return ()
prop_checkForDecimals = verify checkForDecimals "((3.14*c))"
checkForDecimals _ t@(TA_Expansion id _) = potentially $ do
checkForDecimals params t@(TA_Expansion id _) = potentially $ do
guard $ not (hasFloatingPoint params)
str <- getLiteralString t
first <- str !!! 0
guard $ isDigit first && '.' `elem` str
@@ -1562,17 +1663,21 @@ checkPrintfVar _ = checkUnqualifiedCommand "printf" (const f) where
-- Check whether a word is entirely output from a single command
tokenIsJustCommandOutput t = case t of
T_NormalWord id [T_DollarExpansion _ _] -> True
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ _]] -> True
T_NormalWord id [T_Backticked _ _] -> True
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ _]] -> True
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
T_NormalWord id [T_Backticked _ cmds] -> check cmds
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
_ -> False
where
check [x] = not $ isOnlyRedirection x
check _ = False
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
checkUuoeCmd _ = checkUnqualifiedCommand "echo" (const f) where
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
@@ -1586,6 +1691,7 @@ prop_checkUuoeVar5 = verify checkUuoeVar "foo \"$(echo \"$(date) value:\" $value
prop_checkUuoeVar6 = verifyNot checkUuoeVar "foo \"$(echo files: *.png)\""
prop_checkUuoeVar7 = verifyNot checkUuoeVar "foo $(echo $(bar))" -- covered by 2005
prop_checkUuoeVar8 = verifyNot checkUuoeVar "#!/bin/sh\nz=$(echo)"
prop_checkUuoeVar9 = verify checkUuoeVar "foo $(echo $(<file))"
checkUuoeVar _ p =
case p of
T_Backticked id [cmd] -> check id cmd
@@ -1723,8 +1829,22 @@ checkTimeParameters _ = checkUnqualifiedCommand "time" f where
prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"
prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1"
prop_checkTestRedirects3 = verify checkTestRedirects "/usr/bin/test $var > $foo"
checkTestRedirects _ (T_Redirecting id redirs@(redir:_) cmd) | cmd `isCommand` "test" =
warn (getId redir) 2065 "This is interpretted as a shell file redirection, not a comparison."
prop_checkTestRedirects4 = verifyNot checkTestRedirects "test 1 -eq 2 2> file"
checkTestRedirects _ (T_Redirecting id redirs cmd) | cmd `isCommand` "test" =
mapM_ check redirs
where
check t =
when (suspicious t) $
warn (getId t) 2065 "This is interpreted as a shell file redirection, not a comparison."
suspicious t = -- Ignore redirections of stderr because these are valid for squashing e.g. int errors,
case t of -- and >> and similar redirections because these are probably not comparisons.
T_FdRedirect _ fd (T_IoFile _ op _) -> fd /= "2" && isComparison op
otherwise -> False
isComparison t =
case t of
T_Greater _ -> True
T_Less _ -> True
otherwise -> False
checkTestRedirects _ _ = return ()
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file"
@@ -1808,7 +1928,8 @@ checkPS1Assignments _ _ = return ()
prop_checkBackticks1 = verify checkBackticks "echo `foo`"
prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
checkBackticks _ (T_Backticked id _) =
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
checkBackticks _ (T_Backticked id list) | not (null list) =
style id 2006 "Use $(..) instead of legacy `..`."
checkBackticks _ _ = return ()
@@ -2096,6 +2217,7 @@ leadType shell parents t =
lastCreatesSubshell =
case shell of
Bash -> True
Dash -> True
Sh -> True
Ksh -> False
@@ -2144,7 +2266,7 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
"export" -> if "f" `elem` flags
then []
else concatMap getReference rest
"declare" -> if "x" `elem` flags
"declare" -> if any (`elem` flags) ["x", "p"]
then concatMap getReference rest
else []
"readonly" -> concatMap getReference rest
@@ -2305,6 +2427,10 @@ getReferencedVariables t =
TC_Unary id _ "-v" token -> getIfReference t token
TC_Unary id _ "-R" token -> getIfReference t token
TC_Binary id DoubleBracket op lhs rhs ->
if isDereferencing op
then concatMap (getIfReference t) [lhs, rhs]
else []
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
@@ -2330,6 +2456,8 @@ getReferencedVariables t =
when (isDigit $ head str) $ fail "is a number"
return (context, token, getBracedReference str)
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
-- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 =
@@ -2445,6 +2573,8 @@ prop_checkSpacefulness27= verifyNotTree checkSpacefulness "echo ${a:+'foo'}"
prop_checkSpacefulness28= verifyNotTree checkSpacefulness "exec {n}>&1; echo $n"
prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>&-;"
prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;"
prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\""
prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
checkSpacefulness params t =
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
@@ -2461,7 +2591,7 @@ checkSpacefulness params t =
readF _ token name = do
spaced <- hasSpaces name
return [makeComment InfoC (getId token) 2086 warning |
spaced
isExpansion token && spaced
&& not (isArrayExpansion token) -- There's another warning for this
&& not (isCounting token)
&& not (isQuoteFree parents token)
@@ -2483,6 +2613,11 @@ checkSpacefulness params t =
parents = parentMap params
isExpansion t =
case t of
(T_DollarBraced _ _ ) -> True
_ -> False
isCounting (T_DollarBraced id token) =
case concat $ oversimplify token of
'#':_ -> True
@@ -2525,6 +2660,7 @@ prop_checkQuotesInLiterals6 = verifyTree checkQuotesInLiterals "param='my\\ file
prop_checkQuotesInLiterals6a= verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd"
prop_checkQuotesInLiterals7 = verifyTree checkQuotesInLiterals "param='my\\ file'; rm $param"
prop_checkQuotesInLiterals8 = verifyTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm $param"
prop_checkQuotesInLiterals9 = verifyNotTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm ${#param}"
checkQuotesInLiterals params t =
doVariableFlowAnalysis readF writeF Map.empty (variableFlow params)
where
@@ -2556,12 +2692,18 @@ checkQuotesInLiterals params t =
then return $ getId t
else Nothing
squashesQuotes t =
case t of
T_DollarBraced id _ -> "#" `isPrefixOf` bracedString t
otherwise -> False
readF _ expr name = do
assignment <- getQuotes name
return
(if isJust assignment
&& not (isParamTo parents "eval" expr)
&& not (isQuoteFree parents expr)
&& not (squashesQuotes expr)
then [
makeComment WarningC (fromJust assignment) 2089
"Quotes/backslashes will be treated literally. Use an array.",
@@ -2643,6 +2785,9 @@ prop_checkUnused23= verifyNotTree checkUnusedAssignments "a=1; [ -R a ]"
prop_checkUnused24= verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}"
prop_checkUnused25= verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}"
prop_checkUnused26= verifyNotTree checkUnusedAssignments "declare -F foo"
prop_checkUnused27= verifyTree checkUnusedAssignments "var=3; [ var -eq 3 ]"
prop_checkUnused28= verifyNotTree checkUnusedAssignments "var=3; [[ var -eq 3 ]]"
prop_checkUnused29= verifyNotTree checkUnusedAssignments "var=(a b); declare -p var"
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
where
flow = variableFlow params
@@ -2753,7 +2898,7 @@ checkUnassignedReferences params t = warnings
isGuarded _ = False
match var candidate =
if var /= candidate && (map toLower var) == (map toLower candidate)
if var /= candidate && map toLower var == map toLower candidate
then 1
else dist var candidate
@@ -2765,9 +2910,7 @@ checkGlobsAsOptions _ (T_SimpleCommand _ _ args) =
mapM_ check $ takeWhile (not . isEndOfArgs) args
where
check v@(T_NormalWord _ (T_Glob id s:_)) | s == "*" || s == "?" =
info id 2035 $
"Use ./" ++ concat (oversimplify v)
++ " so names with dashes won't become options."
info id 2035 "Use ./*glob* or -- *glob* so names with dashes won't become options."
check _ = return ()
isEndOfArgs t =
@@ -2923,11 +3066,18 @@ checkLoopKeywordScope params t |
subshellType t = case leadType (shellType params) (parentMap params) t of
NoneScope -> Nothing
SubshellScope str -> return str
isFunction t = case t of T_Function {} -> True; _ -> False
relevant t = isLoop t || isFunction t || isJust (subshellType t)
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_checkFunctionDeclarations2 = verify checkFunctionDeclarations "#!/bin/dash\nfunction foo { lol; }"
prop_checkFunctionDeclarations3 = verifyNot checkFunctionDeclarations "foo() { echo bar; }"
@@ -2938,7 +3088,11 @@ checkFunctionDeclarations params
Ksh ->
when (hasKeyword && hasParens) $
err id 2111 "ksh does not allow 'function' keyword and '()' at the same time."
Sh -> do
Dash -> forSh
Sh -> forSh
where
forSh = do
when (hasKeyword && hasParens) $
warn id 2112 "'function' keyword is non-standard. Delete it."
when (hasKeyword && not hasParens) $
@@ -3417,6 +3571,8 @@ prop_checkUncheckedCd3 = verifyNotTree checkUncheckedCd "set -e; cd ~/src; rm -r
prop_checkUncheckedCd4 = verifyNotTree checkUncheckedCd "if cd foo; then rm foo; fi"
prop_checkUncheckedCd5 = verifyTree checkUncheckedCd "if true; then cd foo; fi"
prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCd "cd .."
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCd "#!/bin/bash -e\ncd foo\nrm bar"
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCd "set -o errexit; cd foo; rm bar"
checkUncheckedCd params root =
if hasSetE then [] else execWriter $ doAnalysis checkElement root
where
@@ -3430,9 +3586,12 @@ checkUncheckedCd params root =
hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root
isSetE t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" && "e" `elem` map snd (getAllFlags t)
T_Script _ str _ -> str `matches` re
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t || "e" `elem` map snd (getAllFlags t))
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"
prop_checkLoopVariableReassignment1 = verify checkLoopVariableReassignment "for i in *; do for i in *.bar; do true; done; done"
prop_checkLoopVariableReassignment2 = verify checkLoopVariableReassignment "for i in *; do for((i=0; i<3; i++)); do true; done; done"
@@ -3448,7 +3607,7 @@ checkLoopVariableReassignment params token =
next <- listToMaybe $ filter (\x -> loopVariable x == Just str) path
return $ do
warn (getId token) 2165 "This nested loop overrides the index variable of its parent."
warn (getId next) 2165 "This parent loop has its index variable overridden."
warn (getId next) 2167 "This parent loop has its index variable overridden."
path = drop 1 $ getPath (parentMap params) token
loopVariable :: Token -> Maybe String
loopVariable t =
@@ -3461,5 +3620,90 @@ checkLoopVariableReassignment params token =
_ _ _ -> return var
_ -> fail "not loop"
prop_checkTrailingBracket1 = verify checkTrailingBracket "if -z n ]]; then true; fi "
prop_checkTrailingBracket2 = verifyNot checkTrailingBracket "if [[ -z n ]]; then true; fi "
prop_checkTrailingBracket3 = verify checkTrailingBracket "a || b ] && thing"
prop_checkTrailingBracket4 = verifyNot checkTrailingBracket "run [ foo ]"
prop_checkTrailingBracket5 = verifyNot checkTrailingBracket "run bar ']'"
checkTrailingBracket _ token =
case token of
T_SimpleCommand _ _ tokens@(_:_) -> check (last tokens) token
otherwise -> return ()
where
check t command =
case t of
T_NormalWord id [T_Literal _ str] -> potentially $ do
guard $ str `elem` [ "]]", "]" ]
let opposite = invert str
parameters = oversimplify command
guard $ opposite `notElem` parameters
return $ warn id 2171 $
"Found trailing " ++ str ++ " outside test. Missing " ++ opposite ++ "?"
otherwise -> return ()
invert s =
case s of
"]]" -> "[["
"]" -> "["
x -> x
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
checkNonportableSignals _ = checkUnqualifiedCommand "trap" (const f)
where
f = mapM_ check
check param = potentially $ do
str <- getLiteralString param
let id = getId param
return $ sequence_ $ mapMaybe (\f -> f id str) [
checkNumeric,
checkUntrappable
]
checkNumeric id str = do
guard $ not (null str)
guard $ all isDigit str
guard $ str /= "0" -- POSIX exit trap
guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
return $ warn id 2172
"Trapping signals by number is not well defined. Prefer signal names."
checkUntrappable id str = do
guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
return $ err id 2173
"SIGKILL/SIGSTOP can not be trapped."
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
checkMkdirDashPM _ t@(T_SimpleCommand _ _ args) = potentially $ do
name <- getCommandName t
guard $ name == "mkdir"
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
guard $ any couldHaveSubdirs (drop 1 args) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
where
flags = getAllFlags t
couldHaveSubdirs t = fromMaybe True $ do
name <- getLiteralString t
return $ '/' `elem` name
checkMkdirDashPM _ _ = return ()
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -147,6 +147,9 @@ prop_canSourceBadSyntax =
prop_cantSourceDynamic =
[1090] == checkWithIncludes [("lib", "")] ". \"$1\""
prop_cantSourceDynamic2 =
[1090] == checkWithIncludes [("lib", "")] "source ~/foo"
prop_canSourceDynamicWhenRedirected =
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""

View File

@@ -25,13 +25,14 @@ internalVariables = [
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
"LC_MESSAGES", "LC_NUMERIC", "LINES", "MAIL", "MAILCHECK", "MAILPATH",
"OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND",
"PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT",
"TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
-- Other
"USER", "TZ", "TERM"
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
]
variablesWithoutSpaces = [
@@ -41,6 +42,12 @@ variablesWithoutSpaces = [
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
]
arrayVariables = [
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"
]
commonCommands = [
"admin", "alias", "ar", "asa", "at", "awk", "basename", "batch",
"bc", "bg", "break", "c99", "cal", "cat", "cd", "cflow", "chgrp",
@@ -76,13 +83,12 @@ sampleWords = [
]
shellForExecutable :: String -> Maybe Shell
shellForExecutable "sh" = return Sh
shellForExecutable "ash" = return Sh
shellForExecutable "dash" = return Sh
shellForExecutable "ksh" = return Ksh
shellForExecutable "ksh88" = return Ksh
shellForExecutable "ksh93" = return Ksh
shellForExecutable "bash" = return Bash
shellForExecutable _ = Nothing
shellForExecutable name =
case name of
"sh" -> return Sh
"bash" -> return Bash
"dash" -> return Dash
"ksh" -> return Ksh
"ksh88" -> return Ksh
"ksh93" -> return Ksh
otherwise -> Nothing

View File

@@ -27,15 +27,15 @@ import GHC.Exts
import System.Info
import System.IO
format :: IO Formatter
format = return Formatter {
format :: FormatterOptions -> IO Formatter
format options = return Formatter {
header = return (),
footer = return (),
onFailure = outputError,
onResult = outputResult
onFailure = outputError options,
onResult = outputResult options
}
colorForLevel level =
colorForLevel level =
case level of
"error" -> 31 -- red
"warning" -> 33 -- yellow
@@ -44,13 +44,13 @@ colorForLevel level =
"message" -> 1 -- bold
"source" -> 0 -- none
otherwise -> 0 -- none
outputError file error = do
color <- getColorFunc
outputError options file error = do
color <- getColorFunc $ foColorOption options
hPutStrLn stderr $ color "error" $ file ++ ": " ++ error
outputResult result contents = do
color <- getColorFunc
outputResult options result contents = do
color <- getColorFunc $ foColorOption options
let comments = crComments result
let fileLines = lines contents
let lineCount = fromIntegral $ length fileLines
@@ -75,10 +75,15 @@ cuteIndent comment =
code code = "SC" ++ show code
getColorFunc = do
getColorFunc colorOption = do
term <- hIsTerminalDevice stdout
let windows = "mingw" `isPrefixOf` os
return $ if term && not windows then colorComment else const id
let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return $ if useColor then colorComment else const id
where
colorComment level comment =
ansi (colorForLevel level) ++ comment ++ clear

View File

@@ -72,8 +72,15 @@ data AnalysisResult = AnalysisResult {
arComments :: [TokenComment]
}
-- Formatter options
data FormatterOptions = FormatterOptions {
foColorOption :: ColorOption
}
-- Supporting data types
data Shell = Ksh | Sh | Bash deriving (Show, Eq)
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String
@@ -90,6 +97,12 @@ data Comment = Comment Severity Code String deriving (Show, Eq)
data PositionedComment = PositionedComment Position Comment deriving (Show, Eq)
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
data ColorOption =
ColorAuto
| ColorAlways
| ColorNever
deriving (Ord, Eq, Show)
-- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface {

View File

@@ -359,19 +359,36 @@ readConditionContents single =
readCondBinaryOp = try $ do
optional guardArithmetic
id <- getNextId
op <- choice (map tryOp ["==", "!=", "<=", ">=", "=~", ">", "<", "=", "\\<=", "\\>=", "\\<", "\\>"]) <|> otherOp
op <- getOp
spacingOrLf
return op
where
tryOp s = try $ do
id <- getNextId
string s
return $ TC_Binary id typ s
otherOp = try $ do
flaglessOps = [ "==", "!=", "<=", ">=", "=~", ">", "<", "=" ]
getOp = do
id <- getNextId
op <- anyQuotedOp <|> anyEscapedOp <|> anyOp
return $ TC_Binary id typ op
-- hacks to read quoted operators without having to read a shell word
anyEscapedOp = try $ do
char '\\'
escaped <$> anyOp
anyQuotedOp = try $ do
c <- oneOf "'\""
s <- anyOp
char c
return s
anyOp = flagOp <|> flaglessOp <|> fail
"Expected comparison operator (don't wrap commands in []/[[]])"
flagOp = try $ do
s <- readOp
when (s == "-a" || s == "-o") $ fail "Unexpected operator"
return $ TC_Binary id typ s
return s
flaglessOp =
choice $ map (try . string) flaglessOps
escaped s = if any (`elem` s) "<>" then '\\':s else s
guardArithmetic = do
try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ")
@@ -394,10 +411,17 @@ readConditionContents single =
return $ TC_Unary id typ s
readOp = try $ do
char '-'
s <- many1 letter
char '-' <|> weirdDash
s <- many1 letter <|> fail "Expected a test operator"
return ('-':s)
weirdDash = do
pos <- getPosition
oneOf "\x058A\x05BE\x2010\x2011\x2012\x2013\x2014\x2015\xFE63\xFF0D"
parseProblemAt pos ErrorC 1100
"This is a unicode dash. Delete and retype as ASCII minus."
return '-'
readCondWord = do
notFollowedBy2 (try (spacing >> string "]"))
x <- readNormalWord
@@ -429,6 +453,7 @@ readConditionContents single =
return $ TC_Or id typ x
readAndOrOp op requiresSpacing = do
optional $ lookAhead weirdDash
x <- string op
condSpacing requiresSpacing
return x
@@ -598,7 +623,7 @@ readArithmeticContents =
readDoubleQuoted,
readNormalDollar,
readBraced,
readBackTicked,
readUnquotedBackTicked,
readNormalLiteral "+-*/=%^,]?:"
]
spacing
@@ -706,6 +731,10 @@ prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"
prop_readCondition11= isOk readCondition "[[ a == b ||\n c == d ]]"
prop_readCondition12= isWarning readCondition "[ a == b \n -o c == d ]"
prop_readCondition13= isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
prop_readCondition14= isOk readCondition "[ foo '>' bar ]"
prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]"
prop_readCondition16= isOk readCondition "[ foo \\< bar ]"
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
readCondition = called "test expression" $ do
opos <- getPosition
id <- getNextId
@@ -725,7 +754,7 @@ readCondition = called "test expression" $ do
condition <- readConditionContents single
cpos <- getPosition
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here"
close <- try (string "]]") <|> string "]" <|> fail "Expected test to end here (don't wrap commands in []/[[]])"
when (open == "[[" && close /= "]]") $ parseProblemAt cpos ErrorC 1033 "Did you mean ]] ?"
when (open == "[" && close /= "]" ) $ parseProblemAt opos ErrorC 1034 "Did you mean [[ ?"
spacing
@@ -805,7 +834,7 @@ readNormalWordPart end = do
readGlob,
readNormalDollar,
readBraced,
readBackTicked,
readUnquotedBackTicked,
readProcSub,
readNormalLiteral end,
readLiteralCurlyBraces
@@ -841,7 +870,7 @@ readDollarBracedWord = do
list <- many readDollarBracedPart
return $ T_NormalWord id list
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readBackTicked <|> readDollarBracedLiteral
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readUnquotedBackTicked <|> readDollarBracedLiteral
readDollarBracedLiteral = do
id <- getNextId
@@ -899,15 +928,18 @@ readSingleQuotedPart =
readSingleEscaped
<|> many1 (noneOf "'\\\x2018\x2019")
prop_readBackTicked = isOk readBackTicked "`ls *.mp3`"
prop_readBackTicked2 = isOk readBackTicked "`grep \"\\\"\"`"
prop_readBackTicked3 = isWarning readBackTicked "´grep \"\\\"\"´"
prop_readBackTicked4 = isOk readBackTicked "`echo foo\necho bar`"
prop_readBackTicked = isOk (readBackTicked False) "`ls *.mp3`"
prop_readBackTicked2 = isOk (readBackTicked False) "`grep \"\\\"\"`"
prop_readBackTicked3 = isWarning (readBackTicked False) "´grep \"\\\"\"´"
prop_readBackTicked4 = isOk readSimpleCommand "`echo foo\necho bar`"
prop_readBackTicked5 = isOk readSimpleCommand "echo `foo`bar"
prop_readBackTicked6 = isWarning readSimpleCommand "echo `foo\necho `bar"
prop_readBackTicked7 = isOk readSimpleCommand "`#inline comment`"
prop_readBackTicked8 = isOk readSimpleCommand "echo `#comment` \\\nbar baz"
readBackTicked = called "backtick expansion" $ do
readQuotedBackTicked = readBackTicked True
readUnquotedBackTicked = readBackTicked False
readBackTicked quoted = called "backtick expansion" $ do
id <- getNextId
startPos <- getPosition
backtick
@@ -926,6 +958,7 @@ readBackTicked = called "backtick expansion" $ do
return $ T_Backticked id result
where
unEscape [] = []
unEscape ('\\':'"':rest) | quoted = '"' : unEscape rest
unEscape ('\\':x:rest) | x `elem` "$`\\" = x : unEscape rest
unEscape ('\\':'\n':rest) = unEscape rest
unEscape (c:rest) = c : unEscape rest
@@ -993,7 +1026,7 @@ suggestForgotClosingQuote startPos endPos name = do
parseProblemAt endPos InfoC 1079
"This is actually an end quote, but due to next char it looks suspect."
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readBackTicked
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBackTicked
readDoubleQuotedLiteral = do
doubleQuote
@@ -1020,6 +1053,9 @@ prop_readGlob2 = isOk readGlob "[^0-9]"
prop_readGlob3 = isOk readGlob "[a[:alpha:]]"
prop_readGlob4 = isOk readGlob "[[:alnum:]]"
prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"
prop_readGlob6 = isOk readGlob "[\\|]"
prop_readGlob7 = isOk readGlob "[^[]"
prop_readGlob8 = isOk readGlob "[*?]"
readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
where
readSimple = do
@@ -1030,11 +1066,11 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
readClass = try $ do
id <- getNextId
char '['
s <- many1 (predefined <|> liftM return (letter <|> digit <|> oneOf globchars))
s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars)
char ']'
return $ T_Glob id $ "[" ++ concat s ++ "]"
where
globchars = "^-_:?*.,!~@#$%=+{}/~"
globchars = liftM return . oneOf $ "!$[" ++ extglobStartChars
predefined = do
try $ string "[:"
s <- many1 letter
@@ -1152,12 +1188,19 @@ prop_readBraced3 = isOk readBraced "{1,\\},2}"
prop_readBraced4 = isOk readBraced "{1,{2,3}}"
prop_readBraced5 = isOk readBraced "{JP{,E}G,jp{,e}g}"
prop_readBraced6 = isOk readBraced "{foo,bar,$((${var}))}"
prop_readBraced7 = isNotOk readBraced "{}"
prop_readBraced8 = isNotOk readBraced "{foo}"
readBraced = try braceExpansion
where
braceExpansion =
T_BraceExpansion `withParser` do
char '{'
elements <- bracedElement `sepBy1` char ','
guard $
case elements of
(_:_:_) -> True
[t] -> ".." `isInfixOf` onlyLiteralString t
[] -> False
char '}'
return elements
bracedElement =
@@ -1529,7 +1572,11 @@ readSimpleCommand = called "simple command" $ do
readSource :: Monad m => SourcePos -> Token -> SCParser m Token
readSource pos t@(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:file:_))) = do
override <- getSourceOverride
let literalFile = override `mplus` getLiteralString file
let literalFile = do
name <- override `mplus` getLiteralString file
-- Hack to avoid 'source ~/foo' trying to read from literal tilde
guard . not $ "~/" `isPrefixOf` name
return name
case literalFile of
Nothing -> do
parseNoteAt pos WarningC 1090
@@ -1933,6 +1980,9 @@ prop_readFunctionDefinition5 = isOk readFunctionDefinition ":(){ :|:;}"
prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"
prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }"
prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}"
readFunctionDefinition = called "function" $ do
functionSignature <- try readFunctionSignature
allspacing
@@ -1950,8 +2000,11 @@ readFunctionDefinition = called "function" $ do
whitespace
spacing
name <- readFunctionName
spacing
spaces <- spacing
hasParens <- wasIncluded readParens
when (not hasParens && null spaces) $
acceptButWarn (lookAhead (oneOf "{("))
ErrorC 1095 "You need a space or linefeed between the function name and body."
return $ T_Function id (FunctionKeyword True) (FunctionParentheses hasParens) name
readWithoutFunction = try $ do
@@ -2064,6 +2117,8 @@ prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
prop_readAssignmentWord10= isWarning readAssignmentWord "foo$n=42"
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
@@ -2082,7 +2137,7 @@ readAssignmentWord = try $ do
isEndOfCommand <- liftM isJust $ optionMaybe (try . lookAhead $ (disregard (oneOf "\r\n;&|)") <|> eof))
if not hasLeftSpace && (hasRightSpace || isEndOfCommand)
then do
when (variable /= "IFS" && hasRightSpace) $
when (variable /= "IFS" && hasRightSpace && not isEndOfCommand) $
parseNoteAt pos WarningC 1007
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
value <- readEmptyLiteral
@@ -2334,6 +2389,7 @@ readScript = do
isWarning p s = parsesCleanly p s == Just False
isOk p s = parsesCleanly p s == Just True
isNotOk p s = parsesCleanly p s == Nothing
testParse string = runIdentity $ do
(res, _) <- runParser (mockedSystemInterface []) readScript "-" string

BIN
doc/emacs-flycheck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
doc/terminal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
doc/vim-syntastic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

5
quickrun Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
# quickrun runs ShellCheck in an interpreted mode.
# This allows testing changes without recompiling.
runghc -idist/build/autogen shellcheck.hs "$@"

15
quicktest Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# quicktest runs the ShellCheck unit tests in an interpreted mode.
# This allows running tests without compiling, which can be faster.
# 'cabal test' remains the source of truth.
(
var=$(echo 'liftM and $ sequence [ShellCheck.Analytics.runTests, ShellCheck.Parser.runTests, ShellCheck.Checker.runTests]' | cabal repl | tee /dev/stderr)
if [[ $var == *$'\nTrue'* ]]
then
exit 0
else
grep -C 3 "Fail" <<< "$var"
exit 1
fi
) 2>&1

View File

@@ -16,7 +16,7 @@ errors and pitfalls where the shell just gives a cryptic error message or
strange behavior, but it also reports on a few more advanced issues where
corner cases can cause delayed failures.
ShellCheck gives shell specific advice. Consider the line:
ShellCheck gives shell specific advice. Consider this line:
(( area = 3.14*r*r ))
@@ -32,6 +32,12 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS
**-C**[*WHEN*],\ **--color**[=*WHEN*]
: For TTY outut, enable colors *always*, *never* or *auto*. The default
is *auto*. **--color** without an argument is equivalent to
**--color=always**.
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
: Explicitly exclude the specified codes from the report. Subsequent **-e**
@@ -46,7 +52,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash* and *ksh*.
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
The default is to use the file's shebang, or *bash* if the target shell
can't be determined.
@@ -54,7 +60,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
: Print version information and exit.
**-x**,\ **-external-sources**
**-x**,\ **--external-sources**
: Follow 'source' statements even when the file is not specified as input.
By default, `shellcheck` will only follow files specified on the command
@@ -159,6 +165,16 @@ The environment variable `SHELLCHECK_OPTS` can be set with default flags:
Its value will be split on spaces and prepended to the command line on each
invocation.
# RETURN VALUES
ShellCheck uses the follow exit codes:
+ 0: All files successfully scanned with no issues.
+ 1: All files successfully scanned with some issues.
+ 2: Some files could not be processed (e.g. file not found).
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
# AUTHOR
ShellCheck is written and maintained by Vidar Holen.

View File

@@ -48,7 +48,6 @@ data Flag = Flag String String
data Status =
NoProblems
| SomeProblems
| BadInput
| SupportFailure
| SyntaxFailure
| RuntimeException
@@ -60,12 +59,16 @@ instance Monoid Status where
data Options = Options {
checkSpec :: CheckSpec,
externalSources :: Bool
externalSources :: Bool,
formatterOptions :: FormatterOptions
}
defaultOptions = Options {
checkSpec = emptyCheckSpec,
externalSources = False
externalSources = False,
formatterOptions = FormatterOptions {
foColorOption = ColorAuto
}
}
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
@@ -74,8 +77,11 @@ options = [
(ReqArg (Flag "exclude") "CODE1,CODE2..") "exclude types of warnings",
Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") "output format",
Option "C" ["color"]
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
"Use color (auto, always, never)",
Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (bash,sh,ksh)",
(ReqArg (Flag "shell") "SHELLNAME") "Specify dialect (sh,bash,dash,ksh)",
Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES.",
Option "V" ["version"]
@@ -92,12 +98,12 @@ parseArguments argv =
printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options
throwError SyntaxFailure
formats :: Map.Map String (IO Formatter)
formats = Map.fromList [
formats :: FormatterOptions -> Map.Map String (IO Formatter)
formats options = Map.fromList [
("checkstyle", ShellCheck.Formatter.CheckStyle.format),
("gcc", ShellCheck.Formatter.GCC.format),
("json", ShellCheck.Formatter.JSON.format),
("tty", ShellCheck.Formatter.TTY.format)
("tty", ShellCheck.Formatter.TTY.format options)
]
getOption [] _ = Nothing
@@ -144,7 +150,6 @@ statusToCode status =
case status of
NoProblems -> ExitSuccess
SomeProblems -> ExitFailure 1
BadInput -> ExitFailure 5
SyntaxFailure -> ExitFailure 3
SupportFailure -> ExitFailure 4
RuntimeException -> ExitFailure 2
@@ -154,12 +159,13 @@ process flags files = do
options <- foldM (flip parseOption) defaultOptions flags
verifyFiles files
let format = fromMaybe "tty" $ getOption flags "format"
let formatters = formats $ formatterOptions options
formatter <-
case Map.lookup format formats of
case Map.lookup format formatters of
Nothing -> do
printErr $ "Unknown format " ++ format
printErr "Supported formats:"
mapM_ (printErr . write) $ Map.keys formats
mapM_ (printErr . write) $ Map.keys formatters
throwError SupportFailure
where write s = " " ++ s
Just f -> ExceptT $ fmap Right f
@@ -197,6 +203,13 @@ runFormatter sys format options files = do
then NoProblems
else SomeProblems
parseColorOption colorOption =
case colorOption of
"auto" -> ColorAuto
"always" -> ColorAlways
"never" -> ColorNever
_ -> error $ "Bad value for --color `" ++ colorOption ++ "'"
parseOption flag options =
case flag of
Flag "shell" str ->
@@ -221,11 +234,18 @@ parseOption flag options =
liftIO printVersion
throwError NoProblems
Flag "externals" _ -> do
Flag "externals" _ ->
return options {
externalSources = True
}
Flag "color" color ->
return options {
formatterOptions = (formatterOptions options) {
foColorOption = parseColorOption color
}
}
_ -> return options
where
die s = do