mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
63 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
08d1d37094 | ||
|
e28e90133d | ||
|
2688a81526 | ||
|
82c3084438 | ||
|
c6ff1933b7 | ||
|
22c86256ac | ||
|
a3a4873190 | ||
|
750212af39 | ||
|
2154583fd3 | ||
|
35c74e4747 | ||
|
46fb91b44d | ||
|
128d5d6013 | ||
|
41176c23a6 | ||
|
342726f480 | ||
|
1863f2f12d | ||
|
8809a36952 | ||
|
7f307c5775 | ||
|
eb12086d05 | ||
|
6259a32601 | ||
|
4e13c7cbc1 | ||
|
edb01fa855 | ||
|
85e6c35845 | ||
|
43f667a8f9 | ||
|
daacc98a8f | ||
|
40907b1636 | ||
|
96168fc707 | ||
|
6aee12a572 | ||
|
0048f34b11 | ||
|
e679ff222a | ||
|
6f5648faca | ||
|
30e94ea7ab | ||
|
d8f8a2fa14 | ||
|
df3cc70658 | ||
|
5669702362 | ||
|
bd9d05c759 | ||
|
af87fe9315 | ||
|
c2850e436f | ||
|
d73fc8f91d | ||
|
7124c113e8 | ||
|
f594f01d35 | ||
|
6ccb7e9129 | ||
|
838f0ce4dc | ||
|
105b09792c | ||
|
f6618d4332 | ||
|
0a381be37b | ||
|
46e47dad45 | ||
|
fee6c94d40 | ||
|
cf1c46d852 | ||
|
a40efffec9 | ||
|
44b96fca66 | ||
|
b4fb439191 | ||
|
0897ab7092 | ||
|
9703f89f79 | ||
|
aadf02e635 | ||
|
090051bdc1 | ||
|
b8c96b4361 | ||
|
f26038125d | ||
|
08f7ff37c5 | ||
|
60fc33ebdf | ||
|
3a006f7bcb | ||
|
89b6fd58fa | ||
|
069ddeffcc | ||
|
59c4ed106c |
27
.github/ISSUE_TEMPLATE.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
#### For bugs
|
||||
- Rule Id (if any, e.g. SC1000):
|
||||
- My shellcheck version (`shellcheck --version` or "online"):
|
||||
- [ ] I tried on shellcheck.net and verified that this is still a problem on the latest commit
|
||||
- [ ] It's not reproducible on shellcheck.net, but I think that's because it's an OS, configuration or encoding issue
|
||||
|
||||
#### For new checks and feature suggestions
|
||||
- [ ] shellcheck.net (i.e. the latest commit) currently gives no useful warnings about this
|
||||
- [ ] I searched through https://github.com/koalaman/shellcheck/issues and didn't find anything related
|
||||
|
||||
|
||||
#### Here's a snippet or screenshot that shows the problem:
|
||||
|
||||
```sh
|
||||
|
||||
#!/your/interpreter
|
||||
your script here
|
||||
|
||||
```
|
||||
|
||||
#### Here's what shellcheck currently says:
|
||||
|
||||
|
||||
|
||||
#### Here's what I wanted or expected to see:
|
||||
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
before_install:
|
||||
- export DOCKER_REPO=koalaman/shellcheck
|
||||
- |-
|
||||
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || echo $TRAVIS_BRANCH)
|
||||
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || ([ -n "$TRAVIS_TAG" ] && echo "$TRAVIS_TAG") || echo "$TRAVIS_BRANCH")
|
||||
|
||||
script:
|
||||
- docker build -t builder -f Dockerfile_builder .
|
||||
@@ -18,4 +18,4 @@ script:
|
||||
after_success:
|
||||
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- |-
|
||||
[ "$TRAVIS_BRANCH" == "master" ] && docker push $DOCKER_REPO:$TAG
|
||||
([ "$TRAVIS_BRANCH" == "master" ] || [ -n "$TRAVIS_TAG" ]) && docker push "$DOCKER_REPO:$TAG"
|
||||
|
48
README.md
48
README.md
@@ -36,9 +36,9 @@ Run `shellcheck yourscript` in your terminal for instant output, as seen above.
|
||||
|
||||
You can see ShellCheck suggestions directly in a variety of editors.
|
||||
|
||||
* Vim, through [Syntastic](https://github.com/scrooloose/syntastic):
|
||||
* Vim, through [ALE](https://github.com/w0rp/ale) or [Syntastic](https://github.com/scrooloose/syntastic):
|
||||
|
||||
.
|
||||
.
|
||||
|
||||
* Emacs, through [Flycheck](https://github.com/flycheck/flycheck):
|
||||
|
||||
@@ -54,8 +54,21 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
||||
#### In your build or test suites
|
||||
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
|
||||
|
||||
Use ShellCheck's exit code, or its [CheckStyle compatible XML output](shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
|
||||
ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
|
||||
|
||||
## Travis CI Setup
|
||||
|
||||
If you want to use ShellCheck in Travis CI, setting it up is simple :tada:.
|
||||
|
||||
```yml
|
||||
language: bash
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- debian-sid # Grab ShellCheck from the Debian repo
|
||||
packages:
|
||||
- shellcheck
|
||||
```
|
||||
|
||||
## Installing
|
||||
|
||||
@@ -64,7 +77,7 @@ The easiest way to install ShellCheck locally is through your package manager.
|
||||
On systems with Cabal (installs to `~/.cabal/bin`):
|
||||
|
||||
cabal update
|
||||
cabal install shellcheck
|
||||
cabal install ShellCheck
|
||||
|
||||
On Debian based distros:
|
||||
|
||||
@@ -74,6 +87,11 @@ On Gentoo based distros:
|
||||
|
||||
emerge --ask shellcheck
|
||||
|
||||
On EPEL based distros:
|
||||
|
||||
yum -y install epel-release
|
||||
yum install ShellCheck
|
||||
|
||||
On Fedora based distros:
|
||||
|
||||
dnf install ShellCheck
|
||||
@@ -99,6 +117,9 @@ add OBS devel:languages:haskell repository from https://build.opensuse.org/proje
|
||||
|
||||
or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||
|
||||
From Docker Hub:
|
||||
|
||||
docker pull koalaman/shellcheck
|
||||
|
||||
## Compiling from source
|
||||
|
||||
@@ -107,7 +128,7 @@ This section describes how to build ShellCheck from a source directory. ShellChe
|
||||
|
||||
#### Installing Cabal
|
||||
|
||||
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `yum`, `zypper` or `brew`).
|
||||
ShellCheck is built and packaged using Cabal. Install the package `cabal-install` from your system's package manager (with e.g. `apt-get`, `brew`, `emerge`, `yum`, or `zypper`).
|
||||
|
||||
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/
|
||||
|
||||
@@ -183,6 +204,8 @@ ShellCheck can recognize many types of incorrect test statements.
|
||||
[ $1 -eq "shellcheck" ] # Numerical comparison of strings
|
||||
[ $n && $m ] # && in [ .. ]
|
||||
[ grep -q foo file ] # Command without $(..)
|
||||
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
|
||||
(( 1 -lt 2 )) # Using test operators in ((..))
|
||||
|
||||
|
||||
#### Frequently misused commands
|
||||
@@ -211,9 +234,11 @@ ShellCheck recognizes many common beginner's syntax errors:
|
||||
var$n="Hello" # Wrong indirect assignment
|
||||
echo ${var$n} # Wrong indirect reference
|
||||
var=(1, 2, 3) # Comma separated arrays
|
||||
array=( [index] = value ) # Incorrect index initialization
|
||||
echo "Argument 10 is $10" # Positional parameter misreference
|
||||
if $(myfunction); then ..; fi # Wrapping commands in $()
|
||||
else if othercondition; then .. # Using 'else if'
|
||||
|
||||
|
||||
|
||||
#### Style
|
||||
@@ -236,7 +261,8 @@ ShellCheck can recognize issues related to data and typing:
|
||||
|
||||
args="$@" # Assigning arrays to strings
|
||||
files=(foo bar); echo "$files" # Referencing arrays as strings
|
||||
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays.
|
||||
declare -A arr=(foo bar) # Associative arrays without index
|
||||
printf "%s\n" "Arguments: $@." # Concatenating strings and arrays
|
||||
[[ $# > 2 ]] # Comparing numbers as strings
|
||||
var=World; echo "Hello " var # Unused lowercase variables
|
||||
echo "Hello $name" # Unassigned lowercase variables
|
||||
@@ -269,6 +295,7 @@ ShellCheck will warn when using features not supported by the shebang. For examp
|
||||
foo-bar() { ..; } # Undefined/unsupported function name
|
||||
[ $UID = 0 ] # Variable undefined in dash/sh
|
||||
local var=value # local is undefined in sh
|
||||
time sleep 1 | sleep 5 # Undefined uses of 'time'
|
||||
|
||||
|
||||
#### Miscellaneous
|
||||
@@ -279,11 +306,13 @@ ShellCheck recognizes a menagerie of other issues:
|
||||
PATH="$PATH:~/bin" # Literal tilde in $PATH
|
||||
rm “file” # Unicode quotes
|
||||
echo "Hello world" # Carriage return / DOS line endings
|
||||
echo hello \ # Trailing spaces after \
|
||||
var=42 echo $var # Expansion of inlined environment
|
||||
#!/bin/bash -x -e # Common shebang errors
|
||||
echo $((n/180*100)) # Unnecessary loss of precision
|
||||
ls *[:digit:].txt # Bad character class globs
|
||||
sed 's/foo/bar/' file > file # Redirecting to input
|
||||
sed 's/foo/bar/' file > file # Redirecting to input
|
||||
|
||||
|
||||
|
||||
## Testimonials
|
||||
@@ -293,6 +322,11 @@ ShellCheck recognizes a menagerie of other issues:
|
||||
Alexander Tarasikov,
|
||||
[via Twitter](https://twitter.com/astarasikov/status/568825996532707330)
|
||||
|
||||
## Ignoring issues
|
||||
|
||||
Issues can be ignored via environmental variable, command line, individually or globally within a file:
|
||||
|
||||
https://github.com/koalaman/shellcheck/wiki/Ignore
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.4.5
|
||||
Version: 0.4.6
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
@@ -55,6 +55,7 @@ library
|
||||
ShellCheck.AnalyzerLib
|
||||
ShellCheck.Checker
|
||||
ShellCheck.Checks.Commands
|
||||
ShellCheck.Checks.ShellSupport
|
||||
ShellCheck.Data
|
||||
ShellCheck.Formatter.Format
|
||||
ShellCheck.Formatter.CheckStyle
|
||||
|
@@ -33,6 +33,7 @@ data FunctionKeyword = FunctionKeyword Bool deriving (Show, Eq)
|
||||
data FunctionParentheses = FunctionParentheses Bool deriving (Show, Eq)
|
||||
data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
||||
|
||||
data Root = Root Token
|
||||
data Token =
|
||||
TA_Binary Id String Token Token
|
||||
| TA_Assignment Id String Token Token
|
||||
@@ -44,7 +45,7 @@ data Token =
|
||||
| TC_And Id ConditionType String Token Token
|
||||
| TC_Binary Id ConditionType String Token Token
|
||||
| TC_Group Id ConditionType Token
|
||||
| TC_Noary Id ConditionType Token
|
||||
| TC_Nullary Id ConditionType Token
|
||||
| TC_Or Id ConditionType String Token Token
|
||||
| TC_Unary Id ConditionType String Token
|
||||
| T_AND_IF Id
|
||||
@@ -110,6 +111,7 @@ data Token =
|
||||
| T_NormalWord Id [Token]
|
||||
| T_OR_IF Id
|
||||
| T_OrIf Id (Token) (Token)
|
||||
| T_ParamSubSpecialChar Id String -- e.g. '%' in ${foo%bar} or '/' in ${foo/bar/baz}
|
||||
| T_Pipeline Id [Token] [Token] -- [Pipe separators] [Commands]
|
||||
| T_ProcSub Id String [Token]
|
||||
| T_Rbrace Id
|
||||
@@ -256,7 +258,7 @@ analyze f g i =
|
||||
delve (TC_Group id typ token) = d1 token $ TC_Group id typ
|
||||
delve (TC_Binary id typ op lhs rhs) = d2 lhs rhs $ TC_Binary id typ op
|
||||
delve (TC_Unary id typ op token) = d1 token $ TC_Unary id typ op
|
||||
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
|
||||
delve (TC_Nullary id typ token) = d1 token $ TC_Nullary id typ
|
||||
|
||||
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
|
||||
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
|
||||
@@ -318,6 +320,7 @@ getId t = case t of
|
||||
T_DollarBraced id _ -> id
|
||||
T_DollarArithmetic id _ -> id
|
||||
T_BraceExpansion id _ -> id
|
||||
T_ParamSubSpecialChar id _ -> id
|
||||
T_DollarBraceCommandExpansion id _ -> id
|
||||
T_IoFile id _ _ -> id
|
||||
T_IoDuplicate id _ _ -> id
|
||||
@@ -353,7 +356,7 @@ getId t = case t of
|
||||
TC_Group id _ _ -> id
|
||||
TC_Binary id _ _ _ _ -> id
|
||||
TC_Unary id _ _ _ -> id
|
||||
TC_Noary id _ _ -> id
|
||||
TC_Nullary id _ _ -> id
|
||||
TA_Binary id _ _ _ -> id
|
||||
TA_Assignment id _ _ _ -> id
|
||||
TA_Unary id _ _ -> id
|
||||
@@ -376,7 +379,7 @@ getId t = case t of
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
||||
doAnalysis f = analyze f blank (return . id)
|
||||
doStackAnalysis startToken endToken = analyze startToken endToken (return . id)
|
||||
doAnalysis f = analyze f blank return
|
||||
doStackAnalysis startToken endToken = analyze startToken endToken return
|
||||
doTransform i = runIdentity . analyze blank blank (return . i)
|
||||
|
||||
|
@@ -23,6 +23,7 @@ import ShellCheck.AST
|
||||
|
||||
import Control.Monad.Writer
|
||||
import Control.Monad
|
||||
import Data.Functor
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
|
||||
@@ -86,13 +87,14 @@ oversimplify token =
|
||||
(T_Glob _ s) -> [s]
|
||||
(T_Pipeline _ _ [x]) -> oversimplify x
|
||||
(T_Literal _ x) -> [x]
|
||||
(T_ParamSubSpecialChar _ x) -> [x]
|
||||
(T_SimpleCommand _ vars words) -> concatMap oversimplify words
|
||||
(T_Redirecting _ _ foo) -> oversimplify foo
|
||||
(T_DollarSingleQuoted _ s) -> [s]
|
||||
(T_Annotation _ _ s) -> oversimplify s
|
||||
-- Workaround for let "foo = bar" parsing
|
||||
(TA_Sequence _ [TA_Expansion _ v]) -> concatMap oversimplify v
|
||||
otherwise -> []
|
||||
_ -> []
|
||||
|
||||
|
||||
-- Turn a SimpleCommand foo -avz --bar=baz into args "a", "v", "z", "bar",
|
||||
@@ -111,8 +113,11 @@ getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags o
|
||||
|
||||
-- Get all flags in a GNU way, up until --
|
||||
getAllFlags = getFlagsUntil (== "--")
|
||||
-- Get all flags in a BSD way, up until first non-flag argument
|
||||
getLeadingFlags = getFlagsUntil (not . ("-" `isPrefixOf`))
|
||||
-- Get all flags in a BSD way, up until first non-flag argument or --
|
||||
getLeadingFlags = getFlagsUntil (\x -> x == "--" || (not $ "-" `isPrefixOf` x))
|
||||
|
||||
-- Check if a command has a flag.
|
||||
hasFlag cmd str = str `elem` (map snd $ getAllFlags cmd)
|
||||
|
||||
|
||||
-- Given a T_DollarBraced, return a simplified version of the string contents.
|
||||
@@ -170,6 +175,20 @@ getUnquotedLiteral (T_NormalWord _ list) =
|
||||
str _ = Nothing
|
||||
getUnquotedLiteral _ = Nothing
|
||||
|
||||
-- Get the last unquoted T_Literal in a word like "${var}foo"THIS
|
||||
-- or nothing if the word does not end in an unquoted literal.
|
||||
getTrailingUnquotedLiteral :: Token -> Maybe Token
|
||||
getTrailingUnquotedLiteral t =
|
||||
case t of
|
||||
(T_NormalWord _ list@(_:_)) ->
|
||||
from (last list)
|
||||
_ -> Nothing
|
||||
where
|
||||
from t =
|
||||
case t of
|
||||
(T_Literal {}) -> return t
|
||||
_ -> Nothing
|
||||
|
||||
-- Maybe get the literal string of this token and any globs in it.
|
||||
getGlobOrLiteralString = getLiteralStringExt f
|
||||
where
|
||||
@@ -188,6 +207,7 @@ getLiteralStringExt more = g
|
||||
g (TA_Expansion _ l) = allInList l
|
||||
g (T_SingleQuoted _ s) = return s
|
||||
g (T_Literal _ s) = return s
|
||||
g (T_ParamSubSpecialChar _ s) = return s
|
||||
g x = more x
|
||||
|
||||
-- Is this token a string literal?
|
||||
@@ -221,8 +241,15 @@ getCommand t =
|
||||
|
||||
-- Maybe get the command name of a token representing a command
|
||||
getCommandName t = do
|
||||
(T_SimpleCommand _ _ (w:_)) <- getCommand t
|
||||
getLiteralString w
|
||||
(T_SimpleCommand _ _ (w:rest)) <- getCommand t
|
||||
s <- getLiteralString w
|
||||
if "busybox" `isSuffixOf` s
|
||||
then
|
||||
case rest of
|
||||
(applet:_) -> getLiteralString applet
|
||||
_ -> return s
|
||||
else
|
||||
return s
|
||||
|
||||
-- If a command substitution is a single command, get its name.
|
||||
-- $(date +%s) = Just "date"
|
||||
@@ -260,6 +287,8 @@ isOnlyRedirection t =
|
||||
|
||||
isFunction t = case t of T_Function {} -> True; _ -> False
|
||||
|
||||
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
|
||||
|
||||
-- Get the lists of commands from tokens that contain them, such as
|
||||
-- the body of while loops or branches of if statements.
|
||||
getCommandSequences t =
|
||||
@@ -281,7 +310,7 @@ getAssociativeArrays t =
|
||||
f :: Token -> Writer [String] ()
|
||||
f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do
|
||||
name <- getCommandName t
|
||||
guard $ name == "declare"
|
||||
guard $ name == "declare" || name == "typeset"
|
||||
let flags = getAllFlags t
|
||||
guard $ elem "A" $ map snd flags
|
||||
let args = map fst . filter ((==) "" . snd) $ flags
|
||||
@@ -293,3 +322,65 @@ getAssociativeArrays t =
|
||||
case t of
|
||||
T_Assignment _ _ name _ _ -> return name
|
||||
otherwise -> Nothing
|
||||
|
||||
-- A Pseudoglob is a wildcard pattern used for checking if a match can succeed.
|
||||
-- For example, [[ $(cmd).jpg == [a-z] ]] will give the patterns *.jpg and ?, which
|
||||
-- can be proven never to match.
|
||||
data PseudoGlob = PGAny | PGMany | PGChar Char
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- Turn a word into a PG pattern, replacing all unknown/runtime values with
|
||||
-- PGMany.
|
||||
wordToPseudoGlob :: Token -> Maybe [PseudoGlob]
|
||||
wordToPseudoGlob word =
|
||||
simplifyPseudoGlob <$> concat <$> mapM f (getWordParts word)
|
||||
where
|
||||
f x = case x of
|
||||
T_Literal _ s -> return $ map PGChar s
|
||||
T_SingleQuoted _ s -> return $ map PGChar s
|
||||
|
||||
T_DollarBraced {} -> return [PGMany]
|
||||
T_DollarExpansion {} -> return [PGMany]
|
||||
T_Backticked {} -> return [PGMany]
|
||||
|
||||
T_Glob _ "?" -> return [PGAny]
|
||||
T_Glob _ ('[':_) -> return [PGAny]
|
||||
T_Glob {} -> return [PGMany]
|
||||
|
||||
T_Extglob {} -> return [PGMany]
|
||||
|
||||
_ -> return [PGMany]
|
||||
|
||||
-- Reorder a PseudoGlob for more efficient matching, e.g.
|
||||
-- f?*?**g -> f??*g
|
||||
simplifyPseudoGlob :: [PseudoGlob] -> [PseudoGlob]
|
||||
simplifyPseudoGlob = f
|
||||
where
|
||||
f [] = []
|
||||
f (x@(PGChar _) : rest ) = x : f rest
|
||||
f list =
|
||||
let (anys, rest) = span (\x -> x == PGMany || x == PGAny) list in
|
||||
order anys ++ f rest
|
||||
|
||||
order s = let (any, many) = partition (== PGAny) s in
|
||||
any ++ take 1 many
|
||||
|
||||
-- Check whether the two patterns can ever overlap.
|
||||
pseudoGlobsCanOverlap :: [PseudoGlob] -> [PseudoGlob] -> Bool
|
||||
pseudoGlobsCanOverlap = matchable
|
||||
where
|
||||
matchable x@(xf:xs) y@(yf:ys) =
|
||||
case (xf, yf) of
|
||||
(PGMany, _) -> matchable x ys || matchable xs y
|
||||
(_, PGMany) -> matchable x ys || matchable xs y
|
||||
(PGAny, _) -> matchable xs ys
|
||||
(_, PGAny) -> matchable xs ys
|
||||
(_, _) -> xf == yf && matchable xs ys
|
||||
|
||||
matchable [] [] = True
|
||||
matchable (PGMany : rest) [] = matchable rest []
|
||||
matchable (_:_) [] = False
|
||||
matchable [] r = matchable r []
|
||||
|
||||
wordsCanBeEqual x y = fromMaybe True $
|
||||
liftM2 pseudoGlobsCanOverlap (wordToPseudoGlob x) (wordToPseudoGlob y)
|
||||
|
@@ -22,7 +22,7 @@ module ShellCheck.Analytics (runAnalytics, ShellCheck.Analytics.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.AnalyzerLib hiding (producesComments)
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Interface
|
||||
@@ -50,7 +50,7 @@ treeChecks :: [Parameters -> Token -> [TokenComment]]
|
||||
treeChecks = [
|
||||
runNodeAnalysis
|
||||
(\p t -> (mapM_ ((\ f -> f t) . (\ f -> f p))
|
||||
(nodeChecks ++ checksFor (shellType p))))
|
||||
nodeChecks))
|
||||
,subshellAssignmentCheck
|
||||
,checkSpacefulness
|
||||
,checkQuotesInLiterals
|
||||
@@ -62,30 +62,7 @@ treeChecks = [
|
||||
,checkShebang
|
||||
,checkUnassignedReferences
|
||||
,checkUncheckedCd
|
||||
]
|
||||
|
||||
checksFor Sh = [
|
||||
checkBashisms
|
||||
,checkTimeParameters
|
||||
,checkForDecimals
|
||||
,checkTimedCommand
|
||||
]
|
||||
checksFor Dash = [
|
||||
checkBashisms
|
||||
,checkForDecimals
|
||||
,checkLocalScope
|
||||
,checkTimedCommand
|
||||
]
|
||||
checksFor Ksh = [
|
||||
checkEchoSed
|
||||
]
|
||||
checksFor Bash = [
|
||||
checkTimeParameters
|
||||
,checkBraceExpansionVars
|
||||
,checkEchoSed
|
||||
,checkForDecimals
|
||||
,checkLocalScope
|
||||
,checkMultiDimensionalArrays
|
||||
,checkArrayAssignmentIndices
|
||||
]
|
||||
|
||||
runAnalytics :: AnalysisSpec -> [TokenComment]
|
||||
@@ -122,7 +99,7 @@ nodeChecks = [
|
||||
,checkSingleBracketOperators
|
||||
,checkDoubleBracketOperators
|
||||
,checkLiteralBreakingTest
|
||||
,checkConstantNoary
|
||||
,checkConstantNullary
|
||||
,checkDivBeforeMult
|
||||
,checkArithmeticDeref
|
||||
,checkArithmeticBadOctal
|
||||
@@ -172,7 +149,7 @@ nodeChecks = [
|
||||
,checkMultipleAppends
|
||||
,checkSuspiciousIFS
|
||||
,checkShouldUseGrepQ
|
||||
,checkTestGlobs
|
||||
,checkTestArgumentSplitting
|
||||
,checkConcatenatedDollarAt
|
||||
,checkTildeInPath
|
||||
,checkMaskedReturns
|
||||
@@ -180,6 +157,9 @@ nodeChecks = [
|
||||
,checkLoopVariableReassignment
|
||||
,checkTrailingBracket
|
||||
,checkReturnAgainstZero
|
||||
,checkRedirectedNowhere
|
||||
,checkUnmatchableCases
|
||||
,checkSubshellAsTest
|
||||
]
|
||||
|
||||
|
||||
@@ -264,30 +244,6 @@ checkEchoWc _ (T_Pipeline id _ [a, b]) =
|
||||
countMsg = style id 2000 "See if you can use ${#variable} instead."
|
||||
checkEchoWc _ _ = return ()
|
||||
|
||||
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
||||
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
||||
checkEchoSed _ (T_Pipeline id _ [a, b]) =
|
||||
when (acmd == ["echo", "${VAR}"]) $
|
||||
case bcmd of
|
||||
["sed", v] -> checkIn v
|
||||
["sed", "-e", v] -> checkIn v
|
||||
_ -> return ()
|
||||
where
|
||||
-- This should have used backreferences, but TDFA doesn't support them
|
||||
sedRe = mkRegex "^s(.)([^\n]*)g?$"
|
||||
isSimpleSed s = fromMaybe False $ do
|
||||
[first,rest] <- matchRegex sedRe s
|
||||
let delimiters = filter (== (head first)) rest
|
||||
guard $ length delimiters == 2
|
||||
return True
|
||||
|
||||
acmd = oversimplify a
|
||||
bcmd = oversimplify b
|
||||
checkIn s =
|
||||
when (isSimpleSed s) $
|
||||
style id 2001 "See if you can use ${variable//search/replace} instead."
|
||||
checkEchoSed _ _ = return ()
|
||||
|
||||
prop_checkPipedAssignment1 = verify checkPipedAssignment "A=ls | grep foo"
|
||||
prop_checkPipedAssignment2 = verifyNot checkPipedAssignment "A=foo cmd | grep foo"
|
||||
prop_checkPipedAssignment3 = verifyNot checkPipedAssignment "A=foo"
|
||||
@@ -373,6 +329,13 @@ prop_checkPipePitfalls6 = verify checkPipePitfalls "find . | xargs foo"
|
||||
prop_checkPipePitfalls7 = verifyNot checkPipePitfalls "find . -printf '%s\\n' | xargs foo"
|
||||
prop_checkPipePitfalls8 = verify checkPipePitfalls "foo | grep bar | wc -l"
|
||||
prop_checkPipePitfalls9 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -l"
|
||||
prop_checkPipePitfalls10 = verifyNot checkPipePitfalls "foo | grep -o bar | wc"
|
||||
prop_checkPipePitfalls11 = verifyNot checkPipePitfalls "foo | grep bar | wc"
|
||||
prop_checkPipePitfalls12 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -c"
|
||||
prop_checkPipePitfalls13 = verifyNot checkPipePitfalls "foo | grep bar | wc -c"
|
||||
prop_checkPipePitfalls14 = verifyNot checkPipePitfalls "foo | grep -o bar | wc -cmwL"
|
||||
prop_checkPipePitfalls15 = verifyNot checkPipePitfalls "foo | grep bar | wc -cmwL"
|
||||
prop_checkPipePitfalls16 = verifyNot checkPipePitfalls "foo | grep -r bar | wc -l"
|
||||
checkPipePitfalls _ (T_Pipeline id _ commands) = do
|
||||
for ["find", "xargs"] $
|
||||
\(find:xargs:_) ->
|
||||
@@ -394,10 +357,11 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
|
||||
|
||||
for ["grep", "wc"] $
|
||||
\(grep:wc:_) ->
|
||||
let flags = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep
|
||||
let flagsGrep = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep
|
||||
flagsWc = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand wc
|
||||
in
|
||||
unless (any (`elem` ["o", "only-matching"]) flags) $
|
||||
style (getId grep) 2126 "Consider using grep -c instead of grep|wc."
|
||||
unless ((any (`elem` ["o", "only-matching", "r", "R", "recursive"]) flagsGrep) || (any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc) || ((length flagsWc) == 0)) $
|
||||
style (getId grep) 2126 "Consider using grep -c instead of grep|wc -l."
|
||||
|
||||
didLs <- liftM or . sequence $ [
|
||||
for' ["ls", "grep"] $
|
||||
@@ -448,233 +412,20 @@ prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho co
|
||||
prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l "
|
||||
prop_checkShebang3 = verifyTree checkShebang "ls -l"
|
||||
prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"
|
||||
prop_checkShebang5 = verifyTree checkShebang "#!/usr/bin/env ash"
|
||||
prop_checkShebang6 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=dash\n"
|
||||
prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n"
|
||||
checkShebang params (T_Annotation _ list t) =
|
||||
if any isOverride list then [] else checkShebang params t
|
||||
where
|
||||
isOverride (ShellOverride _) = True
|
||||
isOverride _ = False
|
||||
checkShebang params (T_Script id sb _) =
|
||||
[makeComment ErrorC id 2148
|
||||
"Tips depend on target shell and yours is unknown. Add a shebang."
|
||||
| not (shellTypeSpecified params) && sb == "" ]
|
||||
|
||||
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
|
||||
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
|
||||
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
|
||||
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
|
||||
prop_checkBashisms5 = verify checkBashisms "source file"
|
||||
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
||||
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
||||
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
||||
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
||||
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
|
||||
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
|
||||
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
|
||||
prop_checkBashisms13= verify checkBashisms "exec -c env"
|
||||
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
|
||||
prop_checkBashisms15= verify checkBashisms "let n++"
|
||||
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
|
||||
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
|
||||
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
|
||||
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
|
||||
prop_checkBashisms20= verify checkBashisms "read -ra foo"
|
||||
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
|
||||
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
|
||||
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
|
||||
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
|
||||
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
|
||||
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
|
||||
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
|
||||
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
|
||||
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
|
||||
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
|
||||
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
|
||||
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
|
||||
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
|
||||
prop_checkBashisms41= verify checkBashisms "echo `<file`"
|
||||
prop_checkBashisms42= verify checkBashisms "trap foo int"
|
||||
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
|
||||
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
|
||||
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||
checkBashisms params = bashism
|
||||
where
|
||||
isDash = shellType params == Dash
|
||||
warnMsg id s =
|
||||
if isDash
|
||||
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
|
||||
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
|
||||
|
||||
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
|
||||
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
|
||||
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
|
||||
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
|
||||
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
|
||||
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
|
||||
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
|
||||
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
|
||||
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
|
||||
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
|
||||
bashism (T_HereString id _) = warnMsg id "here-strings are"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "-nt", "-ef", "\\<", "\\>"] =
|
||||
unless isDash $ warnMsg id $ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket "==" _ _) =
|
||||
warnMsg id "== in place of = is"
|
||||
bashism (TC_Unary id _ "-a" _) =
|
||||
warnMsg id "unary -a in place of -e is"
|
||||
bashism (TA_Unary id op _)
|
||||
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
||||
warnMsg id $ filter (/= '|') op ++ " is"
|
||||
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
|
||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
|
||||
bashism (T_FdRedirect id num _)
|
||||
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
|
||||
bashism (T_IoFile id _ word) | isNetworked =
|
||||
warnMsg id "/dev/{tcp,udp} is"
|
||||
where
|
||||
file = onlyLiteralString word
|
||||
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
||||
|
||||
bashism t@(TA_Expansion id _) | isBashism =
|
||||
warnMsg id $ fromJust str ++ " is"
|
||||
where
|
||||
str = getLiteralString t
|
||||
isBashism = isJust str && isBashVariable (fromJust str)
|
||||
bashism t@(T_DollarBraced id token) = do
|
||||
mapM_ check expansion
|
||||
when (isBashVariable var) $
|
||||
warnMsg id $ var ++ " is"
|
||||
where
|
||||
str = bracedString t
|
||||
var = getBracedReference str
|
||||
check (regex, feature) =
|
||||
when (isJust $ matchRegex regex str) $ warnMsg id feature
|
||||
|
||||
bashism t@(T_Pipe id "|&") =
|
||||
warnMsg id "|& in place of 2>&1 | is"
|
||||
bashism (T_Array id _) =
|
||||
warnMsg id "arrays are"
|
||||
bashism (T_IoFile id _ t) | isGlob t =
|
||||
warnMsg id "redirecting to/from globs is"
|
||||
bashism (T_CoProc id _ _) =
|
||||
warnMsg id "coproc is"
|
||||
|
||||
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
||||
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
||||
|
||||
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
||||
warnMsg id "$(<file) to read files is"
|
||||
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
||||
warnMsg id "`<file` to read files is"
|
||||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "echo" && "-" `isPrefixOf` argString =
|
||||
unless ("--" `isPrefixOf` argString) $ -- echo "-----"
|
||||
if isDash
|
||||
then
|
||||
when (argString /= "-n") $
|
||||
warnMsg (getId arg) "echo flags besides -n"
|
||||
else
|
||||
warnMsg (getId arg) "echo flags are"
|
||||
where argString = concat $ oversimplify arg
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||
warnMsg (getId arg) "exec flags are"
|
||||
bashism t@(T_SimpleCommand id _ _)
|
||||
| t `isCommand` "let" = warnMsg id "'let' is"
|
||||
|
||||
bashism t@(T_SimpleCommand id _ (cmd:rest)) =
|
||||
let name = fromMaybe "" $ getCommandName t
|
||||
flags = getLeadingFlags t
|
||||
in do
|
||||
when (name `elem` unsupportedCommands) $
|
||||
warnMsg id $ "'" ++ name ++ "' is"
|
||||
potentially $ do
|
||||
allowed <- Map.lookup name allowedFlags
|
||||
(word, flag) <- listToMaybe $
|
||||
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
|
||||
|
||||
when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
||||
when (name == "trap") $
|
||||
let
|
||||
check token = potentially $ do
|
||||
str <- getLiteralString token
|
||||
let upper = map toUpper str
|
||||
return $ do
|
||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||
warnMsg (getId token) $ "trapping " ++ str ++ " is"
|
||||
when ("SIG" `isPrefixOf` upper) $
|
||||
warnMsg (getId token)
|
||||
"prefixing signal names with 'SIG' is"
|
||||
when (not isDash && upper /= str) $
|
||||
warnMsg (getId token)
|
||||
"using lower/mixed case for signal names is"
|
||||
in
|
||||
mapM_ check (drop 1 rest)
|
||||
|
||||
when (name == "printf") $ potentially $ do
|
||||
format <- rest !!! 0 -- flags are covered by allowedFlags
|
||||
let literal = onlyLiteralString format
|
||||
guard $ "%q" `isInfixOf` literal
|
||||
return $ warnMsg (getId format) "printf %q is"
|
||||
where
|
||||
unsupportedCommands = [
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||
"typeset"
|
||||
] ++ if not isDash then ["local", "type"] else []
|
||||
allowedFlags = Map.fromList [
|
||||
("read", if isDash then ["r", "p"] else ["r"]),
|
||||
("ulimit", ["f"]),
|
||||
("printf", []),
|
||||
("exec", [])
|
||||
]
|
||||
|
||||
bashism _ = return ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
expansion = let re = mkRegex in [
|
||||
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
|
||||
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
|
||||
]
|
||||
bashVars = [
|
||||
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
||||
]
|
||||
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||
dashVars = [ "LINENO" ]
|
||||
isBashVariable var =
|
||||
(var `elem` bashDynamicVars
|
||||
|| var `elem` bashVars && not (isAssigned var))
|
||||
&& not (isDash && var `elem` dashVars)
|
||||
isAssigned var = any f (variableFlow params)
|
||||
where
|
||||
f x = case x of
|
||||
Assignment (_, _, name, _) -> name == var
|
||||
_ -> False
|
||||
checkShebang params (T_Script id sb _) = execWriter $
|
||||
unless (shellTypeSpecified params) $ do
|
||||
when (sb == "") $
|
||||
err id 2148 "Tips depend on target shell and yours is unknown. Add a shebang."
|
||||
when (executableFromShebang sb == "ash") $
|
||||
warn id 2187 "Ash scripts will be checked as Dash. Add '# shellcheck shell=dash' to silence."
|
||||
|
||||
|
||||
prop_checkForInQuoted = verify checkForInQuoted "for f in \"$(ls)\"; do echo foo; done"
|
||||
@@ -690,13 +441,13 @@ checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [word@(T_DoubleQuoted id list)]]
|
||||
when (any (\x -> willSplit x && not (mayBecomeMultipleArgs x)) list
|
||||
|| (liftM wouldHaveBeenGlob (getLiteralString word) == Just True)) $
|
||||
err id 2066 "Since you double quoted this, it will not word split, and the loop will only run once."
|
||||
checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id s]] _) =
|
||||
warn id 2041 $ "This is a literal string. To run as a command, use $(" ++ s ++ ")."
|
||||
checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_SingleQuoted id _]] _) =
|
||||
warn id 2041 "This is a literal string. To run as a command, use $(..) instead of '..' . "
|
||||
checkForInQuoted _ (T_ForIn _ f [T_NormalWord _ [T_Literal id s]] _) =
|
||||
if ',' `elem` s
|
||||
then unless ('{' `elem` s) $
|
||||
warn id 2042 "Use spaces, not commas, to separate loop elements."
|
||||
else warn id 2043 $ "This loop will only run once, with " ++ f ++ "='" ++ s ++ "'."
|
||||
else warn id 2043 "This loop will only ever run once for a constant value. Did you perhaps mean to loop over dir/*, $var or $(cmd)?"
|
||||
checkForInQuoted _ _ = return ()
|
||||
|
||||
prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"
|
||||
@@ -994,9 +745,9 @@ checkStderrRedirect params redir@(T_Redirecting _ [
|
||||
usesOutput t =
|
||||
case t of
|
||||
(T_Pipeline _ _ list) -> length list > 1 && not (isParentOf (parentMap params) (last list) redir)
|
||||
(T_ProcSub {}) -> True
|
||||
(T_DollarExpansion {}) -> True
|
||||
(T_Backticked {}) -> True
|
||||
T_ProcSub {} -> True
|
||||
T_DollarExpansion {} -> True
|
||||
T_Backticked {} -> True
|
||||
_ -> False
|
||||
isCaptured = any usesOutput $ getPath (parentMap params) redir
|
||||
|
||||
@@ -1025,6 +776,7 @@ prop_checkSingleQuotedVariables9 = verifyNot checkSingleQuotedVariables "find .
|
||||
prop_checkSingleQuotedVariables10= verify checkSingleQuotedVariables "echo '`pwd`'"
|
||||
prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${/lol/d}'"
|
||||
prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"
|
||||
prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"
|
||||
checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
when (s `matches` re) $
|
||||
if "sed" == commandName
|
||||
@@ -1062,7 +814,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
isOkAssignment t =
|
||||
case t of
|
||||
T_Assignment _ _ name _ _ -> name `elem` commonlyQuoted
|
||||
otherwise -> False
|
||||
_ -> False
|
||||
|
||||
re = mkRegex "\\$[{(0-9a-zA-Z_]|`.*`"
|
||||
sedContra = mkRegex "\\$[{dpsaic]($|[^a-zA-Z])"
|
||||
@@ -1222,6 +974,7 @@ prop_checkQuotedCondRegex2 = verify checkQuotedCondRegex "[[ $foo =~ '(cow|bar)'
|
||||
prop_checkQuotedCondRegex3 = verifyNot checkQuotedCondRegex "[[ $foo =~ $foo ]]"
|
||||
prop_checkQuotedCondRegex4 = verifyNot checkQuotedCondRegex "[[ $foo =~ \"bar\" ]]"
|
||||
prop_checkQuotedCondRegex5 = verifyNot checkQuotedCondRegex "[[ $foo =~ 'cow bar' ]]"
|
||||
prop_checkQuotedCondRegex6 = verify checkQuotedCondRegex "[[ $foo =~ 'cow|bar' ]]"
|
||||
checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
|
||||
case rhs of
|
||||
T_NormalWord id [T_DoubleQuoted _ _] -> error rhs
|
||||
@@ -1232,7 +985,7 @@ checkQuotedCondRegex _ (TC_Binary _ _ "=~" _ rhs) =
|
||||
unless (isConstantNonRe t) $
|
||||
err (getId t) 2076
|
||||
"Don't quote rhs of =~, it'll match literally rather than as a regex."
|
||||
re = mkRegex "[][*.+()]"
|
||||
re = mkRegex "[][*.+()|]"
|
||||
hasMetachars s = s `matches` re
|
||||
isConstantNonRe t = fromMaybe False $ do
|
||||
s <- getLiteralString t
|
||||
@@ -1260,14 +1013,20 @@ prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"
|
||||
prop_checkConstantIfs6 = verifyNot checkConstantIfs "[[ a -ot b ]]"
|
||||
prop_checkConstantIfs7 = verifyNot checkConstantIfs "[ a -nt b ]"
|
||||
prop_checkConstantIfs8 = verifyNot checkConstantIfs "[[ ~foo == '~foo' ]]"
|
||||
prop_checkConstantIfs9 = verify checkConstantIfs "[[ *.png == [a-z] ]]"
|
||||
checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =
|
||||
when (isConstant lhs && isConstant rhs) $
|
||||
warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
|
||||
if isConstant lhs && isConstant rhs
|
||||
then warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
|
||||
else checkUnmatchable id op lhs rhs
|
||||
where
|
||||
isDynamic =
|
||||
op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]
|
||||
&& typ == DoubleBracket
|
||||
|| op `elem` [ "-nt", "-ot", "-ef"]
|
||||
|
||||
checkUnmatchable id op lhs rhs =
|
||||
when (op `elem` ["=", "==", "!="] && not (wordsCanBeEqual lhs rhs)) $
|
||||
warn id 2193 "The arguments to this comparison can never be equal. Make sure your syntax is correct."
|
||||
checkConstantIfs _ _ = return ()
|
||||
|
||||
prop_checkLiteralBreakingTest = verify checkLiteralBreakingTest "[[ a==$foo ]]"
|
||||
@@ -1281,7 +1040,7 @@ prop_checkLiteralBreakingTest8 = verifyNot checkLiteralBreakingTest "[ $(true)$(
|
||||
prop_checkLiteralBreakingTest10 = verify checkLiteralBreakingTest "[ -z foo ]"
|
||||
checkLiteralBreakingTest _ t = potentially $
|
||||
case t of
|
||||
(TC_Noary _ _ w@(T_NormalWord _ l)) -> do
|
||||
(TC_Nullary _ _ w@(T_NormalWord _ l)) -> do
|
||||
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)) ->
|
||||
@@ -1305,14 +1064,14 @@ checkLiteralBreakingTest _ t = potentially $
|
||||
token <- listToMaybe $ filter isNonEmpty $ getWordParts t
|
||||
return $ err (getId token) 2157 s
|
||||
|
||||
prop_checkConstantNoary = verify checkConstantNoary "[[ '$(foo)' ]]"
|
||||
prop_checkConstantNoary2 = verify checkConstantNoary "[ \"-f lol\" ]"
|
||||
prop_checkConstantNoary3 = verify checkConstantNoary "[[ cmd ]]"
|
||||
prop_checkConstantNoary4 = verify checkConstantNoary "[[ ! cmd ]]"
|
||||
prop_checkConstantNoary5 = verify checkConstantNoary "[[ true ]]"
|
||||
prop_checkConstantNoary6 = verify checkConstantNoary "[ 1 ]"
|
||||
prop_checkConstantNoary7 = verify checkConstantNoary "[ false ]"
|
||||
checkConstantNoary _ (TC_Noary _ _ t) | isConstant t =
|
||||
prop_checkConstantNullary = verify checkConstantNullary "[[ '$(foo)' ]]"
|
||||
prop_checkConstantNullary2 = verify checkConstantNullary "[ \"-f lol\" ]"
|
||||
prop_checkConstantNullary3 = verify checkConstantNullary "[[ cmd ]]"
|
||||
prop_checkConstantNullary4 = verify checkConstantNullary "[[ ! cmd ]]"
|
||||
prop_checkConstantNullary5 = verify checkConstantNullary "[[ true ]]"
|
||||
prop_checkConstantNullary6 = verify checkConstantNullary "[ 1 ]"
|
||||
prop_checkConstantNullary7 = verify checkConstantNullary "[ false ]"
|
||||
checkConstantNullary _ (TC_Nullary _ _ t) | isConstant t =
|
||||
case fromMaybe "" $ getLiteralString t of
|
||||
"false" -> err (getId t) 2158 "[ false ] is true. Remove the brackets."
|
||||
"0" -> err (getId t) 2159 "[ 0 ] is true. Use 'false' instead."
|
||||
@@ -1322,29 +1081,7 @@ checkConstantNoary _ (TC_Noary _ _ t) | isConstant t =
|
||||
where
|
||||
string = fromMaybe "" $ getLiteralString t
|
||||
|
||||
checkConstantNoary _ _ = return ()
|
||||
|
||||
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
|
||||
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
|
||||
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
|
||||
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
|
||||
checkBraceExpansionVars params t@(T_BraceExpansion id list) = mapM_ check list
|
||||
where
|
||||
check element =
|
||||
when (any (`isInfixOf` toString element) ["$..", "..$"]) $
|
||||
if isEvaled
|
||||
then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval."
|
||||
else warn id 2051 "Bash doesn't support variables in brace range expansions."
|
||||
literalExt t =
|
||||
case t of
|
||||
T_DollarBraced {} -> return "$"
|
||||
T_DollarExpansion {} -> return "$"
|
||||
T_DollarArithmetic {} -> return "$"
|
||||
otherwise -> return "-"
|
||||
toString t = fromJust $ getLiteralStringExt literalExt t
|
||||
isEvaled = fromMaybe False $
|
||||
(`isUnqualifiedCommand` "eval") <$> getClosestCommand (parentMap params) t
|
||||
checkBraceExpansionVars _ _ = return ()
|
||||
checkConstantNullary _ _ = return ()
|
||||
|
||||
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
|
||||
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
|
||||
@@ -1419,7 +1156,7 @@ checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T
|
||||
warn id 2053 $ "Quote the rhs of " ++ op ++ " in [[ ]] to prevent glob matching."
|
||||
checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
|
||||
| (op == "=" || op == "==") && isGlob word =
|
||||
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or grep."
|
||||
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
|
||||
checkComparisonAgainstGlob _ _ = return ()
|
||||
|
||||
prop_checkCommarrays1 = verify checkCommarrays "a=(1, 2)"
|
||||
@@ -1462,10 +1199,10 @@ prop_checkValidCondOps2a= verifyNot checkValidCondOps "[ 3 \\> 2 ]"
|
||||
prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]"
|
||||
prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]"
|
||||
checkValidCondOps _ (TC_Binary id _ s _ _)
|
||||
| s `notElem` ["-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="] =
|
||||
| s `notElem` binaryTestOps =
|
||||
warn id 2057 "Unknown binary operator."
|
||||
checkValidCondOps _ (TC_Unary id _ s _)
|
||||
| s `notElem` [ "!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p", "-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n", "-o", "-v", "-R"] =
|
||||
| s `notElem` unaryTestOps =
|
||||
warn id 2058 "Unknown unary operator."
|
||||
checkValidCondOps _ _ = return ()
|
||||
|
||||
@@ -1503,40 +1240,6 @@ checkUuoeVar _ p =
|
||||
"Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'."
|
||||
otherwise -> return ()
|
||||
|
||||
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
|
||||
prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"
|
||||
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
|
||||
checkTimeParameters _ = checkUnqualifiedCommand "time" f where
|
||||
f cmd (x:_) = let s = concat $ oversimplify x in
|
||||
when ("-" `isPrefixOf` s && s /= "-p") $
|
||||
info (getId cmd) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one."
|
||||
f _ _ = return ()
|
||||
|
||||
prop_checkTimedCommand1 = verify checkTimedCommand "time -p foo | bar"
|
||||
prop_checkTimedCommand2 = verify checkTimedCommand "time ( foo; bar; )"
|
||||
prop_checkTimedCommand3 = verifyNot checkTimedCommand "time sleep 1"
|
||||
checkTimedCommand _ = checkUnqualifiedCommand "time" f where
|
||||
f c args@(_:_) = do
|
||||
let cmd = last args
|
||||
when (isPiped cmd) $
|
||||
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
|
||||
when (isSimple cmd == Just False) $
|
||||
warn (getId cmd) 2177 "'time' is undefined for compound commands, time sh -c instead."
|
||||
f _ _ = return ()
|
||||
isPiped cmd =
|
||||
case cmd of
|
||||
T_Pipeline _ _ (_:_:_) -> True
|
||||
_ -> False
|
||||
getCommand cmd =
|
||||
case cmd of
|
||||
T_Pipeline _ _ ((T_Redirecting _ _ a):_) -> return a
|
||||
_ -> fail ""
|
||||
isSimple cmd = do
|
||||
innerCommand <- getCommand cmd
|
||||
case innerCommand of
|
||||
T_SimpleCommand {} -> return True
|
||||
_ -> return False
|
||||
|
||||
|
||||
prop_checkTestRedirects1 = verify checkTestRedirects "test 3 > 1"
|
||||
prop_checkTestRedirects2 = verifyNot checkTestRedirects "test 3 \\> 1"
|
||||
@@ -1646,6 +1349,7 @@ prop_checkInexplicablyUnquoted3 = verifyNot checkInexplicablyUnquoted "wget --us
|
||||
prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUES (\"id\")\""
|
||||
prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""
|
||||
prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\""
|
||||
prop_checkInexplicablyUnquoted7 = verifyNot checkInexplicablyUnquoted "${dir/\"foo\"/\"bar\"}"
|
||||
checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)
|
||||
where
|
||||
check (T_SingleQuoted _ _:T_Literal id str:_)
|
||||
@@ -1674,7 +1378,7 @@ checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens
|
||||
warnAboutExpansion id =
|
||||
warn id 2027 "The surrounding quotes actually unquote this. Remove or escape them."
|
||||
warnAboutLiteral id =
|
||||
warn id 2140 "Word is on the form \"A\"B\"C\" (B indicated). Did you mean \"ABC\" or \"A\\\"B\\\"C\"?"
|
||||
warn id 2140 "Word is of the form \"A\"B\"C\" (B indicated). Did you mean \"ABC\" or \"A\\\"B\\\"C\"?"
|
||||
checkInexplicablyUnquoted _ _ = return ()
|
||||
|
||||
prop_checkTildeInQuotes1 = verify checkTildeInQuotes "var=\"~/out.txt\""
|
||||
@@ -1880,6 +1584,8 @@ prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>
|
||||
prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;"
|
||||
prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\""
|
||||
prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
|
||||
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
|
||||
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
||||
|
||||
checkSpacefulness params t =
|
||||
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
||||
@@ -2148,6 +1854,7 @@ prop_checkUnassignedReferences24= verifyNotTree checkUnassignedReferences "decla
|
||||
prop_checkUnassignedReferences25= verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;"
|
||||
prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b"
|
||||
prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}"
|
||||
prop_checkUnassignedReferences28= verifyNotTree checkUnassignedReferences "#!/bin/ksh\necho \"${.sh.version}\"\n"
|
||||
checkUnassignedReferences params t = warnings
|
||||
where
|
||||
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
||||
@@ -2246,12 +1953,14 @@ prop_checkWhileReadPitfalls4 = verifyNot checkWhileReadPitfalls "while read foo;
|
||||
prop_checkWhileReadPitfalls5 = verifyNot checkWhileReadPitfalls "while read foo; do echo ls | ssh $foo; done"
|
||||
prop_checkWhileReadPitfalls6 = verifyNot checkWhileReadPitfalls "while read foo <&3; do ssh $foo; done 3< foo"
|
||||
prop_checkWhileReadPitfalls7 = verify checkWhileReadPitfalls "while read foo; do if true; then ssh $foo uptime; fi; done < file"
|
||||
prop_checkWhileReadPitfalls8 = verifyNot checkWhileReadPitfalls "while read foo; do ssh -n $foo uptime; done < file"
|
||||
|
||||
checkWhileReadPitfalls _ (T_WhileExpression id [command] contents)
|
||||
| isStdinReadCommand command =
|
||||
mapM_ checkMuncher contents
|
||||
where
|
||||
munchers = [ "ssh", "ffmpeg", "mplayer" ]
|
||||
munchers = [ "ssh", "ffmpeg", "mplayer", "HandBrakeCLI" ]
|
||||
preventionFlags = ["n", "noconsolecontrols" ]
|
||||
|
||||
isStdinReadCommand (T_Pipeline _ _ [T_Redirecting id redirs cmd]) =
|
||||
let plaintext = oversimplify cmd
|
||||
@@ -2268,6 +1977,10 @@ checkWhileReadPitfalls _ (T_WhileExpression id [command] contents)
|
||||
_ -> potentially $ do
|
||||
name <- getCommandBasename cmd
|
||||
guard $ name `elem` munchers
|
||||
|
||||
-- Sloppily check if the command has a flag to prevent eating stdin.
|
||||
let flags = getAllFlags cmd
|
||||
guard . not $ any (`elem` preventionFlags) $ map snd flags
|
||||
return $ do
|
||||
info id 2095 $
|
||||
name ++ " may swallow stdin, preventing this loop from working properly."
|
||||
@@ -2386,14 +2099,6 @@ checkLoopKeywordScope params 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; }"
|
||||
@@ -2677,7 +2382,7 @@ prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"
|
||||
prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]"
|
||||
checkShouldUseGrepQ params t =
|
||||
potentially $ case t of
|
||||
TC_Noary id _ token -> check id True token
|
||||
TC_Nullary id _ token -> check id True token
|
||||
TC_Unary id _ "-n" token -> check id True token
|
||||
TC_Unary id _ "-z" token -> check id False token
|
||||
_ -> fail "not check"
|
||||
@@ -2705,12 +2410,67 @@ checkShouldUseGrepQ params t =
|
||||
_ -> fail "unknown"
|
||||
isGrep = (`elem` ["grep", "egrep", "fgrep", "zgrep"])
|
||||
|
||||
prop_checkTestGlobs1 = verify checkTestGlobs "[ -e *.mp3 ]"
|
||||
prop_checkTestGlobs2 = verifyNot checkTestGlobs "[[ $a == *b* ]]"
|
||||
checkTestGlobs params (TC_Unary _ _ op token) | isGlob token =
|
||||
err (getId token) 2144 $
|
||||
op ++ " doesn't work with globs. Use a for loop."
|
||||
checkTestGlobs _ _ = return ()
|
||||
prop_checkTestArgumentSplitting1 = verify checkTestArgumentSplitting "[ -e *.mp3 ]"
|
||||
prop_checkTestArgumentSplitting2 = verifyNot checkTestArgumentSplitting "[[ $a == *b* ]]"
|
||||
prop_checkTestArgumentSplitting3 = verify checkTestArgumentSplitting "[[ *.png == '' ]]"
|
||||
prop_checkTestArgumentSplitting4 = verify checkTestArgumentSplitting "[[ foo == f{o,oo,ooo} ]]"
|
||||
prop_checkTestArgumentSplitting5 = verify checkTestArgumentSplitting "[[ $@ ]]"
|
||||
prop_checkTestArgumentSplitting6 = verify checkTestArgumentSplitting "[ -e $@ ]"
|
||||
prop_checkTestArgumentSplitting7 = verify checkTestArgumentSplitting "[ $@ == $@ ]"
|
||||
prop_checkTestArgumentSplitting8 = verify checkTestArgumentSplitting "[[ $@ = $@ ]]"
|
||||
prop_checkTestArgumentSplitting9 = verifyNot checkTestArgumentSplitting "[[ foo =~ bar{1,2} ]]"
|
||||
prop_checkTestArgumentSplitting10 = verifyNot checkTestArgumentSplitting "[ \"$@\" ]"
|
||||
prop_checkTestArgumentSplitting11 = verify checkTestArgumentSplitting "[[ \"$@\" ]]"
|
||||
prop_checkTestArgumentSplitting12 = verify checkTestArgumentSplitting "[ *.png ]"
|
||||
prop_checkTestArgumentSplitting13 = verify checkTestArgumentSplitting "[ \"$@\" == \"\" ]"
|
||||
prop_checkTestArgumentSplitting14 = verify checkTestArgumentSplitting "[[ \"$@\" == \"\" ]]"
|
||||
prop_checkTestArgumentSplitting15 = verifyNot checkTestArgumentSplitting "[[ \"$*\" == \"\" ]]"
|
||||
checkTestArgumentSplitting :: Parameters -> Token -> Writer [TokenComment] ()
|
||||
checkTestArgumentSplitting _ t =
|
||||
case t of
|
||||
(TC_Unary _ _ op token) | isGlob token ->
|
||||
err (getId token) 2144 $
|
||||
op ++ " doesn't work with globs. Use a for loop."
|
||||
|
||||
(TC_Nullary _ typ token) -> do
|
||||
checkBraces typ token
|
||||
checkGlobs typ token
|
||||
when (typ == DoubleBracket) $
|
||||
checkArrays typ token
|
||||
|
||||
(TC_Unary _ typ op token) -> checkAll typ token
|
||||
|
||||
(TC_Binary _ typ op lhs rhs) ->
|
||||
if op `elem` ["=", "==", "!=", "=~"]
|
||||
then do
|
||||
checkAll typ lhs
|
||||
checkArrays typ rhs
|
||||
checkBraces typ rhs
|
||||
else mapM_ (checkAll typ) [lhs, rhs]
|
||||
_ -> return ()
|
||||
where
|
||||
checkAll typ token = do
|
||||
checkArrays typ token
|
||||
checkBraces typ token
|
||||
checkGlobs typ token
|
||||
|
||||
checkArrays typ token =
|
||||
when (any isArrayExpansion $ getWordParts token) $
|
||||
if typ == SingleBracket
|
||||
then warn (getId token) 2198 "Arrays don't work as operands in [ ]. Use a loop (or concatenate with * instead of @)."
|
||||
else err (getId token) 2199 "Arrays implicitly concatenate in [[ ]]. Use a loop (or explicit * instead of @)."
|
||||
|
||||
checkBraces typ token =
|
||||
when (any isBraceExpansion $ getWordParts token) $
|
||||
if typ == SingleBracket
|
||||
then warn (getId token) 2200 "Brace expansions don't work as operands in [ ]. Use a loop."
|
||||
else err (getId token) 2201 "Brace expansion doesn't happen in [[ ]]. Use a loop."
|
||||
|
||||
checkGlobs typ token =
|
||||
when (isGlob token) $
|
||||
if typ == SingleBracket
|
||||
then warn (getId token) 2202 "Globs don't work as operands in [ ]. Use a loop."
|
||||
else err (getId token) 2203 "Globs are ignored in [[ ]] except right of =/!=. Use a loop."
|
||||
|
||||
|
||||
prop_checkMaskedReturns1 = verify checkMaskedReturns "f() { local a=$(false); }"
|
||||
@@ -2757,7 +2517,7 @@ checkUncheckedCd params root =
|
||||
when(t `isUnqualifiedCommand` "cd"
|
||||
&& not (isCdDotDot t)
|
||||
&& not (isCondition $ getPath (parentMap params) t)) $
|
||||
warn (getId t) 2164 "Use cd ... || exit in case cd fails."
|
||||
warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
|
||||
checkElement _ = return ()
|
||||
isCdDotDot t = oversimplify t == ["cd", ".."]
|
||||
hasSetE = isNothing $ doAnalysis (guard . not . isSetE) root
|
||||
@@ -2823,25 +2583,6 @@ checkTrailingBracket _ token =
|
||||
"]" -> "["
|
||||
x -> x
|
||||
|
||||
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
|
||||
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
|
||||
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
|
||||
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
|
||||
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
|
||||
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
|
||||
checkMultiDimensionalArrays _ token =
|
||||
case token of
|
||||
T_Assignment _ _ name (first:second:_) _ -> about second
|
||||
T_IndexedElement _ (first:second:_) _ -> about second
|
||||
T_DollarBraced {} ->
|
||||
when (isMultiDim token) $ about token
|
||||
_ -> return ()
|
||||
where
|
||||
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
|
||||
|
||||
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
||||
isMultiDim t = getBracedModifier (bracedString t) `matches` re
|
||||
|
||||
prop_checkReturnAgainstZero1 = verify checkReturnAgainstZero "[ $? -eq 0 ]"
|
||||
prop_checkReturnAgainstZero2 = verify checkReturnAgainstZero "[[ \"$?\" -gt 0 ]]"
|
||||
prop_checkReturnAgainstZero3 = verify checkReturnAgainstZero "[[ 0 -ne $? ]]"
|
||||
@@ -2872,5 +2613,129 @@ checkReturnAgainstZero _ token =
|
||||
otherwise -> False
|
||||
message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?."
|
||||
|
||||
prop_checkRedirectedNowhere1 = verify checkRedirectedNowhere "> file"
|
||||
prop_checkRedirectedNowhere2 = verify checkRedirectedNowhere "> file | grep foo"
|
||||
prop_checkRedirectedNowhere3 = verify checkRedirectedNowhere "grep foo | > bar"
|
||||
prop_checkRedirectedNowhere4 = verifyNot checkRedirectedNowhere "grep foo > bar"
|
||||
prop_checkRedirectedNowhere5 = verifyNot checkRedirectedNowhere "foo | grep bar > baz"
|
||||
prop_checkRedirectedNowhere6 = verifyNot checkRedirectedNowhere "var=$(value) 2> /dev/null"
|
||||
prop_checkRedirectedNowhere7 = verifyNot checkRedirectedNowhere "var=$(< file)"
|
||||
prop_checkRedirectedNowhere8 = verifyNot checkRedirectedNowhere "var=`< file`"
|
||||
checkRedirectedNowhere params token =
|
||||
case token of
|
||||
T_Pipeline _ _ [single] -> potentially $ do
|
||||
redir <- getDanglingRedirect single
|
||||
guard . not $ isInExpansion token
|
||||
return $ warn (getId redir) 2188 "This redirection doesn't have a command. Move to its command (or use 'true' as no-op)."
|
||||
|
||||
T_Pipeline _ _ list -> forM_ list $ \x -> potentially $ do
|
||||
redir <- getDanglingRedirect x
|
||||
return $ err (getId redir) 2189 "You can't have | between this redirection and the command it should apply to."
|
||||
|
||||
_ -> return ()
|
||||
where
|
||||
isInExpansion t =
|
||||
case drop 1 $ getPath (parentMap params) t of
|
||||
T_DollarExpansion _ [_] : _ -> True
|
||||
T_Backticked _ [_] : _ -> True
|
||||
T_Annotation _ _ u : _ -> isInExpansion u
|
||||
_ -> False
|
||||
getDanglingRedirect token =
|
||||
case token of
|
||||
T_Redirecting _ (first:_) (T_SimpleCommand _ [] []) -> return first
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
prop_checkArrayAssignmentIndices1 = verifyTree checkArrayAssignmentIndices "declare -A foo; foo=(bar)"
|
||||
prop_checkArrayAssignmentIndices2 = verifyNotTree checkArrayAssignmentIndices "declare -a foo; foo=(bar)"
|
||||
prop_checkArrayAssignmentIndices3 = verifyNotTree checkArrayAssignmentIndices "declare -A foo; foo=([i]=bar)"
|
||||
prop_checkArrayAssignmentIndices4 = verifyTree checkArrayAssignmentIndices "typeset -A foo; foo+=(bar)"
|
||||
prop_checkArrayAssignmentIndices5 = verifyTree checkArrayAssignmentIndices "arr=( [foo]= bar )"
|
||||
prop_checkArrayAssignmentIndices6 = verifyTree checkArrayAssignmentIndices "arr=( [foo] = bar )"
|
||||
prop_checkArrayAssignmentIndices7 = verifyTree checkArrayAssignmentIndices "arr=( var=value )"
|
||||
prop_checkArrayAssignmentIndices8 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=bar )"
|
||||
prop_checkArrayAssignmentIndices9 = verifyNotTree checkArrayAssignmentIndices "arr=( [foo]=\"\" )"
|
||||
checkArrayAssignmentIndices params root =
|
||||
runNodeAnalysis check params root
|
||||
where
|
||||
assocs = getAssociativeArrays root
|
||||
check _ t =
|
||||
case t of
|
||||
T_Assignment _ _ name [] (T_Array _ list) ->
|
||||
let isAssoc = name `elem` assocs in
|
||||
mapM_ (checkElement isAssoc) list
|
||||
_ -> return ()
|
||||
|
||||
checkElement isAssociative t =
|
||||
case t of
|
||||
T_IndexedElement _ _ (T_Literal id "") ->
|
||||
warn id 2192 "This array element has no value. Remove spaces after = or use \"\" for empty string."
|
||||
T_IndexedElement {} ->
|
||||
return ()
|
||||
|
||||
T_NormalWord _ parts ->
|
||||
let literalEquals = do
|
||||
part <- parts
|
||||
(id, str) <- case part of
|
||||
T_Literal id str -> [(id,str)]
|
||||
_ -> []
|
||||
guard $ '=' `elem` str
|
||||
return $ warn id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it."
|
||||
in
|
||||
if (null literalEquals && isAssociative)
|
||||
then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ."
|
||||
else sequence_ literalEquals
|
||||
|
||||
_ -> return ()
|
||||
|
||||
prop_checkUnmatchableCases1 = verify checkUnmatchableCases "case foo in bar) true; esac"
|
||||
prop_checkUnmatchableCases2 = verify checkUnmatchableCases "case foo-$bar in ??|*) true; esac"
|
||||
prop_checkUnmatchableCases3 = verify checkUnmatchableCases "case foo in foo) true; esac"
|
||||
prop_checkUnmatchableCases4 = verifyNot checkUnmatchableCases "case foo-$bar in foo*|*bar|*baz*) true; esac"
|
||||
checkUnmatchableCases _ t =
|
||||
case t of
|
||||
T_CaseExpression _ word list ->
|
||||
if isConstant word
|
||||
then warn (getId word) 2194
|
||||
"This word is constant. Did you forget the $ on a variable?"
|
||||
else potentially $ do
|
||||
pg <- wordToPseudoGlob word
|
||||
return $ mapM_ (check pg) (concatMap (\(_,x,_) -> x) list)
|
||||
_ -> return ()
|
||||
where
|
||||
check target candidate = potentially $ do
|
||||
candidateGlob <- wordToPseudoGlob candidate
|
||||
guard . not $ pseudoGlobsCanOverlap target candidateGlob
|
||||
return $ warn (getId candidate) 2195
|
||||
"This pattern will never match the case statement's word. Double check them."
|
||||
|
||||
prop_checkSubshellAsTest1 = verify checkSubshellAsTest "( -e file )"
|
||||
prop_checkSubshellAsTest2 = verify checkSubshellAsTest "( 1 -gt 2 )"
|
||||
prop_checkSubshellAsTest3 = verifyNot checkSubshellAsTest "( grep -c foo bar )"
|
||||
prop_checkSubshellAsTest4 = verifyNot checkSubshellAsTest "[ 1 -gt 2 ]"
|
||||
prop_checkSubshellAsTest5 = verify checkSubshellAsTest "( -e file && -x file )"
|
||||
prop_checkSubshellAsTest6 = verify checkSubshellAsTest "( -e file || -x file && -t 1 )"
|
||||
prop_checkSubshellAsTest7 = verify checkSubshellAsTest "( ! -d file )"
|
||||
checkSubshellAsTest _ t =
|
||||
case t of
|
||||
T_Subshell id [w] -> check id w
|
||||
_ -> return ()
|
||||
where
|
||||
check id t = case t of
|
||||
(T_Banged _ w) -> check id w
|
||||
(T_AndIf _ w _) -> check id w
|
||||
(T_OrIf _ w _) -> check id w
|
||||
(T_Pipeline _ _ [T_Redirecting _ _ (T_SimpleCommand _ [] (first:second:_))]) ->
|
||||
checkParams id first second
|
||||
_ -> return ()
|
||||
|
||||
|
||||
checkParams id first second = do
|
||||
when (fromMaybe False $ (`elem` unaryTestOps) <$> getLiteralString first) $
|
||||
err id 2204 "(..) is a subshell. Did you mean [ .. ], a test expression?"
|
||||
when (fromMaybe False $ (`elem` binaryTestOps) <$> getLiteralString second) $
|
||||
warn id 2205 "(..) is a subshell. Did you mean [ .. ], a test expression?"
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
@@ -23,7 +23,9 @@ import ShellCheck.Analytics
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Interface
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
|
||||
|
||||
-- TODO: Clean up the cruft this is layered on
|
||||
@@ -32,5 +34,12 @@ analyzeScript spec = AnalysisResult {
|
||||
arComments =
|
||||
filterByAnnotation (asScript spec) . nub $
|
||||
runAnalytics spec
|
||||
++ ShellCheck.Checks.Commands.runChecks spec
|
||||
++ runChecker params (checkers params)
|
||||
}
|
||||
where
|
||||
params = makeParameters spec
|
||||
|
||||
checkers params = mconcat $ map ($ params) [
|
||||
ShellCheck.Checks.Commands.checker,
|
||||
ShellCheck.Checks.ShellSupport.checker
|
||||
]
|
||||
|
@@ -29,7 +29,7 @@ import ShellCheck.Regex
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.Reader
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.State
|
||||
import Control.Monad.Writer
|
||||
import Data.Char
|
||||
@@ -40,16 +40,48 @@ import qualified Data.Map as Map
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
type Analysis = ReaderT Parameters (Writer [TokenComment]) ()
|
||||
type Analysis = AnalyzerM ()
|
||||
type AnalyzerM a = RWS Parameters [TokenComment] Cache a
|
||||
nullCheck = const $ return ()
|
||||
|
||||
|
||||
data Checker = Checker {
|
||||
perScript :: Root -> Analysis,
|
||||
perToken :: Token -> Analysis
|
||||
}
|
||||
|
||||
runChecker :: Parameters -> Checker -> [TokenComment]
|
||||
runChecker params checker = notes
|
||||
where
|
||||
root = rootNode params
|
||||
check = perScript checker `composeAnalyzers` (\(Root x) -> void $ doAnalysis (perToken checker) x)
|
||||
notes = snd $ evalRWS (check $ Root root) params Cache
|
||||
|
||||
instance Monoid Checker where
|
||||
mempty = Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = nullCheck
|
||||
}
|
||||
mappend x y = Checker {
|
||||
perScript = perScript x `composeAnalyzers` perScript y,
|
||||
perToken = perToken x `composeAnalyzers` perToken y
|
||||
}
|
||||
|
||||
|
||||
composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
||||
composeAnalyzers f g x = f x >> g x
|
||||
|
||||
data Parameters = Parameters {
|
||||
variableFlow :: [StackData],
|
||||
parentMap :: Map.Map Id Token,
|
||||
shellType :: Shell,
|
||||
shellTypeSpecified :: Bool
|
||||
shellTypeSpecified :: Bool,
|
||||
rootNode :: Token
|
||||
}
|
||||
|
||||
-- TODO: Cache results of common AST ops here
|
||||
data Cache = Cache {}
|
||||
|
||||
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
|
||||
data StackData =
|
||||
StackScope Scope
|
||||
@@ -81,6 +113,14 @@ pScript s =
|
||||
}
|
||||
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||
|
||||
-- For testing. If parsed, returns whether there are any comments
|
||||
producesComments :: Checker -> String -> Maybe Bool
|
||||
producesComments c s = do
|
||||
root <- pScript s
|
||||
let spec = defaultSpec root
|
||||
let params = makeParameters spec
|
||||
return . not . null $ runChecker params c
|
||||
|
||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||
makeComment severity id code note =
|
||||
TokenComment id $ Comment severity code note
|
||||
@@ -95,6 +135,7 @@ style id code str = addComment $ makeComment StyleC id code str
|
||||
|
||||
makeParameters spec =
|
||||
let params = Parameters {
|
||||
rootNode = root,
|
||||
shellType = fromMaybe (determineShell root) $ asShellType spec,
|
||||
shellTypeSpecified = isJust $ asShellType spec,
|
||||
parentMap = getParentTree root,
|
||||
@@ -112,6 +153,7 @@ prop_determineShell4 = determineShell (fromJust $ pScript
|
||||
prop_determineShell5 = determineShell (fromJust $ pScript
|
||||
"#shellcheck shell=sh\nfoo") == Sh
|
||||
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
|
||||
prop_determineShell7 = determineShell (fromJust $ pScript "#! /bin/ash") == Dash
|
||||
determineShell t = fromMaybe Bash $ do
|
||||
shellString <- foldl mplus Nothing $ getCandidates t
|
||||
shellForExecutable shellString
|
||||
@@ -125,8 +167,13 @@ determineShell t = fromMaybe Bash $ do
|
||||
getCandidates (T_Annotation _ annotations s) =
|
||||
map forAnnotation annotations ++
|
||||
[Just $ fromShebang s]
|
||||
fromShebang (T_Script _ s t) = shellFor s
|
||||
fromShebang (T_Script _ s t) = executableFromShebang s
|
||||
|
||||
-- Given a string like "/bin/bash" or "/usr/bin/env dash",
|
||||
-- return the shell basename like "bash" or "dash"
|
||||
executableFromShebang :: String -> String
|
||||
executableFromShebang = shellFor
|
||||
where
|
||||
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
|
||||
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
||||
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
||||
@@ -170,16 +217,13 @@ isQuoteFreeNode strict tree t =
|
||||
-- Are any subnodes inherently self-quoting?
|
||||
isQuoteFreeContext t =
|
||||
case t of
|
||||
TC_Noary _ DoubleBracket _ -> return True
|
||||
TC_Nullary _ DoubleBracket _ -> return True
|
||||
TC_Unary _ DoubleBracket _ _ -> return True
|
||||
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment {} -> return True
|
||||
T_Redirecting {} -> return $
|
||||
if strict then False else
|
||||
-- Not true, just a hack to prevent warning about non-expansion refs
|
||||
any (isCommand t) ["local", "declare", "typeset", "export", "trap", "readonly"]
|
||||
T_Redirecting {} -> return False
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
T_CaseExpression {} -> return True
|
||||
@@ -211,6 +255,10 @@ getClosestCommand tree t =
|
||||
getCommand t@(T_Redirecting {}) = return t
|
||||
getCommand _ = Nothing
|
||||
|
||||
getClosestCommandM t = do
|
||||
tree <- asks parentMap
|
||||
return $ getClosestCommand tree t
|
||||
|
||||
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
where
|
||||
go currentId (T_NormalWord id [word]:rest)
|
||||
@@ -227,6 +275,12 @@ getPath tree t = t :
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
|
||||
-- Version of the above taking the map from the current context
|
||||
-- Todo: give this the name "getPath"
|
||||
getPathM t = do
|
||||
map <- asks parentMap
|
||||
return $ getPath map t
|
||||
|
||||
isParentOf tree parent child =
|
||||
elem (getId parent) . map getId $ getPath tree child
|
||||
|
||||
@@ -347,6 +401,7 @@ getModifiedVariables t =
|
||||
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||
|
||||
--Points to 'for' rather than variable
|
||||
T_ForIn id str [] _ -> [(t, t, str, DataString $ SourceExternal)]
|
||||
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||
_ -> []
|
||||
@@ -644,6 +699,10 @@ headOrDefault def _ = def
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
-- Run a command if the shell is in the given list
|
||||
whenShell l c = do
|
||||
shell <- asks shellType
|
||||
when (shell `elem` l ) c
|
||||
|
||||
|
||||
filterByAnnotation token =
|
||||
|
@@ -158,5 +158,25 @@ prop_sourceDirectiveDoesntFollowFile =
|
||||
[("foo", "source bar"), ("bar", "baz=3")]
|
||||
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
|
||||
|
||||
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
|
||||
prop_filewideAnnotation1 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
|
||||
prop_filewideAnnotation2 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation3 = null $
|
||||
check "#!/bin/sh\n#unerlated\n# shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation4 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||
prop_filewideAnnotation5 = null $
|
||||
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation6 = null $
|
||||
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
|
||||
prop_filewideAnnotation7 = null $
|
||||
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
|
||||
|
||||
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
|
||||
prop_filewideAnnotation8 = null $
|
||||
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
@@ -21,7 +21,7 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
|
||||
-- This module contains checks that examine specific commands by name.
|
||||
module ShellCheck.Checks.Commands (runChecks
|
||||
module ShellCheck.Checks.Commands (checker
|
||||
, ShellCheck.Checks.Commands.runTests
|
||||
) where
|
||||
|
||||
@@ -34,8 +34,7 @@ import ShellCheck.Parser
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Reader
|
||||
import Control.Monad.Writer
|
||||
import Control.Monad.RWS
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
@@ -49,22 +48,10 @@ data CommandName = Exactly String | Basename String
|
||||
data CommandCheck =
|
||||
CommandCheck CommandName (Token -> Analysis)
|
||||
|
||||
nullCheck :: Token -> Analysis
|
||||
nullCheck _ = return ()
|
||||
|
||||
|
||||
verify :: CommandCheck -> String -> Bool
|
||||
verify f s = producesComments f s == Just True
|
||||
verifyNot f s = producesComments f s == Just False
|
||||
|
||||
producesComments :: CommandCheck -> String -> Maybe Bool
|
||||
producesComments f s = do
|
||||
root <- pScript s
|
||||
return . not . null $ runList (defaultSpec root) [f]
|
||||
|
||||
composeChecks f g t = do
|
||||
f t
|
||||
g t
|
||||
verify f s = producesComments (getChecker [f]) s == Just True
|
||||
verifyNot f s = producesComments (getChecker [f]) s == Just False
|
||||
|
||||
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||
|
||||
@@ -92,13 +79,19 @@ commandChecks = [
|
||||
,checkAliasesExpandEarly
|
||||
,checkUnsetGlobs
|
||||
,checkFindWithoutPath
|
||||
,checkTimeParameters
|
||||
,checkTimedCommand
|
||||
,checkLocalScope
|
||||
,checkDeprecatedTempfile
|
||||
,checkDeprecatedEgrep
|
||||
,checkDeprecatedFgrep
|
||||
]
|
||||
|
||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||
buildCommandMap = foldl' addCheck Map.empty
|
||||
where
|
||||
addCheck map (CommandCheck name function) =
|
||||
Map.insertWith' composeChecks name function map
|
||||
Map.insertWith' composeAnalyzers name function map
|
||||
|
||||
|
||||
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||
@@ -116,15 +109,17 @@ checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ d
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
checkCommand _ _ = return ()
|
||||
|
||||
runList spec list = notes
|
||||
where
|
||||
root = asScript spec
|
||||
params = makeParameters spec
|
||||
notes = execWriter $ runReaderT (doAnalysis (checkCommand map) root) params
|
||||
map = buildCommandMap list
|
||||
getChecker :: [CommandCheck] -> Checker
|
||||
getChecker list = Checker {
|
||||
perScript = const $ return (),
|
||||
perToken = checkCommand map
|
||||
}
|
||||
where
|
||||
map = buildCommandMap list
|
||||
|
||||
runChecks spec = runList spec commandChecks
|
||||
|
||||
checker :: Parameters -> Checker
|
||||
checker params = getChecker commandChecks
|
||||
|
||||
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||
@@ -199,23 +194,27 @@ prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
|
||||
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
|
||||
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
|
||||
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
|
||||
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
|
||||
|
||||
checkGrepRe = CommandCheck (Basename "grep") (f . arguments) where
|
||||
checkGrepRe = CommandCheck (Basename "grep") check where
|
||||
check cmd = f cmd (arguments cmd)
|
||||
-- --regex=*(extglob) doesn't work. Fixme?
|
||||
skippable (Just s) = not ("--regex=" `isPrefixOf` s) && "-" `isPrefixOf` s
|
||||
skippable _ = False
|
||||
f [] = return ()
|
||||
f (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f r
|
||||
f (re:_) = do
|
||||
f _ [] = return ()
|
||||
f cmd (x:r) | skippable (getLiteralStringExt (const $ return "_") x) = f cmd r
|
||||
f cmd (re:_) = do
|
||||
when (isGlob re) $
|
||||
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
|
||||
let string = concat $ oversimplify re
|
||||
if isConfusedGlobRegex string then
|
||||
warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
|
||||
else potentially $ do
|
||||
char <- getSuspiciousRegexWildcard string
|
||||
return $ info (getId re) 2022 $
|
||||
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
|
||||
|
||||
unless (cmd `hasFlag` "F") $ do
|
||||
let string = concat $ oversimplify re
|
||||
if isConfusedGlobRegex string then
|
||||
warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
|
||||
else potentially $ do
|
||||
char <- getSuspiciousRegexWildcard string
|
||||
return $ info (getId re) 2022 $
|
||||
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
|
||||
|
||||
wordStartingWith c =
|
||||
head . filter ([c] `isPrefixOf`) $ candidates
|
||||
@@ -344,7 +343,7 @@ checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
|
||||
|
||||
pattern = [
|
||||
(`elem` ["-exec", "-execdir"]),
|
||||
(`elem` ["sh", "bash", "ksh"]),
|
||||
(`elem` ["sh", "bash", "dash", "ksh"]),
|
||||
(== "-c")
|
||||
]
|
||||
action (id, arg) =
|
||||
@@ -364,7 +363,7 @@ checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
|
||||
then warnFor (list !! (length pattern - 1))
|
||||
else f rest
|
||||
isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
|
||||
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]
|
||||
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0", "-fls", "-fprint", "-fprint0", "-fprintf", "-ls", "-ok", "-okdir", "-printf" ]
|
||||
isParam strs t = fromMaybe False $ do
|
||||
param <- getLiteralString t
|
||||
return $ param `elem` strs
|
||||
@@ -478,6 +477,7 @@ prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
|
||||
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
|
||||
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||
@@ -487,6 +487,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
countFormats string =
|
||||
case string of
|
||||
'%':'%':rest -> countFormats rest
|
||||
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
|
||||
'%':rest -> 1 + countFormats rest
|
||||
_:rest -> countFormats rest
|
||||
[] -> 0
|
||||
@@ -619,5 +620,66 @@ checkFindWithoutPath = CommandCheck (Basename "find") f
|
||||
hasPath [] = False
|
||||
|
||||
|
||||
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10"
|
||||
prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10"
|
||||
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo"
|
||||
prop_checkTimeParameters4 = verifyNot checkTimeParameters "command time -f lol sleep 10"
|
||||
checkTimeParameters = CommandCheck (Exactly "time") f
|
||||
where
|
||||
f (T_SimpleCommand _ _ (cmd:args:_)) =
|
||||
whenShell [Bash, Sh] $
|
||||
let s = concat $ oversimplify args in
|
||||
when ("-" `isPrefixOf` s && s /= "-p") $
|
||||
info (getId cmd) 2023 "The shell may override 'time' as seen in man time(1). Use 'command time ..' for that one."
|
||||
|
||||
f _ = return ()
|
||||
|
||||
prop_checkTimedCommand1 = verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
|
||||
prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
|
||||
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
|
||||
checkTimedCommand = CommandCheck (Exactly "time") f where
|
||||
f (T_SimpleCommand _ _ (c:args@(_:_))) =
|
||||
whenShell [Sh, Dash] $ do
|
||||
let cmd = last args -- "time" is parsed with a command as argument
|
||||
when (isPiped cmd) $
|
||||
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
|
||||
when (isSimple cmd == Just False) $
|
||||
warn (getId cmd) 2177 "'time' is undefined for compound commands, time sh -c instead."
|
||||
f _ = return ()
|
||||
isPiped cmd =
|
||||
case cmd of
|
||||
T_Pipeline _ _ (_:_:_) -> True
|
||||
_ -> False
|
||||
getCommand cmd =
|
||||
case cmd of
|
||||
T_Pipeline _ _ (T_Redirecting _ _ a : _) -> return a
|
||||
_ -> fail ""
|
||||
isSimple cmd = do
|
||||
innerCommand <- getCommand cmd
|
||||
case innerCommand of
|
||||
T_SimpleCommand {} -> return True
|
||||
_ -> return False
|
||||
|
||||
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
|
||||
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
|
||||
checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
||||
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
|
||||
path <- getPathM t
|
||||
unless (any isFunction path) $
|
||||
err (getId t) 2168 "'local' is only valid in functions."
|
||||
|
||||
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||
\t -> warn (getId t) 2186 "tempfile is deprecated. Use mktemp instead."
|
||||
|
||||
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'"
|
||||
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
|
||||
\t -> info (getId t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
|
||||
|
||||
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
|
||||
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
||||
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
388
ShellCheck/Checks/ShellSupport.hs
Normal file
388
ShellCheck/Checks/ShellSupport.hs
Normal file
@@ -0,0 +1,388 @@
|
||||
{-
|
||||
Copyright 2012-2016 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
http://www.vidarholen.net/contents/shellcheck
|
||||
|
||||
ShellCheck is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ShellCheck is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module ShellCheck.Checks.ShellSupport (checker
|
||||
, ShellCheck.Checks.ShellSupport.runTests
|
||||
) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.RWS
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as Map
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
data ForShell = ForShell [Shell] (Token -> Analysis)
|
||||
|
||||
getChecker params list = Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = foldl composeAnalyzers nullCheck $ mapMaybe include list
|
||||
}
|
||||
where
|
||||
shell = shellType params
|
||||
include (ForShell list a) = do
|
||||
guard $ shell `elem` list
|
||||
return a
|
||||
|
||||
checker params = getChecker params checks
|
||||
|
||||
checks = [
|
||||
checkForDecimals
|
||||
,checkBashisms
|
||||
,checkEchoSed
|
||||
,checkBraceExpansionVars
|
||||
,checkMultiDimensionalArrays
|
||||
]
|
||||
|
||||
testChecker (ForShell _ t) =
|
||||
Checker {
|
||||
perScript = nullCheck,
|
||||
perToken = t
|
||||
}
|
||||
verify c s = producesComments (testChecker c) s == Just True
|
||||
verifyNot c s = producesComments (testChecker c) s == Just False
|
||||
|
||||
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))"
|
||||
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar"
|
||||
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
||||
checkForDecimals = ForShell [Sh, Dash, Bash] f
|
||||
where
|
||||
f t@(TA_Expansion id _) = potentially $ do
|
||||
str <- getLiteralString t
|
||||
first <- str !!! 0
|
||||
guard $ isDigit first && '.' `elem` str
|
||||
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
|
||||
f _ = return ()
|
||||
|
||||
|
||||
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
|
||||
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
|
||||
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
|
||||
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
|
||||
prop_checkBashisms5 = verify checkBashisms "source file"
|
||||
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
||||
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
||||
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
||||
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
||||
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
|
||||
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
|
||||
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
|
||||
prop_checkBashisms13= verify checkBashisms "exec -c env"
|
||||
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
|
||||
prop_checkBashisms15= verify checkBashisms "let n++"
|
||||
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
|
||||
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
|
||||
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
|
||||
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
|
||||
prop_checkBashisms20= verify checkBashisms "read -ra foo"
|
||||
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
|
||||
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
|
||||
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
|
||||
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
|
||||
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
|
||||
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
|
||||
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
|
||||
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
|
||||
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
|
||||
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
|
||||
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
|
||||
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
|
||||
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
|
||||
prop_checkBashisms41= verify checkBashisms "echo `<file`"
|
||||
prop_checkBashisms42= verify checkBashisms "trap foo int"
|
||||
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
|
||||
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/dash\necho $LINENO"
|
||||
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
params <- ask
|
||||
kludge params t
|
||||
where
|
||||
-- This code was copy-pasted from Analytics where params was a variable
|
||||
kludge params = bashism
|
||||
where
|
||||
isDash = shellType params == Dash
|
||||
warnMsg id s =
|
||||
if isDash
|
||||
then warn id 2169 $ "In dash, " ++ s ++ " not supported."
|
||||
else warn id 2039 $ "In POSIX sh, " ++ s ++ " undefined."
|
||||
|
||||
bashism (T_ProcSub id _ _) = warnMsg id "process substitution is"
|
||||
bashism (T_Extglob id _ _) = warnMsg id "extglob is"
|
||||
bashism (T_DollarSingleQuoted id _) = warnMsg id "$'..' is"
|
||||
bashism (T_DollarDoubleQuoted id _) = warnMsg id "$\"..\" is"
|
||||
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id "arithmetic for loops are"
|
||||
bashism (T_Arithmetic id _) = warnMsg id "standalone ((..)) is"
|
||||
bashism (T_DollarBracket id _) = warnMsg id "$[..] in place of $((..)) is"
|
||||
bashism (T_SelectIn id _ _ _) = warnMsg id "select loops are"
|
||||
bashism (T_BraceExpansion id _) = warnMsg id "brace expansion is"
|
||||
bashism (T_Condition id DoubleBracket _) = warnMsg id "[[ ]] is"
|
||||
bashism (T_HereString id _) = warnMsg id "here-strings are"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "-nt", "-ef", "\\<", "\\>"] =
|
||||
unless isDash $ warnMsg id $ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket "==" _ _) =
|
||||
warnMsg id "== in place of = is"
|
||||
bashism (TC_Unary id _ "-a" _) =
|
||||
warnMsg id "unary -a in place of -e is"
|
||||
bashism (TA_Unary id op _)
|
||||
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
||||
warnMsg id $ filter (/= '|') op ++ " is"
|
||||
bashism (TA_Binary id "**" _ _) = warnMsg id "exponentials are"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id ">& is"
|
||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id "named file descriptors are"
|
||||
bashism (T_FdRedirect id num _)
|
||||
| all isDigit num && length num > 1 = warnMsg id "FDs outside 0-9 are"
|
||||
bashism (T_Assignment id Append _ _ _) =
|
||||
warnMsg id "+= is"
|
||||
bashism (T_IoFile id _ word) | isNetworked =
|
||||
warnMsg id "/dev/{tcp,udp} is"
|
||||
where
|
||||
file = onlyLiteralString word
|
||||
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||
warnMsg id "^ in place of ! in glob bracket expressions is"
|
||||
|
||||
bashism t@(TA_Expansion id _) | isBashism =
|
||||
warnMsg id $ fromJust str ++ " is"
|
||||
where
|
||||
str = getLiteralString t
|
||||
isBashism = isJust str && isBashVariable (fromJust str)
|
||||
bashism t@(T_DollarBraced id token) = do
|
||||
mapM_ check expansion
|
||||
when (isBashVariable var) $
|
||||
warnMsg id $ var ++ " is"
|
||||
where
|
||||
str = bracedString t
|
||||
var = getBracedReference str
|
||||
check (regex, feature) =
|
||||
when (isJust $ matchRegex regex str) $ warnMsg id feature
|
||||
|
||||
bashism t@(T_Pipe id "|&") =
|
||||
warnMsg id "|& in place of 2>&1 | is"
|
||||
bashism (T_Array id _) =
|
||||
warnMsg id "arrays are"
|
||||
bashism (T_IoFile id _ t) | isGlob t =
|
||||
warnMsg id "redirecting to/from globs is"
|
||||
bashism (T_CoProc id _ _) =
|
||||
warnMsg id "coproc is"
|
||||
|
||||
bashism (T_Function id _ _ str _) | not (isVariableName str) =
|
||||
warnMsg id "naming functions outside [a-zA-Z_][a-zA-Z0-9_]* is"
|
||||
|
||||
bashism (T_DollarExpansion id [x]) | isOnlyRedirection x =
|
||||
warnMsg id "$(<file) to read files is"
|
||||
bashism (T_Backticked id [x]) | isOnlyRedirection x =
|
||||
warnMsg id "`<file` to read files is"
|
||||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "echo" && "-" `isPrefixOf` argString =
|
||||
unless ("--" `isPrefixOf` argString) $ -- echo "-----"
|
||||
if isDash
|
||||
then
|
||||
when (argString /= "-n") $
|
||||
warnMsg (getId arg) "echo flags besides -n"
|
||||
else
|
||||
warnMsg (getId arg) "echo flags are"
|
||||
where argString = concat $ oversimplify arg
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||
warnMsg (getId arg) "exec flags are"
|
||||
bashism t@(T_SimpleCommand id _ _)
|
||||
| t `isCommand` "let" = warnMsg id "'let' is"
|
||||
|
||||
bashism t@(T_SimpleCommand id _ (cmd:rest)) =
|
||||
let name = fromMaybe "" $ getCommandName t
|
||||
flags = getLeadingFlags t
|
||||
in do
|
||||
when (name `elem` unsupportedCommands) $
|
||||
warnMsg id $ "'" ++ name ++ "' is"
|
||||
potentially $ do
|
||||
allowed <- Map.lookup name allowedFlags
|
||||
(word, flag) <- listToMaybe $
|
||||
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
|
||||
|
||||
when (name == "source") $ warnMsg id "'source' in place of '.' is"
|
||||
when (name == "trap") $
|
||||
let
|
||||
check token = potentially $ do
|
||||
str <- getLiteralString token
|
||||
let upper = map toUpper str
|
||||
return $ do
|
||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||
warnMsg (getId token) $ "trapping " ++ str ++ " is"
|
||||
when ("SIG" `isPrefixOf` upper) $
|
||||
warnMsg (getId token)
|
||||
"prefixing signal names with 'SIG' is"
|
||||
when (not isDash && upper /= str) $
|
||||
warnMsg (getId token)
|
||||
"using lower/mixed case for signal names is"
|
||||
in
|
||||
mapM_ check (drop 1 rest)
|
||||
|
||||
when (name == "printf") $ potentially $ do
|
||||
format <- rest !!! 0 -- flags are covered by allowedFlags
|
||||
let literal = onlyLiteralString format
|
||||
guard $ "%q" `isInfixOf` literal
|
||||
return $ warnMsg (getId format) "printf %q is"
|
||||
where
|
||||
unsupportedCommands = [
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
"enable", "mapfile", "readarray", "pushd", "popd", "shopt", "suspend",
|
||||
"typeset"
|
||||
] ++ if not isDash then ["local", "type"] else []
|
||||
allowedFlags = Map.fromList [
|
||||
("read", if isDash then ["r", "p"] else ["r"]),
|
||||
("ulimit", ["f"]),
|
||||
("printf", []),
|
||||
("exec", [])
|
||||
]
|
||||
|
||||
bashism _ = return ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
expansion = let re = mkRegex in [
|
||||
(re $ "^![" ++ varChars ++ "]", "indirect expansion is"),
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "]+:[^-=?+]", "string indexing is"),
|
||||
(re $ "^[" ++ varChars ++ "]+(\\[.*\\])?/", "string replacement is")
|
||||
]
|
||||
bashVars = [
|
||||
"LINENO", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
|
||||
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
|
||||
]
|
||||
bashDynamicVars = [ "RANDOM", "SECONDS" ]
|
||||
dashVars = [ "LINENO" ]
|
||||
isBashVariable var =
|
||||
(var `elem` bashDynamicVars
|
||||
|| var `elem` bashVars && not (isAssigned var))
|
||||
&& not (isDash && var `elem` dashVars)
|
||||
isAssigned var = any f (variableFlow params)
|
||||
where
|
||||
f x = case x of
|
||||
Assignment (_, _, name, _) -> name == var
|
||||
_ -> False
|
||||
|
||||
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
||||
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
||||
checkEchoSed = ForShell [Bash, Ksh] f
|
||||
where
|
||||
f (T_Pipeline id _ [a, b]) =
|
||||
when (acmd == ["echo", "${VAR}"]) $
|
||||
case bcmd of
|
||||
["sed", v] -> checkIn v
|
||||
["sed", "-e", v] -> checkIn v
|
||||
_ -> return ()
|
||||
where
|
||||
-- This should have used backreferences, but TDFA doesn't support them
|
||||
sedRe = mkRegex "^s(.)([^\n]*)g?$"
|
||||
isSimpleSed s = fromMaybe False $ do
|
||||
[first,rest] <- matchRegex sedRe s
|
||||
let delimiters = filter (== head first) rest
|
||||
guard $ length delimiters == 2
|
||||
return True
|
||||
|
||||
acmd = oversimplify a
|
||||
bcmd = oversimplify b
|
||||
checkIn s =
|
||||
when (isSimpleSed s) $
|
||||
style id 2001 "See if you can use ${variable//search/replace} instead."
|
||||
f _ = return ()
|
||||
|
||||
|
||||
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
|
||||
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
|
||||
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
|
||||
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
|
||||
checkBraceExpansionVars = ForShell [Bash] f
|
||||
where
|
||||
f t@(T_BraceExpansion id list) = mapM_ check list
|
||||
where
|
||||
check element =
|
||||
when (any (`isInfixOf` toString element) ["$..", "..$"]) $ do
|
||||
c <- isEvaled element
|
||||
if c
|
||||
then style id 2175 "Quote this invalid brace expansion since it should be passed literally to eval."
|
||||
else warn id 2051 "Bash doesn't support variables in brace range expansions."
|
||||
f _ = return ()
|
||||
|
||||
literalExt t =
|
||||
case t of
|
||||
T_DollarBraced {} -> return "$"
|
||||
T_DollarExpansion {} -> return "$"
|
||||
T_DollarArithmetic {} -> return "$"
|
||||
otherwise -> return "-"
|
||||
toString t = fromJust $ getLiteralStringExt literalExt t
|
||||
isEvaled t = do
|
||||
cmd <- getClosestCommandM t
|
||||
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
|
||||
|
||||
|
||||
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3"
|
||||
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3"
|
||||
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
|
||||
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
|
||||
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
|
||||
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
|
||||
checkMultiDimensionalArrays = ForShell [Bash] f
|
||||
where
|
||||
f token =
|
||||
case token of
|
||||
T_Assignment _ _ name (first:second:_) _ -> about second
|
||||
T_IndexedElement _ (first:second:_) _ -> about second
|
||||
T_DollarBraced {} ->
|
||||
when (isMultiDim token) $ about token
|
||||
_ -> return ()
|
||||
about t = warn (getId t) 2180 "Bash does not support multidimensional arrays. Use 1D or associative arrays."
|
||||
|
||||
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
|
||||
isMultiDim t = getBracedModifier (bracedString t) `matches` re
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -33,6 +33,9 @@ internalVariables = [
|
||||
-- Other
|
||||
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||
"HOSTNAME", "KRB5CCNAME", "XAUTHORITY"
|
||||
|
||||
-- Ksh
|
||||
, ".sh.version"
|
||||
]
|
||||
|
||||
variablesWithoutSpaces = [
|
||||
@@ -82,12 +85,24 @@ sampleWords = [
|
||||
"zulu"
|
||||
]
|
||||
|
||||
binaryTestOps = [
|
||||
"-nt", "-ot", "-ef", "==", "!=", "<=", ">=", "-eq", "-ne", "-lt", "-le",
|
||||
"-gt", "-ge", "=~", ">", "<", "=", "\\<", "\\>", "\\<=", "\\>="
|
||||
]
|
||||
|
||||
unaryTestOps = [
|
||||
"!", "-a", "-b", "-c", "-d", "-e", "-f", "-g", "-h", "-L", "-k", "-p",
|
||||
"-r", "-s", "-S", "-t", "-u", "-w", "-x", "-O", "-G", "-N", "-z", "-n",
|
||||
"-o", "-v", "-R"
|
||||
]
|
||||
|
||||
shellForExecutable :: String -> Maybe Shell
|
||||
shellForExecutable name =
|
||||
case name of
|
||||
"sh" -> return Sh
|
||||
"bash" -> return Bash
|
||||
"dash" -> return Dash
|
||||
"ash" -> return Dash -- There's also a warning for this.
|
||||
"ksh" -> return Ksh
|
||||
"ksh88" -> return Ksh
|
||||
"ksh93" -> return Ksh
|
||||
|
@@ -58,17 +58,18 @@ linefeed = do
|
||||
c <- char '\n'
|
||||
readPendingHereDocs
|
||||
return c
|
||||
singleQuote = char '\'' <|> unicodeSingleQuote
|
||||
doubleQuote = char '"' <|> unicodeDoubleQuote
|
||||
singleQuote = char '\''
|
||||
doubleQuote = char '"'
|
||||
variableStart = upper <|> lower <|> oneOf "_"
|
||||
variableChars = upper <|> lower <|> digit <|> oneOf "_"
|
||||
functionChars = variableChars <|> oneOf ":+-.?"
|
||||
specialVariable = oneOf "@*#?-$!"
|
||||
paramSubSpecialChars = oneOf "/:+-=%"
|
||||
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
|
||||
quotable = almostSpace <|> unicodeDoubleQuote <|> oneOf quotableChars
|
||||
quotable = almostSpace <|> oneOf quotableChars
|
||||
bracedQuotable = oneOf "}\"$`'"
|
||||
doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars
|
||||
doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars
|
||||
doubleQuotableChars = "\"$`"
|
||||
doubleQuotable = oneOf doubleQuotableChars
|
||||
whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed
|
||||
linewhitespace = oneOf " \t" <|> almostSpace
|
||||
|
||||
@@ -77,7 +78,8 @@ suspectCharAfterQuotes = variableChars <|> char '%'
|
||||
extglobStartChars = "?*@!+"
|
||||
extglobStart = oneOf extglobStartChars
|
||||
|
||||
unicodeDoubleQuoteChars = "\x201C\x201D\x2033\x2036"
|
||||
unicodeDoubleQuotes = "\x201C\x201D\x2033\x2036"
|
||||
unicodeSingleQuotes = "\x2018\x2019"
|
||||
|
||||
prop_spacing = isOk spacing " \\\n # Comment"
|
||||
spacing = do
|
||||
@@ -106,17 +108,12 @@ allspacingOrFail = do
|
||||
s <- allspacing
|
||||
when (null s) $ fail "Expected whitespace"
|
||||
|
||||
unicodeDoubleQuote = do
|
||||
readUnicodeQuote = do
|
||||
pos <- getPosition
|
||||
oneOf unicodeDoubleQuoteChars
|
||||
parseProblemAt pos WarningC 1015 "This is a unicode double quote. Delete and retype it."
|
||||
return '"'
|
||||
|
||||
unicodeSingleQuote = do
|
||||
pos <- getPosition
|
||||
char '\x2018' <|> char '\x2019'
|
||||
parseProblemAt pos WarningC 1016 "This is a unicode single quote. Delete and retype it."
|
||||
return '"'
|
||||
c <- oneOf (unicodeSingleQuotes ++ unicodeDoubleQuotes)
|
||||
parseProblemAt pos WarningC 1110 "This is a unicode quote. Delete and retype it (or quote to make literal)."
|
||||
id <- getNextIdAt pos
|
||||
return $ T_Literal id [c]
|
||||
|
||||
carriageReturn = do
|
||||
parseNote ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
|
||||
@@ -332,6 +329,13 @@ parseProblemAtWithEnd start end level code msg = do
|
||||
|
||||
parseProblemAt pos = parseProblemAtWithEnd pos pos
|
||||
|
||||
parseProblemAtId :: Monad m => Id -> Severity -> Integer -> String -> SCParser m ()
|
||||
parseProblemAtId id level code msg = do
|
||||
map <- getMap
|
||||
let pos = Map.findWithDefault
|
||||
(error "Internal error (no position for id). Please report.") id map
|
||||
parseProblemAt pos level code msg
|
||||
|
||||
-- Store non-parse problems inside
|
||||
|
||||
parseNote c l a = do
|
||||
@@ -527,7 +531,7 @@ readConditionContents single =
|
||||
condSpacing requiresSpacing
|
||||
return x
|
||||
|
||||
readCondNoaryOrBinary = do
|
||||
readCondNullaryOrBinary = do
|
||||
id <- getNextId
|
||||
x <- readCondWord `attempting` (do
|
||||
pos <- getPosition
|
||||
@@ -544,7 +548,16 @@ readConditionContents single =
|
||||
then readRegex
|
||||
else readCondWord <|> (parseProblemAt pos ErrorC 1027 "Expected another argument for this operator." >> mzero)
|
||||
return (x `op` y)
|
||||
) <|> return (TC_Noary id typ x)
|
||||
) <|> ( do
|
||||
checkTrailingOp x
|
||||
return $ TC_Nullary id typ x
|
||||
)
|
||||
|
||||
checkTrailingOp x = fromMaybe (return ()) $ do
|
||||
(T_Literal id str) <- getTrailingUnquotedLiteral x
|
||||
trailingOp <- listToMaybe (filter (`isSuffixOf` str) binaryTestOps)
|
||||
return $ parseProblemAtId id ErrorC 1108 $
|
||||
"You need a space before and after the " ++ trailingOp ++ " ."
|
||||
|
||||
readCondGroup = do
|
||||
id <- getNextId
|
||||
@@ -621,7 +634,7 @@ readConditionContents single =
|
||||
return $ TC_Unary id typ "!" expr
|
||||
|
||||
readCondExpr =
|
||||
readCondGroup <|> readCondUnaryExp <|> readCondNoaryOrBinary
|
||||
readCondGroup <|> readCondUnaryExp <|> readCondNullaryOrBinary
|
||||
|
||||
readCondOr = chainl1 readCondAnd readCondAndOp
|
||||
readCondAnd = chainl1 readCondTerm readCondOrOp
|
||||
@@ -930,6 +943,9 @@ prop_readNormalWord6 = isOk readNormalWord "foo/{}"
|
||||
prop_readNormalWord7 = isOk readNormalWord "foo\\\nbar"
|
||||
prop_readNormalWord8 = isWarning readSubshell "(foo\\ \nbar)"
|
||||
prop_readNormalWord9 = isOk readSubshell "(foo\\ ;\nbar)"
|
||||
prop_readNormalWord10 = isWarning readNormalWord "\x201Chello\x201D"
|
||||
prop_readNormalWord11 = isWarning readNormalWord "\x2018hello\x2019"
|
||||
prop_readNormalWord12 = isWarning readNormalWord "hello\x2018"
|
||||
readNormalWord = readNormalishWord ""
|
||||
|
||||
readNormalishWord end = do
|
||||
@@ -969,6 +985,7 @@ readNormalWordPart end = do
|
||||
readBraced,
|
||||
readUnquotedBackTicked,
|
||||
readProcSub,
|
||||
readUnicodeQuote,
|
||||
readNormalLiteral end,
|
||||
readLiteralCurlyBraces
|
||||
]
|
||||
@@ -1003,13 +1020,19 @@ readDollarBracedWord = do
|
||||
list <- many readDollarBracedPart
|
||||
return $ T_NormalWord id list
|
||||
|
||||
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|> readExtglob <|> readNormalDollar <|> readUnquotedBackTicked <|> readDollarBracedLiteral
|
||||
readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
|
||||
readParamSubSpecialChar <|> readExtglob <|> readNormalDollar <|>
|
||||
readUnquotedBackTicked <|> readDollarBracedLiteral
|
||||
|
||||
readDollarBracedLiteral = do
|
||||
id <- getNextId
|
||||
vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable
|
||||
return $ T_Literal id $ concat vars
|
||||
|
||||
readParamSubSpecialChar = do
|
||||
id <- getNextId
|
||||
T_ParamSubSpecialChar id <$> many1 paramSubSpecialChars
|
||||
|
||||
prop_readProcSub1 = isOk readProcSub "<(echo test | wc -l)"
|
||||
prop_readProcSub2 = isOk readProcSub "<( if true; then true; fi )"
|
||||
prop_readProcSub3 = isOk readProcSub "<( # nothing here \n)"
|
||||
@@ -1026,15 +1049,16 @@ readProcSub = called "process substitution" $ do
|
||||
|
||||
prop_readSingleQuoted = isOk readSingleQuoted "'foo bar'"
|
||||
prop_readSingleQuoted2 = isWarning readSingleQuoted "'foo bar\\'"
|
||||
prop_readsingleQuoted3 = isWarning readSingleQuoted "\x2018hello\x2019"
|
||||
prop_readSingleQuoted4 = isWarning readNormalWord "'it's"
|
||||
prop_readSingleQuoted5 = isWarning readSimpleCommand "foo='bar\ncow 'arg"
|
||||
prop_readSingleQuoted6 = isOk readSimpleCommand "foo='bar cow 'arg"
|
||||
prop_readSingleQuoted7 = isOk readSingleQuoted "'foo\x201C\&bar'"
|
||||
prop_readSingleQuoted8 = isWarning readSingleQuoted "'foo\x2018\&bar'"
|
||||
readSingleQuoted = called "single quoted string" $ do
|
||||
id <- getNextId
|
||||
startPos <- getPosition
|
||||
singleQuote
|
||||
s <- readSingleQuotedPart `reluctantlyTill` singleQuote
|
||||
s <- many readSingleQuotedPart
|
||||
let string = concat s
|
||||
endPos <- getPosition
|
||||
singleQuote <|> fail "Expected end of single quoted string"
|
||||
@@ -1059,7 +1083,15 @@ readSingleQuotedLiteral = do
|
||||
|
||||
readSingleQuotedPart =
|
||||
readSingleEscaped
|
||||
<|> many1 (noneOf "'\\\x2018\x2019")
|
||||
<|> many1 (noneOf $ "'\\" ++ unicodeSingleQuotes)
|
||||
<|> readUnicodeQuote
|
||||
where
|
||||
readUnicodeQuote = do
|
||||
pos <- getPosition
|
||||
x <- oneOf unicodeSingleQuotes
|
||||
parseProblemAt pos WarningC 1112
|
||||
"This is a unicode quote. Delete and retype it (or ignore/doublequote for literal)."
|
||||
return [x]
|
||||
|
||||
|
||||
prop_readBackTicked = isOk (readBackTicked False) "`ls *.mp3`"
|
||||
@@ -1135,11 +1167,12 @@ parseForgettingContext alsoOnSuccess parser = do
|
||||
|
||||
prop_readDoubleQuoted = isOk readDoubleQuoted "\"Hello $FOO\""
|
||||
prop_readDoubleQuoted2 = isOk readDoubleQuoted "\"$'\""
|
||||
prop_readDoubleQuoted3 = isWarning readDoubleQuoted "\x201Chello\x201D"
|
||||
prop_readDoubleQuoted3 = isOk readDoubleQuoted "\"\x2018hello\x2019\""
|
||||
prop_readDoubleQuoted4 = isWarning readSimpleCommand "\"foo\nbar\"foo"
|
||||
prop_readDoubleQuoted5 = isOk readSimpleCommand "lol \"foo\nbar\" etc"
|
||||
prop_readDoubleQuoted6 = isOk readSimpleCommand "echo \"${ ls; }\""
|
||||
prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\""
|
||||
prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\""
|
||||
readDoubleQuoted = called "double quoted string" $ do
|
||||
id <- getNextId
|
||||
startPos <- getPosition
|
||||
@@ -1164,7 +1197,15 @@ 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 <|> readQuotedBackTicked
|
||||
doubleQuotedPart = readDoubleLiteral <|> readDoubleQuotedDollar <|> readQuotedBackTicked <|> readUnicodeQuote
|
||||
where
|
||||
readUnicodeQuote = do
|
||||
pos <- getPosition
|
||||
id <- getNextId
|
||||
c <- oneOf unicodeDoubleQuotes
|
||||
parseProblemAt pos WarningC 1111
|
||||
"This is a unicode quote. Delete and retype it (or ignore/singlequote for literal)."
|
||||
return $ T_Literal id [c]
|
||||
|
||||
readDoubleQuotedLiteral = do
|
||||
doubleQuote
|
||||
@@ -1178,7 +1219,7 @@ readDoubleLiteral = do
|
||||
return $ T_Literal id (concat s)
|
||||
|
||||
readDoubleLiteralPart = do
|
||||
x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars)))
|
||||
x <- many1 (readDoubleEscaped <|> many1 (noneOf ('\\':doubleQuotableChars ++ unicodeDoubleQuotes)))
|
||||
return $ concat x
|
||||
|
||||
readNormalLiteral end = do
|
||||
@@ -1220,8 +1261,15 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
|
||||
c <- extglobStart <|> char '['
|
||||
return $ T_Literal id [c]
|
||||
|
||||
readNormalLiteralPart end =
|
||||
readNormalEscaped <|> many1 (noneOf (end ++ quotableChars ++ extglobStartChars ++ "[{}"))
|
||||
readNormalLiteralPart customEnd =
|
||||
readNormalEscaped <|>
|
||||
many1 (noneOf (customEnd ++ standardEnd))
|
||||
where
|
||||
standardEnd = "[{}"
|
||||
++ quotableChars
|
||||
++ extglobStartChars
|
||||
++ unicodeDoubleQuotes
|
||||
++ unicodeSingleQuotes
|
||||
|
||||
readNormalEscaped = called "escaped char" $ do
|
||||
pos <- getPosition
|
||||
@@ -1286,19 +1334,16 @@ readExtglobPart = do
|
||||
|
||||
|
||||
readSingleEscaped = do
|
||||
pos <- getPosition
|
||||
s <- backslash
|
||||
let attempt level code p msg = do { try $ parseNote level code msg; x <- p; return [s,x]; }
|
||||
x <- lookAhead anyChar
|
||||
|
||||
do {
|
||||
x <- lookAhead singleQuote;
|
||||
parseProblem InfoC 1003 "Are you trying to escape that single quote? echo 'You'\\''re doing it wrong'.";
|
||||
return [s];
|
||||
}
|
||||
<|> attempt InfoC 1004 linefeed "You don't break lines with \\ in single quotes, it results in literal backslash-linefeed."
|
||||
<|> do
|
||||
x <- anyChar
|
||||
return [s,x]
|
||||
case x of
|
||||
'\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\''s done'.";
|
||||
'\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line."
|
||||
_ -> return ()
|
||||
|
||||
return [s]
|
||||
|
||||
readDoubleEscaped = do
|
||||
bs <- backslash
|
||||
@@ -1359,19 +1404,29 @@ readBraced = try braceExpansion
|
||||
braceLiteral =
|
||||
T_Literal `withParser` readGenericLiteral1 (oneOf "{}\"$'," <|> whitespace)
|
||||
|
||||
readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
|
||||
readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
|
||||
ensureDollar =
|
||||
-- The grammar should have been designed along the lines of readDollarExpr = char '$' >> stuff, but
|
||||
-- instead, each subunit parses its own $. This results in ~7 1-3 char lookaheads instead of one 1-char.
|
||||
-- Instead of optimizing the grammar, here's a green cut that decreases shellcheck runtime by 10%:
|
||||
lookAhead $ char '$'
|
||||
|
||||
readNormalDollar = do
|
||||
ensureDollar
|
||||
readDollarExp <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
|
||||
readDoubleQuotedDollar = do
|
||||
ensureDollar
|
||||
readDollarExp <|> readDollarLonely
|
||||
|
||||
|
||||
prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))"
|
||||
prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)"
|
||||
prop_readDollarExpression3 = isWarning readDollarExpression "$((\"$@\" &); foo;)"
|
||||
readDollarExpression :: Monad m => SCParser m Token
|
||||
readDollarExpression = do
|
||||
-- The grammar should have been designed along the lines of readDollarExpr = char '$' >> stuff, but
|
||||
-- instead, each subunit parses its own $. This results in ~7 1-3 char lookaheads instead of one 1-char.
|
||||
-- Instead of optimizing the grammar, here's a green cut that decreases shellcheck runtime by 10%:
|
||||
lookAhead $ char '$'
|
||||
arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
|
||||
ensureDollar
|
||||
readDollarExp
|
||||
|
||||
readDollarExp = arithmetic <|> readDollarExpansion <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarVariable
|
||||
where
|
||||
arithmetic = readAmbiguous "$((" readDollarArithmetic readDollarExpansion (\pos ->
|
||||
parseNoteAt pos WarningC 1102 "Shells disambiguate $(( differently or not at all. For $(command substition), add space after $( . For $((arithmetics)), fix parsing errors.")
|
||||
@@ -1527,40 +1582,32 @@ prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo"
|
||||
prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"
|
||||
prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
|
||||
prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
|
||||
prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"
|
||||
readHereDoc = called "here document" $ do
|
||||
fid <- getNextId
|
||||
pos <- getPosition
|
||||
try $ string "<<"
|
||||
dashed <- (char '-' >> return Dashed) <|> return Undashed
|
||||
tokenPosition <- getPosition
|
||||
sp <- spacing
|
||||
optional $ do
|
||||
try . lookAhead $ char '('
|
||||
let message = "Shells are space sensitive. Use '< <(cmd)', not '<<" ++ sp ++ "(cmd)'."
|
||||
parseProblemAt pos ErrorC 1038 message
|
||||
hid <- getNextId
|
||||
(quoted, endToken) <-
|
||||
liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral
|
||||
<|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral
|
||||
<|> (readToken >>= (\x -> return (Unquoted, x)))
|
||||
(quoted, endToken) <- readToken
|
||||
|
||||
-- add empty tokens for now, read the rest in readPendingHereDocs
|
||||
let doc = T_HereDoc hid dashed quoted endToken []
|
||||
addPendingHereDoc doc
|
||||
return doc
|
||||
where
|
||||
stripLiteral (T_Literal _ x) = x
|
||||
stripLiteral (T_SingleQuoted _ x) = x
|
||||
|
||||
readToken =
|
||||
liftM concat $ many1 (escaped <|> quoted <|> normal)
|
||||
where
|
||||
quoted = liftM stripLiteral readDoubleQuotedLiteral <|> readSingleQuotedLiteral
|
||||
normal = anyChar `reluctantlyTill1` (whitespace <|> oneOf "<>;&)'\"\\")
|
||||
escaped = do -- surely the user must be doing something wrong at this point
|
||||
char '\\'
|
||||
c <- anyChar
|
||||
return [c]
|
||||
quotes = "\"'\\"
|
||||
-- Fun fact: bash considers << foo"" quoted, but not << <("foo").
|
||||
-- Instead of replicating this, just read a token and strip quotes.
|
||||
readToken = do
|
||||
str <- readStringForParser readNormalWord
|
||||
return (if any (`elem` quotes) str then Quoted else Unquoted,
|
||||
filter (not . (`elem` quotes)) str)
|
||||
|
||||
|
||||
readPendingHereDocs = do
|
||||
@@ -1674,11 +1721,19 @@ readLineBreak = optional readNewlineList
|
||||
|
||||
prop_readSeparator1 = isWarning readScript "a &; b"
|
||||
prop_readSeparator2 = isOk readScript "a & b"
|
||||
prop_readSeparator3 = isWarning readScript "a & b"
|
||||
prop_readSeparator4 = isWarning readScript "a > file; b"
|
||||
readSeparatorOp = do
|
||||
notFollowedBy2 (void g_AND_IF <|> void readCaseSeparator)
|
||||
notFollowedBy2 (string "&>")
|
||||
f <- try (do
|
||||
pos <- getPosition
|
||||
char '&'
|
||||
optional $ do
|
||||
s <- lookAhead . choice . map (try . string) $
|
||||
["amp;", "gt;", "lt;"]
|
||||
parseProblemAt pos ErrorC 1109 "This is an unquoted HTML entity. Replace with corresponding character."
|
||||
|
||||
spacing
|
||||
pos <- getPosition
|
||||
char ';'
|
||||
@@ -2273,7 +2328,7 @@ readCompoundListOrEmpty = do
|
||||
|
||||
readCmdPrefix = many1 (readIoRedirect <|> readAssignmentWord)
|
||||
readCmdSuffix = many1 (readIoRedirect <|> readCmdWord)
|
||||
readModifierSuffix = many1 (readIoRedirect <|> readAssignmentWord <|> readCmdWord)
|
||||
readModifierSuffix = many1 (readIoRedirect <|> readWellFormedAssignment <|> readCmdWord)
|
||||
readTimeSuffix = do
|
||||
flags <- many readFlag
|
||||
pipeline <- readPipeline
|
||||
@@ -2339,12 +2394,16 @@ 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'"
|
||||
readAssignmentWord = try $ do
|
||||
readAssignmentWord = readAssignmentWordExt True
|
||||
readWellFormedAssignment = readAssignmentWordExt False
|
||||
readAssignmentWordExt lenient = try $ do
|
||||
id <- getNextId
|
||||
pos <- getPosition
|
||||
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")
|
||||
when lenient $
|
||||
optional (char '$' >> parseNote ErrorC 1066 "Don't use $ on the left side of assignments.")
|
||||
variable <- readVariableName
|
||||
optional (readNormalDollar >> parseNoteAt pos ErrorC
|
||||
when lenient $
|
||||
optional (readNormalDollar >> parseNoteAt pos ErrorC
|
||||
1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")
|
||||
indices <- many readArrayIndex
|
||||
hasLeftSpace <- liftM (not . null) spacing
|
||||
@@ -2563,6 +2622,7 @@ readScriptFile = do
|
||||
verifyShell pos (getShell sb)
|
||||
if isValidShell (getShell sb) /= Just False
|
||||
then do
|
||||
allspacing
|
||||
annotationId <- getNextId
|
||||
annotations <- readAnnotations
|
||||
commands <- withAnnotations annotations readCompoundListOrEmpty
|
||||
@@ -2587,8 +2647,8 @@ readScriptFile = do
|
||||
verifyShell pos s =
|
||||
case isValidShell s of
|
||||
Just True -> return ()
|
||||
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/ksh scripts. Sorry!"
|
||||
Nothing -> parseProblemAt pos InfoC 1008 "This shebang was unrecognized. Note that ShellCheck only handles sh/bash/ksh."
|
||||
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!"
|
||||
Nothing -> parseProblemAt pos InfoC 1008 "This shebang was unrecognized. Note that ShellCheck only handles sh/bash/dash/ksh."
|
||||
|
||||
isValidShell s =
|
||||
let good = s == "" || any (`isPrefixOf` s) goodShells
|
||||
|
10
nextnumber
Executable file
10
nextnumber
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# TODO: Find a less trashy way to get the next available error code
|
||||
|
||||
shopt -s globstar
|
||||
|
||||
for i in 1 2
|
||||
do
|
||||
last=$(grep -hv "^prop" **/*.hs | grep -Ewo "$i[0-9]{3}" | sort -n | tail -n 1)
|
||||
echo "Next ${i}xxx: $((last+1))"
|
||||
done
|
@@ -9,6 +9,7 @@
|
||||
,ShellCheck.Parser.runTests
|
||||
,ShellCheck.Checker.runTests
|
||||
,ShellCheck.Checks.Commands.runTests
|
||||
,ShellCheck.Checks.ShellSupport.runTests
|
||||
,ShellCheck.AnalyzerLib.runTests
|
||||
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
|
||||
if [[ $var == *$'\nTrue'* ]]
|
||||
|
@@ -175,6 +175,15 @@ ShellCheck uses the follow exit codes:
|
||||
+ 3: ShellCheck was invoked with bad syntax (e.g. unknown flag).
|
||||
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
|
||||
|
||||
# LOCALE
|
||||
This version of ShellCheck is only available in English. All files are
|
||||
leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid
|
||||
sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
|
||||
locales where encoding is unspecified (such as the `C` locale).
|
||||
|
||||
Windows users seeing `commitBuffer: invalid argument (invalid character)`
|
||||
should set their terminal to use UTF-8 with `chcp 65001`.
|
||||
|
||||
# AUTHOR
|
||||
ShellCheck is written and maintained by Vidar Holen.
|
||||
|
||||
|
@@ -31,6 +31,7 @@ import qualified ShellCheck.Formatter.TTY
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
import Data.Functor
|
||||
import Data.Either
|
||||
@@ -288,14 +289,47 @@ ioInterface options files = do
|
||||
fallback path _ = return path
|
||||
|
||||
inputFile file = do
|
||||
contents <-
|
||||
handle <-
|
||||
if file == "-"
|
||||
then getContents
|
||||
else readFile file
|
||||
then return stdin
|
||||
else openBinaryFile file ReadMode
|
||||
|
||||
hSetBinaryMode handle True
|
||||
contents <- decodeString <$> hGetContents handle -- closes handle
|
||||
|
||||
seq (length contents) $
|
||||
return contents
|
||||
|
||||
-- Decode a char8 string into a utf8 string, with fallback on
|
||||
-- ISO-8859-1. This avoids depending on additional libraries.
|
||||
decodeString = decode
|
||||
where
|
||||
decode [] = []
|
||||
decode (c:rest) | isAscii c = c : decode rest
|
||||
decode (c:rest) =
|
||||
let num = (fromIntegral $ ord c) :: Int
|
||||
next = case num of
|
||||
_ | num >= 0xF8 -> Nothing
|
||||
| num >= 0xF0 -> construct (num .&. 0x07) 3 rest
|
||||
| num >= 0xE0 -> construct (num .&. 0x0F) 2 rest
|
||||
| num >= 0xC0 -> construct (num .&. 0x1F) 1 rest
|
||||
| True -> Nothing
|
||||
in
|
||||
case next of
|
||||
Just (n, remainder) -> chr n : decode remainder
|
||||
Nothing -> c : decode rest
|
||||
|
||||
construct x 0 rest = do
|
||||
guard $ x <= 0x10FFFF
|
||||
return (x, rest)
|
||||
construct x n (c:rest) =
|
||||
let num = (fromIntegral $ ord c) :: Int in
|
||||
if num >= 0x80 && num <= 0xBF
|
||||
then construct ((x `shiftL` 6) .|. (num .&. 0x3f)) (n-1) rest
|
||||
else Nothing
|
||||
construct _ _ _ = Nothing
|
||||
|
||||
|
||||
verifyFiles files =
|
||||
when (null files) $ do
|
||||
printErr "No files specified.\n"
|
||||
|
@@ -7,12 +7,14 @@ import qualified ShellCheck.Analytics
|
||||
import qualified ShellCheck.AnalyzerLib
|
||||
import qualified ShellCheck.Parser
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
|
||||
main = do
|
||||
putStrLn "Running ShellCheck tests..."
|
||||
results <- sequence [
|
||||
ShellCheck.Checker.runTests,
|
||||
ShellCheck.Checks.Commands.runTests,
|
||||
ShellCheck.Checks.ShellSupport.runTests,
|
||||
ShellCheck.Analytics.runTests,
|
||||
ShellCheck.AnalyzerLib.runTests,
|
||||
ShellCheck.Parser.runTests
|
||||
|
Reference in New Issue
Block a user