mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a21df2d88f | ||
|
d473fb8867 | ||
|
f754363733 | ||
|
ef1f8f535e | ||
|
f9909504dd | ||
|
fa4cefda9d | ||
|
f2f6c66902 | ||
|
1f4dd85548 | ||
|
528381796e | ||
|
ad7ad28246 | ||
|
33ab998b02 | ||
|
9c28237d52 | ||
|
e0e5ba3a90 | ||
|
b4390414ef | ||
|
8acd5b13cd | ||
|
d00ca0c283 | ||
|
8bc98d89a7 | ||
|
c7964a7a78 | ||
|
8ec87d6655 | ||
|
c3df2bf761 | ||
|
d1df3713ca | ||
|
23496e93b0 | ||
|
437e69fbba | ||
|
63ad3f99ad | ||
|
0044c3dd6e | ||
|
a3d4101d6c | ||
|
bd359c5c0f | ||
|
498de63337 | ||
|
52ab7dee2d | ||
|
1a5296659b | ||
|
a66ee2967c | ||
|
d985380f48 | ||
|
6739c4a729 | ||
|
7415c9dcb7 | ||
|
d3fc1f355d | ||
|
48fd793581 | ||
|
e5842e2e2b | ||
|
cf445c7d20 | ||
|
ffb9578a98 | ||
|
630f20e888 | ||
|
8f5f91f041 | ||
|
8d9d4533c3 | ||
|
a4b4954a23 | ||
|
38cea9201d | ||
|
4ce916ec1d | ||
|
b9cb040128 | ||
|
2488be7298 | ||
|
d01b59a827 | ||
|
f77821625c | ||
|
1eece5b2ee | ||
|
58d45e3fa4 | ||
|
5aaa1a7d9a | ||
|
3b36c2c820 | ||
|
55692926b9 | ||
|
4172722167 | ||
|
485593da2c | ||
|
1181c6b3af | ||
|
ee181cfc43 | ||
|
c72667407b | ||
|
5467a0f1d9 | ||
|
3fc77d94ec | ||
|
23e0420cb1 | ||
|
a898165ac7 | ||
|
ba5e3db31a | ||
|
56145217fe |
270
README.md
270
README.md
@@ -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
|
||||
.
|
||||
|
||||
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):
|
||||
|
||||
.
|
||||
|
||||
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
|
||||
|
||||
.
|
||||
|
||||
* 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!
|
||||
|
15
Setup.hs
15
Setup.hs
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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 =
|
||||
|
@@ -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 }) ) |])
|
||||
|
@@ -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\""
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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
BIN
doc/emacs-flycheck.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
doc/terminal.png
Normal file
BIN
doc/terminal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
doc/vim-syntastic.png
Normal file
BIN
doc/vim-syntastic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
5
quickrun
Executable file
5
quickrun
Executable 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
15
quicktest
Executable 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
|
@@ -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.
|
||||
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user