mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 00:39:19 +08:00
Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5efb724a3e | ||
|
619b6c42f3 | ||
|
88c56ecd53 | ||
|
6b62b5bf7e | ||
|
8672af29ef | ||
|
1a8e34bfea | ||
|
b0dae063bf | ||
|
4fc4899803 | ||
|
cd4896192c | ||
|
868d53af95 | ||
|
6a4b86cbea | ||
|
fe2398edc9 | ||
|
3a7dc86de1 | ||
|
1c0ec9c6f6 | ||
|
84110dbef4 | ||
|
1a7e98beaf | ||
|
a5d5831a2e | ||
|
47201822f9 | ||
|
32689ef5eb | ||
|
87481dce25 | ||
|
a90b6d14b3 | ||
|
5a46eeb09a | ||
|
47a7065a7a | ||
|
dbafbb3b3b | ||
|
13a2070a32 | ||
|
fa4874c044 | ||
|
6a71ff6f46 | ||
|
36263fb3f5 | ||
|
6dc419bbf5 | ||
|
7af3470a91 | ||
|
42f7479fb8 | ||
|
50084c06c5 | ||
|
e3bef9dc97 | ||
|
6c1abb2dee | ||
|
43c26061b9 | ||
|
07fd5724b8 | ||
|
eb2472ada8 | ||
|
3e5ecaa262 | ||
|
e1cec6c5d3 | ||
|
eaa319ec57 | ||
|
717b5e91f5 | ||
|
7f5f5b7fb5 | ||
|
856d57f7d8 | ||
|
c45e9d4878 | ||
|
89c6f6c800 | ||
|
85e69f86eb | ||
|
47fd16b8e8 |
21
.travis.yml
Normal file
21
.travis.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
sudo: required
|
||||
|
||||
language: sh
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- export DOCKER_REPO=koalaman/shellcheck
|
||||
- |-
|
||||
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || echo $TRAVIS_BRANCH)
|
||||
|
||||
script:
|
||||
- docker build -t builder -f Dockerfile_builder .
|
||||
- docker run --rm -it -v $(pwd):/mnt builder
|
||||
- docker build -t $DOCKER_REPO:$TAG .
|
||||
|
||||
after_success:
|
||||
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
- |-
|
||||
[ "$TRAVIS_BRANCH" == "master" ] && docker push $DOCKER_REPO:$TAG
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM alpine:latest
|
||||
|
||||
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
|
||||
|
||||
COPY package/bin/shellcheck /usr/local/bin/
|
||||
COPY package/lib/ /usr/local/lib/
|
||||
|
||||
RUN ldconfig /usr/local/lib
|
||||
|
||||
WORKDIR /mnt
|
||||
ENTRYPOINT ["shellcheck"]
|
54
Dockerfile_builder
Normal file
54
Dockerfile_builder
Normal file
@@ -0,0 +1,54 @@
|
||||
FROM mitchty/alpine-ghc:latest
|
||||
|
||||
MAINTAINER Nikyle Nguyen <NLKNguyen@MSN.com>
|
||||
|
||||
RUN apk add --no-cache build-base
|
||||
|
||||
RUN mkdir -p /usr/src/shellcheck
|
||||
WORKDIR /usr/src/shellcheck
|
||||
|
||||
# # ------------------------------------------------------------
|
||||
# # Build & Test
|
||||
# # ------------------------------------------------------------
|
||||
|
||||
# Obtain the dependencies first, which are less likely to change, in order to reduce
|
||||
# subsequent build time by leveraging image cache. This benefits developers when they
|
||||
# build their code with this image locally. In case of Travis CI, this doesn't help
|
||||
# reduce building time because Travis CI doesn't use cache.
|
||||
COPY ShellCheck.cabal .
|
||||
RUN cabal update && cabal install --only-dependencies
|
||||
|
||||
# Copy the rest of the source files, including ShellCheck.cabal again but doesn't matter
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN cabal install
|
||||
|
||||
# Test
|
||||
RUN cabal test
|
||||
|
||||
# # ------------------------------------------------------------
|
||||
# # Set PATH
|
||||
# # ------------------------------------------------------------
|
||||
|
||||
# Add runtime path to easily reach the executable file. This only exists during build.
|
||||
ENV PATH "/root/.cabal/bin:$PATH"
|
||||
|
||||
# Make it permanent for someone who login to the container of this image
|
||||
RUN echo "export PATH=${PATH}" >> /etc/profile
|
||||
|
||||
# # ------------------------------------------------------------
|
||||
# # Extract Binaries
|
||||
# # ------------------------------------------------------------
|
||||
|
||||
# Get shellcheck binary
|
||||
RUN mkdir -p /package/bin/
|
||||
RUN cp $(which shellcheck) /package/bin/
|
||||
|
||||
# Get shared libraries using magic
|
||||
RUN mkdir -p /package/lib/
|
||||
RUN ldd $(which shellcheck) | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /package/lib/
|
||||
|
||||
|
||||
# Copy shellcheck package out to mounted directory
|
||||
CMD ["cp", "-avr", "/package", "/mnt/"]
|
@@ -70,6 +70,10 @@ On Debian based distros:
|
||||
|
||||
apt-get install shellcheck
|
||||
|
||||
On Gentoo based distros:
|
||||
|
||||
emerge --ask shellcheck
|
||||
|
||||
On Fedora based distros:
|
||||
|
||||
dnf install ShellCheck
|
||||
|
@@ -1,5 +1,5 @@
|
||||
Name: ShellCheck
|
||||
Version: 0.4.4
|
||||
Version: 0.4.5
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
|
@@ -21,6 +21,7 @@ module ShellCheck.AST where
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Identity
|
||||
import Text.Parsec
|
||||
import qualified ShellCheck.Regex as Re
|
||||
|
||||
data Id = Id Int deriving (Show, Eq, Ord)
|
||||
@@ -50,8 +51,10 @@ data Token =
|
||||
| T_AndIf Id (Token) (Token)
|
||||
| T_Arithmetic Id Token
|
||||
| T_Array Id [Token]
|
||||
| T_IndexedElement Id Token Token
|
||||
| T_Assignment Id AssignmentMode String (Maybe Token) Token
|
||||
| T_IndexedElement Id [Token] Token
|
||||
-- Store the index as string, and parse as arithmetic or string later
|
||||
| T_UnparsedIndex Id SourcePos String
|
||||
| T_Assignment Id AssignmentMode String [Token] Token
|
||||
| T_Backgrounded Id Token
|
||||
| T_Backticked Id [Token]
|
||||
| T_Bang Id
|
||||
@@ -96,6 +99,7 @@ data Token =
|
||||
| T_IfExpression Id [([Token],[Token])] [Token]
|
||||
| T_In Id
|
||||
| T_IoFile Id Token Token
|
||||
| T_IoDuplicate Id Token String
|
||||
| T_LESSAND Id
|
||||
| T_LESSGREAT Id
|
||||
| T_Lbrace Id
|
||||
@@ -145,7 +149,7 @@ tokenEquals a b = kludge a == kludge b
|
||||
instance Eq Token where
|
||||
(==) = tokenEquals
|
||||
|
||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> Token) -> Token -> m Token
|
||||
analyze :: Monad m => (Token -> m ()) -> (Token -> m ()) -> (Token -> m Token) -> Token -> m Token
|
||||
analyze f g i =
|
||||
round
|
||||
where
|
||||
@@ -153,7 +157,7 @@ analyze f g i =
|
||||
f t
|
||||
newT <- delve t
|
||||
g t
|
||||
return . i $ newT
|
||||
i newT
|
||||
roundAll = mapM round
|
||||
|
||||
roundMaybe Nothing = return Nothing
|
||||
@@ -186,14 +190,18 @@ analyze f g i =
|
||||
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
||||
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
|
||||
delve (T_IoFile id op file) = d2 op file $ T_IoFile id
|
||||
delve (T_IoDuplicate id op num) = d1 op $ \x -> T_IoDuplicate id x num
|
||||
delve (T_HereString id word) = d1 word $ T_HereString id
|
||||
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
||||
delve (T_Assignment id mode var index value) = do
|
||||
a <- roundMaybe index
|
||||
delve (T_Assignment id mode var indices value) = do
|
||||
a <- roundAll indices
|
||||
b <- round value
|
||||
return $ T_Assignment id mode var a b
|
||||
delve (T_Array id t) = dl t $ T_Array id
|
||||
delve (T_IndexedElement id t1 t2) = d2 t1 t2 $ T_IndexedElement id
|
||||
delve (T_IndexedElement id indices t) = do
|
||||
a <- roundAll indices
|
||||
b <- round t
|
||||
return $ T_IndexedElement id a b
|
||||
delve (T_Redirecting id redirs cmd) = do
|
||||
a <- roundAll redirs
|
||||
b <- round cmd
|
||||
@@ -312,6 +320,7 @@ getId t = case t of
|
||||
T_BraceExpansion id _ -> id
|
||||
T_DollarBraceCommandExpansion id _ -> id
|
||||
T_IoFile id _ _ -> id
|
||||
T_IoDuplicate id _ _ -> id
|
||||
T_HereDoc id _ _ _ _ -> id
|
||||
T_HereString id _ -> id
|
||||
T_FdRedirect id _ _ -> id
|
||||
@@ -363,10 +372,11 @@ getId t = case t of
|
||||
T_CoProc id _ _ -> id
|
||||
T_CoProcBody id _ -> id
|
||||
T_Include id _ _ -> id
|
||||
T_UnparsedIndex id _ _ -> id
|
||||
|
||||
blank :: Monad m => Token -> m ()
|
||||
blank = const $ return ()
|
||||
doAnalysis f = analyze f blank id
|
||||
doStackAnalysis startToken endToken = analyze startToken endToken id
|
||||
doTransform i = runIdentity . analyze blank blank i
|
||||
doAnalysis f = analyze f blank (return . id)
|
||||
doStackAnalysis startToken endToken = analyze startToken endToken (return . id)
|
||||
doTransform i = runIdentity . analyze blank blank (return . i)
|
||||
|
||||
|
@@ -21,6 +21,7 @@ module ShellCheck.ASTLib where
|
||||
|
||||
import ShellCheck.AST
|
||||
|
||||
import Control.Monad.Writer
|
||||
import Control.Monad
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
@@ -54,6 +55,8 @@ isGlob _ = False
|
||||
-- Is this shell word a constant?
|
||||
isConstant token =
|
||||
case token of
|
||||
-- This ignores some cases like ~"foo":
|
||||
T_NormalWord _ (T_Literal _ ('~':_) : _) -> False
|
||||
T_NormalWord _ l -> all isConstant l
|
||||
T_DoubleQuoted _ l -> all isConstant l
|
||||
T_SingleQuoted _ _ -> True
|
||||
@@ -194,6 +197,8 @@ isLiteral t = isJust $ getLiteralString t
|
||||
-- Turn a NormalWord like foo="bar $baz" into a series of constituent elements like [foo=,bar ,$baz]
|
||||
getWordParts (T_NormalWord _ l) = concatMap getWordParts l
|
||||
getWordParts (T_DoubleQuoted _ l) = l
|
||||
-- TA_Expansion is basically T_NormalWord for arithmetic expressions
|
||||
getWordParts (TA_Expansion _ l) = concatMap getWordParts l
|
||||
getWordParts other = [other]
|
||||
|
||||
-- Return a list of NormalWords that would result from brace expansion
|
||||
@@ -206,14 +211,32 @@ braceExpand (T_NormalWord id list) = take 1000 $ do
|
||||
braceExpand item
|
||||
part x = return x
|
||||
|
||||
-- Maybe get the command name of a token representing a command
|
||||
getCommandName t =
|
||||
-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections
|
||||
getCommand t =
|
||||
case t of
|
||||
T_Redirecting _ _ w -> getCommandName w
|
||||
T_SimpleCommand _ _ (w:_) -> getLiteralString w
|
||||
T_Annotation _ _ t -> getCommandName t
|
||||
T_Redirecting _ _ w -> getCommand w
|
||||
T_SimpleCommand _ _ (w:_) -> return t
|
||||
T_Annotation _ _ t -> getCommand t
|
||||
otherwise -> Nothing
|
||||
|
||||
-- Maybe get the command name of a token representing a command
|
||||
getCommandName t = do
|
||||
(T_SimpleCommand _ _ (w:_)) <- getCommand t
|
||||
getLiteralString w
|
||||
|
||||
-- If a command substitution is a single command, get its name.
|
||||
-- $(date +%s) = Just "date"
|
||||
getCommandNameFromExpansion :: Token -> Maybe String
|
||||
getCommandNameFromExpansion t =
|
||||
case t of
|
||||
T_DollarExpansion _ [c] -> extract c
|
||||
T_Backticked _ [c] -> extract c
|
||||
T_DollarBraceCommandExpansion _ [c] -> extract c
|
||||
otherwise -> Nothing
|
||||
where
|
||||
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
|
||||
extract _ = Nothing
|
||||
|
||||
-- Get the basename of a token representing a command
|
||||
getCommandBasename = liftM basename . getCommandName
|
||||
where
|
||||
@@ -237,8 +260,8 @@ isOnlyRedirection t =
|
||||
|
||||
isFunction t = case t of T_Function {} -> True; _ -> False
|
||||
|
||||
-- Get the list of commands from tokens that contain them, such as
|
||||
-- the body of while loops and if statements.
|
||||
-- Get the lists of commands from tokens that contain them, such as
|
||||
-- the body of while loops or branches of if statements.
|
||||
getCommandSequences t =
|
||||
case t of
|
||||
T_Script _ _ cmds -> [cmds]
|
||||
@@ -251,3 +274,22 @@ getCommandSequences t =
|
||||
T_IfExpression _ thens elses -> map snd thens ++ [elses]
|
||||
otherwise -> []
|
||||
|
||||
-- Get a list of names of associative arrays
|
||||
getAssociativeArrays t =
|
||||
nub . execWriter $ doAnalysis f t
|
||||
where
|
||||
f :: Token -> Writer [String] ()
|
||||
f t@(T_SimpleCommand {}) = fromMaybe (return ()) $ do
|
||||
name <- getCommandName t
|
||||
guard $ name == "declare"
|
||||
let flags = getAllFlags t
|
||||
guard $ elem "A" $ map snd flags
|
||||
let args = map fst . filter ((==) "" . snd) $ flags
|
||||
let names = mapMaybe (getLiteralStringExt nameAssignments) args
|
||||
return $ tell names
|
||||
f _ = return ()
|
||||
|
||||
nameAssignments t =
|
||||
case t of
|
||||
T_Assignment _ _ name _ _ -> return name
|
||||
otherwise -> Nothing
|
||||
|
@@ -85,6 +85,7 @@ checksFor Bash = [
|
||||
,checkEchoSed
|
||||
,checkForDecimals
|
||||
,checkLocalScope
|
||||
,checkMultiDimensionalArrays
|
||||
]
|
||||
|
||||
runAnalytics :: AnalysisSpec -> [TokenComment]
|
||||
@@ -178,6 +179,7 @@ nodeChecks = [
|
||||
,checkReadWithoutR
|
||||
,checkLoopVariableReassignment
|
||||
,checkTrailingBracket
|
||||
,checkReturnAgainstZero
|
||||
]
|
||||
|
||||
|
||||
@@ -369,6 +371,8 @@ prop_checkPipePitfalls4 = verifyNot checkPipePitfalls "find . -print0 | xargs -0
|
||||
prop_checkPipePitfalls5 = verifyNot checkPipePitfalls "ls -N | foo"
|
||||
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"
|
||||
checkPipePitfalls _ (T_Pipeline id _ commands) = do
|
||||
for ["find", "xargs"] $
|
||||
\(find:xargs:_) ->
|
||||
@@ -388,8 +392,12 @@ checkPipePitfalls _ (T_Pipeline id _ commands) = do
|
||||
for' ["ps", "grep"] $
|
||||
\x -> info x 2009 "Consider using pgrep instead of grepping ps output."
|
||||
|
||||
for' ["grep", "wc"] $
|
||||
\x -> style x 2126 "Consider using grep -c instead of grep|wc."
|
||||
for ["grep", "wc"] $
|
||||
\(grep:wc:_) ->
|
||||
let flags = fromMaybe [] $ map snd <$> getAllFlags <$> getCommand grep
|
||||
in
|
||||
unless (any (`elem` ["o", "only-matching"]) flags) $
|
||||
style (getId grep) 2126 "Consider using grep -c instead of grep|wc."
|
||||
|
||||
didLs <- liftM or . sequence $ [
|
||||
for' ["ls", "grep"] $
|
||||
@@ -439,10 +447,16 @@ checkShebangParameters _ (T_Script id sb _) =
|
||||
prop_checkShebang1 = verifyNotTree checkShebang "#!/usr/bin/env bash -x\necho cow"
|
||||
prop_checkShebang2 = verifyNotTree checkShebang "#! /bin/sh -l "
|
||||
prop_checkShebang3 = verifyTree checkShebang "ls -l"
|
||||
checkShebang params (T_Annotation _ _ t) = checkShebang params t
|
||||
prop_checkShebang4 = verifyNotTree checkShebang "#shellcheck shell=sh\nfoo"
|
||||
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 == "" ]
|
||||
[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 ]"
|
||||
@@ -493,6 +507,9 @@ 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
|
||||
@@ -524,6 +541,7 @@ checkBashisms params = bashism
|
||||
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"
|
||||
@@ -765,18 +783,23 @@ prop_checkUnquotedExpansions4 = verifyNot checkUnquotedExpansions "[[ $(foo) ==
|
||||
prop_checkUnquotedExpansions5 = verifyNot checkUnquotedExpansions "for f in $(cmd); do echo $f; done"
|
||||
prop_checkUnquotedExpansions6 = verifyNot checkUnquotedExpansions "$(cmd)"
|
||||
prop_checkUnquotedExpansions7 = verifyNot checkUnquotedExpansions "cat << foo\n$(ls)\nfoo"
|
||||
prop_checkUnquotedExpansions8 = verifyNot checkUnquotedExpansions "set -- $(seq 1 4)"
|
||||
prop_checkUnquotedExpansions9 = verifyNot checkUnquotedExpansions "echo foo `# inline comment`"
|
||||
checkUnquotedExpansions params =
|
||||
check
|
||||
where
|
||||
check t@(T_DollarExpansion _ _) = examine t
|
||||
check t@(T_Backticked _ _) = examine t
|
||||
check t@(T_DollarBraceCommandExpansion _ _) = examine t
|
||||
check t@(T_DollarExpansion _ c) = examine t c
|
||||
check t@(T_Backticked _ c) = examine t c
|
||||
check t@(T_DollarBraceCommandExpansion _ c) = examine t c
|
||||
check _ = return ()
|
||||
tree = parentMap params
|
||||
examine t =
|
||||
unless (isQuoteFree tree t || usedAsCommandName tree t) $
|
||||
examine t contents =
|
||||
unless (null contents || shouldBeSplit t || isQuoteFree tree t || usedAsCommandName tree t) $
|
||||
warn (getId t) 2046 "Quote this to prevent word splitting."
|
||||
|
||||
shouldBeSplit t =
|
||||
getCommandNameFromExpansion t == Just "seq"
|
||||
|
||||
|
||||
prop_checkRedirectToSame = verify checkRedirectToSame "cat foo > foo"
|
||||
prop_checkRedirectToSame2 = verify checkRedirectToSame "cat lol | sed -e 's/a/b/g' > lol"
|
||||
@@ -933,7 +956,7 @@ checkArrayWithoutIndex params _ =
|
||||
"Expanding an array without an index only gives the first element."
|
||||
readF _ _ _ = return []
|
||||
|
||||
writeF _ (T_Assignment id mode name Nothing _) _ (DataString _) = do
|
||||
writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do
|
||||
isArray <- gets (isJust . Map.lookup name)
|
||||
return $ if not isArray then [] else
|
||||
case mode of
|
||||
@@ -951,7 +974,7 @@ checkArrayWithoutIndex params _ =
|
||||
|
||||
isIndexed expr =
|
||||
case expr of
|
||||
T_Assignment _ _ _ (Just _) _ -> True
|
||||
T_Assignment _ _ _ (_:_) _ -> True
|
||||
_ -> False
|
||||
|
||||
prop_checkStderrRedirect = verify checkStderrRedirect "test 2>&1 > cow"
|
||||
@@ -961,7 +984,7 @@ prop_checkStderrRedirect4 = verifyNot checkStderrRedirect "errors=$(test 2>&1 >
|
||||
prop_checkStderrRedirect5 = verifyNot checkStderrRedirect "read < <(test 2>&1 > file)"
|
||||
prop_checkStderrRedirect6 = verify checkStderrRedirect "foo | bar 2>&1 > /dev/null"
|
||||
checkStderrRedirect params redir@(T_Redirecting _ [
|
||||
T_FdRedirect id "2" (T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "1"])),
|
||||
T_FdRedirect id "2" (T_IoDuplicate _ (T_GREATAND _) "1"),
|
||||
T_FdRedirect _ _ (T_IoFile _ op _)
|
||||
] _) = case op of
|
||||
T_Greater _ -> error
|
||||
@@ -1030,6 +1053,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) =
|
||||
,"alias"
|
||||
,"sudo" -- covering "sudo sh" and such
|
||||
,"dpkg-query"
|
||||
,"jq" -- could also check that user provides --arg
|
||||
]
|
||||
|| "awk" `isSuffixOf` commandName
|
||||
|| "perl" `isPrefixOf` commandName
|
||||
@@ -1154,6 +1178,7 @@ prop_checkSingleBracketOperators1 = verify checkSingleBracketOperators "[ test =
|
||||
prop_checkSingleBracketOperators2 = verify checkSingleBracketOperators "[ $foo > $bar ]"
|
||||
prop_checkSingleBracketOperators3 = verifyNot checkSingleBracketOperators "[[ foo < bar ]]"
|
||||
prop_checkSingleBracketOperators5 = verify checkSingleBracketOperators "until [ $n <= $z ]; do echo foo; done"
|
||||
prop_checkSingleBracketOperators6 = verifyNot checkSingleBracketOperators "[ $foo '>' $bar ]"
|
||||
checkSingleBracketOperators _ (TC_Binary id typ op lhs rhs)
|
||||
| typ == SingleBracket && op `elem` ["<", ">", "<=", ">="] =
|
||||
err id 2073 $ "Can't use " ++ op ++" in [ ]. Escape it or use [[..]]."
|
||||
@@ -1234,12 +1259,11 @@ prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]"
|
||||
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' ]]"
|
||||
checkConstantIfs _ (TC_Binary id typ op lhs rhs) | not isDynamic =
|
||||
when (isJust lLit && isJust rLit) $
|
||||
when (isConstant lhs && isConstant rhs) $
|
||||
warn id 2050 "This expression is constant. Did you forget the $ on a variable?"
|
||||
where
|
||||
lLit = getLiteralString lhs
|
||||
rLit = getLiteralString rhs
|
||||
isDynamic =
|
||||
op `elem` [ "-lt", "-gt", "-le", "-ge", "-eq", "-ne" ]
|
||||
&& typ == DoubleBracket
|
||||
@@ -1322,7 +1346,9 @@ checkBraceExpansionVars params t@(T_BraceExpansion id list) = mapM_ check list
|
||||
(`isUnqualifiedCommand` "eval") <$> getClosestCommand (parentMap params) t
|
||||
checkBraceExpansionVars _ _ = return ()
|
||||
|
||||
prop_checkForDecimals = verify checkForDecimals "((3.14*c))"
|
||||
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 params t@(TA_Expansion id _) = potentially $ do
|
||||
guard $ not (hasFloatingPoint params)
|
||||
str <- getLiteralString t
|
||||
@@ -1356,7 +1382,7 @@ checkArithmeticDeref params t@(TA_Expansion _ [b@(T_DollarBraced id _)]) =
|
||||
unless (isException $ bracedString b) getWarning
|
||||
where
|
||||
isException [] = True
|
||||
isException s = any (`elem` "/.:#%?*@$") s || isDigit (head s)
|
||||
isException s = any (`elem` "/.:#%?*@$-") s || isDigit (head s)
|
||||
getWarning = fromMaybe noWarning . msum . map warningFor $ parents params t
|
||||
warningFor t =
|
||||
case t of
|
||||
@@ -2071,6 +2097,8 @@ prop_checkUnused30= verifyTree checkUnusedAssignments "let a=1"
|
||||
prop_checkUnused31= verifyTree checkUnusedAssignments "let 'a=1'"
|
||||
prop_checkUnused32= verifyTree checkUnusedAssignments "let a=b=c; echo $a"
|
||||
prop_checkUnused33= verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]"
|
||||
prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t"
|
||||
prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"
|
||||
checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||
where
|
||||
flow = variableFlow params
|
||||
@@ -2115,6 +2143,11 @@ prop_checkUnassignedReferences19= verifyNotTree checkUnassignedReferences "reado
|
||||
prop_checkUnassignedReferences20= verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo"
|
||||
prop_checkUnassignedReferences21= verifyTree checkUnassignedReferences "echo ${#foo}"
|
||||
prop_checkUnassignedReferences22= verifyNotTree checkUnassignedReferences "echo ${!os*}"
|
||||
prop_checkUnassignedReferences23= verifyTree checkUnassignedReferences "declare -a foo; foo[bar]=42;"
|
||||
prop_checkUnassignedReferences24= verifyNotTree checkUnassignedReferences "declare -A foo; foo[bar]=42;"
|
||||
prop_checkUnassignedReferences25= verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;"
|
||||
prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b"
|
||||
prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}"
|
||||
checkUnassignedReferences params t = warnings
|
||||
where
|
||||
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
||||
@@ -2262,7 +2295,7 @@ checkPrefixAssignmentReference params t@(T_DollarBraced id value) =
|
||||
case t of
|
||||
T_SimpleCommand _ vars (_:_) -> mapM_ checkVar vars
|
||||
otherwise -> check rest
|
||||
checkVar (T_Assignment aId mode aName Nothing value) |
|
||||
checkVar (T_Assignment aId mode aName [] value) |
|
||||
aName == name && (aId `notElem` idPath) = do
|
||||
warn aId 2097 "This assignment is only seen by the forked process."
|
||||
warn id 2098 "This expansion will not see the mentioned assignment."
|
||||
@@ -2542,7 +2575,7 @@ prop_checkOverridingPath8 = verifyNot checkOverridingPath "PATH=$PATH:/stuff"
|
||||
checkOverridingPath _ (T_SimpleCommand _ vars []) =
|
||||
mapM_ checkVar vars
|
||||
where
|
||||
checkVar (T_Assignment id Assign "PATH" Nothing word) =
|
||||
checkVar (T_Assignment id Assign "PATH" [] word) =
|
||||
let string = concat $ oversimplify word
|
||||
in unless (any (`isInfixOf` string) ["/bin", "/sbin" ]) $ do
|
||||
when ('/' `elem` string && ':' `notElem` string) $ notify id
|
||||
@@ -2557,7 +2590,7 @@ prop_checkTildeInPath3 = verifyNot checkTildeInPath "PATH=~/bin"
|
||||
checkTildeInPath _ (T_SimpleCommand _ vars _) =
|
||||
mapM_ checkVar vars
|
||||
where
|
||||
checkVar (T_Assignment id Assign "PATH" Nothing (T_NormalWord _ parts)) =
|
||||
checkVar (T_Assignment id Assign "PATH" [] (T_NormalWord _ parts)) =
|
||||
when (any (\x -> isQuoted x && hasTilde x) parts) $
|
||||
warn id 2147 "Literal tilde in PATH works poorly across programs."
|
||||
checkVar _ = return ()
|
||||
@@ -2618,7 +2651,7 @@ checkMultipleAppends params t =
|
||||
|
||||
prop_checkSuspiciousIFS1 = verify checkSuspiciousIFS "IFS=\"\\n\""
|
||||
prop_checkSuspiciousIFS2 = verifyNot checkSuspiciousIFS "IFS=$'\\t'"
|
||||
checkSuspiciousIFS params (T_Assignment id Assign "IFS" Nothing value) =
|
||||
checkSuspiciousIFS params (T_Assignment id Assign "IFS" [] value) =
|
||||
potentially $ do
|
||||
str <- getLiteralString value
|
||||
return $ check str
|
||||
@@ -2790,5 +2823,54 @@ 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 $? ]]"
|
||||
prop_checkReturnAgainstZero4 = verifyNot checkReturnAgainstZero "[[ $? -eq 4 ]]"
|
||||
prop_checkReturnAgainstZero5 = verify checkReturnAgainstZero "[[ 0 -eq $? ]]"
|
||||
prop_checkReturnAgainstZero6 = verifyNot checkReturnAgainstZero "[[ $R -eq 0 ]]"
|
||||
prop_checkReturnAgainstZero7 = verify checkReturnAgainstZero "(( $? == 0 ))"
|
||||
prop_checkReturnAgainstZero8 = verify checkReturnAgainstZero "(( $? ))"
|
||||
prop_checkReturnAgainstZero9 = verify checkReturnAgainstZero "(( ! $? ))"
|
||||
checkReturnAgainstZero _ token =
|
||||
case token of
|
||||
TC_Binary id _ _ lhs rhs -> check lhs rhs
|
||||
TA_Binary id _ lhs rhs -> check lhs rhs
|
||||
TA_Unary id _ exp ->
|
||||
when (isExitCode exp) $ message (getId exp)
|
||||
TA_Sequence _ [exp] ->
|
||||
when (isExitCode exp) $ message (getId exp)
|
||||
otherwise -> return ()
|
||||
where
|
||||
check lhs rhs =
|
||||
if isZero rhs && isExitCode lhs
|
||||
then message (getId lhs)
|
||||
else when (isZero lhs && isExitCode rhs) $ message (getId rhs)
|
||||
isZero t = getLiteralString t == Just "0"
|
||||
isExitCode t =
|
||||
case getWordParts t of
|
||||
[exp@(T_DollarBraced {})] -> bracedString exp == "?"
|
||||
otherwise -> False
|
||||
message id = style id 2181 "Check exit code directly with e.g. 'if mycmd;', not indirectly with $?."
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
@@ -334,6 +334,12 @@ getModifiedVariables t =
|
||||
name <- getLiteralString lhs
|
||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||
|
||||
T_DollarBraced _ l -> maybeToList $ do
|
||||
let string = bracedString t
|
||||
let modifier = getBracedModifier string
|
||||
guard $ ":=" `isPrefixOf` modifier
|
||||
return (t, t, getBracedReference string, DataString $ SourceFrom [l])
|
||||
|
||||
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&2 modifies foo
|
||||
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||
|
||||
@@ -361,7 +367,10 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
||||
"declare" -> if any (`elem` flags) ["x", "p"]
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
"readonly" -> concatMap getReference rest
|
||||
"readonly" ->
|
||||
if any (`elem` flags) ["f", "p"]
|
||||
then []
|
||||
else concatMap getReference rest
|
||||
"trap" ->
|
||||
case rest of
|
||||
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
|
||||
@@ -395,7 +404,10 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||
"typeset" -> declaredVars
|
||||
|
||||
"local" -> concatMap getModifierParamString rest
|
||||
"readonly" -> concatMap getModifierParamString rest
|
||||
"readonly" ->
|
||||
if any (`elem` flags) ["f", "p"]
|
||||
then []
|
||||
else concatMap getModifierParamString rest
|
||||
"set" -> maybeToList $ do
|
||||
params <- getSetParams rest
|
||||
return (base, base, "@", DataString $ SourceFrom params)
|
||||
@@ -474,11 +486,20 @@ getIndexReferences s = fromMaybe [] $ do
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 0
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^ *:(.*)"
|
||||
|
||||
getReferencedVariables parents t =
|
||||
case t of
|
||||
T_DollarBraced id l -> let str = bracedString t in
|
||||
(t, t, getBracedReference str) :
|
||||
map (\x -> (l, l, x)) (getIndexReferences str)
|
||||
map (\x -> (l, l, x)) (
|
||||
getIndexReferences str
|
||||
++ getOffsetReferences (getBracedModifier str))
|
||||
TA_Expansion id _ ->
|
||||
if isArithmeticAssignment t
|
||||
then []
|
||||
@@ -520,7 +541,7 @@ getReferencedVariables parents t =
|
||||
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||
|
||||
isArithmeticAssignment t = case getPath parents t of
|
||||
this: TA_Assignment _ "=" _ _ :_ -> True
|
||||
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
_ -> False
|
||||
|
||||
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||
@@ -573,6 +594,7 @@ prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
||||
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
|
||||
prop_getBracedReference11= getBracedReference "!os*" == ""
|
||||
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
|
||||
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
|
||||
getBracedReference s = fromMaybe s $
|
||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||
where
|
||||
@@ -595,6 +617,20 @@ getBracedReference s = fromMaybe s $
|
||||
return ""
|
||||
nameExpansion _ = Nothing
|
||||
|
||||
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
||||
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
||||
getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||
let var = getBracedReference s
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Useful generic functions
|
||||
potentially :: Monad m => Maybe (m ()) -> m ()
|
||||
@@ -628,5 +664,5 @@ filterByAnnotation token =
|
||||
getCode (TokenComment _ (Comment _ c _)) = c
|
||||
|
||||
|
||||
return []
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
@@ -39,7 +39,7 @@ import Test.QuickCheck.All
|
||||
|
||||
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
||||
position <- Map.lookup id map
|
||||
return $ PositionedComment position c
|
||||
return $ PositionedComment position position c
|
||||
where
|
||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||
|
||||
@@ -65,13 +65,13 @@ checkScript sys spec = do
|
||||
return . nub . sortMessages . filter shouldInclude $
|
||||
(parseMessages ++ map translator analysisMessages)
|
||||
|
||||
shouldInclude (PositionedComment _ (Comment _ code _)) =
|
||||
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
|
||||
code `notElem` csExcludedWarnings spec
|
||||
|
||||
sortMessages = sortBy (comparing order)
|
||||
order (PositionedComment pos (Comment severity code message)) =
|
||||
order (PositionedComment pos _ (Comment severity code message)) =
|
||||
(posFile pos, posLine pos, posColumn pos, severity, code, message)
|
||||
getPosition (PositionedComment pos _) = pos
|
||||
getPosition (PositionedComment pos _ _) = pos
|
||||
|
||||
analysisSpec root =
|
||||
AnalysisSpec {
|
||||
@@ -84,7 +84,7 @@ getErrors sys spec =
|
||||
sort . map getCode . crComments $
|
||||
runIdentity (checkScript sys spec)
|
||||
where
|
||||
getCode (PositionedComment _ (Comment _ code _)) = code
|
||||
getCode (PositionedComment _ _ (Comment _ code _)) = code
|
||||
|
||||
check = checkWithIncludes []
|
||||
|
||||
|
@@ -90,6 +90,8 @@ commandChecks = [
|
||||
,checkExportedExpansions
|
||||
,checkAliasesUsesArgs
|
||||
,checkAliasesExpandEarly
|
||||
,checkUnsetGlobs
|
||||
,checkFindWithoutPath
|
||||
]
|
||||
|
||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||
@@ -150,7 +152,7 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||
info (getId word) 2020 "tr replaces sets of chars, not words (mentioned due to duplicates)."
|
||||
unless ("[:" `isPrefixOf` s) $
|
||||
when ("[" `isPrefixOf` s && "]" `isSuffixOf` s && (length s > 2) && ('*' `notElem` s)) $
|
||||
info (getId word) 2021 "Don't use [] around ranges in tr, it replaces literal square brackets."
|
||||
info (getId word) 2021 "Don't use [] around classes in tr, it replaces literal square brackets."
|
||||
Nothing -> return ()
|
||||
|
||||
duplicated s =
|
||||
@@ -470,16 +472,49 @@ prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\""
|
||||
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'"
|
||||
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)"
|
||||
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var"
|
||||
prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar"
|
||||
prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
|
||||
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
|
||||
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
|
||||
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
|
||||
f (format:params) = check format
|
||||
f (format:params) = check format params
|
||||
f _ = return ()
|
||||
check format =
|
||||
|
||||
countFormats string =
|
||||
case string of
|
||||
'%':'%':rest -> countFormats rest
|
||||
'%':rest -> 1 + countFormats rest
|
||||
_:rest -> countFormats rest
|
||||
[] -> 0
|
||||
|
||||
check format more = do
|
||||
fromMaybe (return ()) $ do
|
||||
string <- getLiteralString format
|
||||
let vars = countFormats string
|
||||
|
||||
return $ do
|
||||
when (vars == 0 && more /= []) $
|
||||
err (getId format) 2182
|
||||
"This printf format string has no variables. Other arguments are ignored."
|
||||
|
||||
when (vars > 0
|
||||
&& length more < vars
|
||||
&& all (not . mayBecomeMultipleArgs) more) $
|
||||
warn (getId format) 2183 $
|
||||
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
|
||||
|
||||
|
||||
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||
warn (getId format) 2059
|
||||
info (getId format) 2059
|
||||
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
|
||||
|
||||
|
||||
|
||||
|
||||
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)"
|
||||
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`"
|
||||
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\""
|
||||
@@ -556,5 +591,33 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
||||
checkArg _ = return ()
|
||||
|
||||
|
||||
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
|
||||
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
|
||||
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
|
||||
where
|
||||
check arg =
|
||||
when (isGlob arg) $
|
||||
warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded."
|
||||
|
||||
|
||||
prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f"
|
||||
prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find"
|
||||
prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f"
|
||||
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
|
||||
checkFindWithoutPath = CommandCheck (Basename "find") f
|
||||
where
|
||||
f (T_SimpleCommand _ _ (cmd:args)) =
|
||||
unless (hasPath args) $
|
||||
info (getId cmd) 2185 "Some finds don't have a default path. Specify '.' explicitly."
|
||||
|
||||
-- This is a bit of a kludge. find supports flag arguments both before and after the path,
|
||||
-- as well as multiple non-flag arguments that are not the path. We assume that all the
|
||||
-- pre-path flags are single characters, which is generally the case.
|
||||
hasPath (first:rest) =
|
||||
let flag = fromJust $ getLiteralStringExt (const $ return "___") first in
|
||||
not ("-" `isPrefixOf` flag) || length flag <= 2 && hasPath rest
|
||||
hasPath [] = False
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
@@ -30,13 +30,15 @@ data Formatter = Formatter {
|
||||
footer :: IO ()
|
||||
}
|
||||
|
||||
lineNo (PositionedComment pos _) = posLine pos
|
||||
colNo (PositionedComment pos _) = posColumn pos
|
||||
codeNo (PositionedComment _ (Comment _ code _)) = code
|
||||
messageText (PositionedComment _ (Comment _ _ t)) = t
|
||||
lineNo (PositionedComment pos _ _) = posLine pos
|
||||
endLineNo (PositionedComment _ end _) = posLine end
|
||||
colNo (PositionedComment pos _ _) = posColumn pos
|
||||
endColNo (PositionedComment _ end _) = posColumn end
|
||||
codeNo (PositionedComment _ _ (Comment _ code _)) = code
|
||||
messageText (PositionedComment _ _ (Comment _ _ t)) = t
|
||||
|
||||
severityText :: PositionedComment -> String
|
||||
severityText (PositionedComment _ (Comment c _ _)) =
|
||||
severityText (PositionedComment _ _ (Comment c _ _)) =
|
||||
case c of
|
||||
ErrorC -> "error"
|
||||
WarningC -> "warning"
|
||||
@@ -48,12 +50,15 @@ makeNonVirtual comments contents =
|
||||
map fix comments
|
||||
where
|
||||
ls = lines contents
|
||||
fix c@(PositionedComment pos comment) = PositionedComment pos {
|
||||
posColumn =
|
||||
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
||||
else colNo c
|
||||
fix c@(PositionedComment start end comment) = PositionedComment start {
|
||||
posColumn = realignColumn lineNo colNo c
|
||||
} end {
|
||||
posColumn = realignColumn endLineNo endColNo c
|
||||
} comment
|
||||
realignColumn lineNo colNo c =
|
||||
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
||||
else colNo c
|
||||
real _ r v target | target <= v = r
|
||||
real [] r v _ = r -- should never happen
|
||||
real ('\t':rest) r v target =
|
||||
|
@@ -37,10 +37,12 @@ format = do
|
||||
}
|
||||
|
||||
instance JSON (PositionedComment) where
|
||||
showJSON comment@(PositionedComment pos (Comment level code string)) = makeObj [
|
||||
("file", showJSON $ posFile pos),
|
||||
("line", showJSON $ posLine pos),
|
||||
("column", showJSON $ posColumn pos),
|
||||
showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [
|
||||
("file", showJSON $ posFile start),
|
||||
("line", showJSON $ posLine start),
|
||||
("endLine", showJSON $ posLine end),
|
||||
("column", showJSON $ posColumn start),
|
||||
("endColumn", showJSON $ posColumn end),
|
||||
("level", showJSON $ severityText comment),
|
||||
("code", showJSON code),
|
||||
("message", showJSON string)
|
||||
|
@@ -94,7 +94,7 @@ data Position = Position {
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data Comment = Comment Severity Code String deriving (Show, Eq)
|
||||
data PositionedComment = PositionedComment Position Comment deriving (Show, Eq)
|
||||
data PositionedComment = PositionedComment Position Position Comment deriving (Show, Eq)
|
||||
data TokenComment = TokenComment Id Comment deriving (Show, Eq)
|
||||
|
||||
data ColorOption =
|
||||
|
@@ -135,7 +135,7 @@ almostSpace =
|
||||
|
||||
--------- Message/position annotation on top of user state
|
||||
data Note = Note Id Severity Code String deriving (Show, Eq)
|
||||
data ParseNote = ParseNote SourcePos Severity Code String deriving (Show, Eq)
|
||||
data ParseNote = ParseNote SourcePos SourcePos Severity Code String deriving (Show, Eq)
|
||||
data Context =
|
||||
ContextName SourcePos String
|
||||
| ContextAnnotation [Annotation]
|
||||
@@ -162,9 +162,9 @@ initialUserState = UserState {
|
||||
pendingHereDocs = []
|
||||
}
|
||||
|
||||
codeForParseNote (ParseNote _ _ code _) = code
|
||||
codeForParseNote (ParseNote _ _ _ code _) = code
|
||||
noteToParseNote map (Note id severity code message) =
|
||||
ParseNote pos severity code message
|
||||
ParseNote pos pos severity code message
|
||||
where
|
||||
pos = fromJust $ Map.lookup id map
|
||||
|
||||
@@ -181,6 +181,7 @@ getNextIdAt sourcepos = do
|
||||
return newId
|
||||
where incId (Id n) = Id $ n+1
|
||||
|
||||
getNextId :: Monad m => SCParser m Id
|
||||
getNextId = do
|
||||
pos <- getPosition
|
||||
getNextIdAt pos
|
||||
@@ -320,14 +321,16 @@ pushContext c = do
|
||||
v <- getCurrentContexts
|
||||
setCurrentContexts (c:v)
|
||||
|
||||
parseProblemAt pos level code msg = do
|
||||
parseProblemAtWithEnd start end level code msg = do
|
||||
irrelevant <- shouldIgnoreCode code
|
||||
unless irrelevant $
|
||||
Ms.modify (\state -> state {
|
||||
parseProblems = note:parseProblems state
|
||||
})
|
||||
where
|
||||
note = ParseNote pos level code msg
|
||||
note = ParseNote start end level code msg
|
||||
|
||||
parseProblemAt pos = parseProblemAtWithEnd pos pos
|
||||
|
||||
-- Store non-parse problems inside
|
||||
|
||||
@@ -335,7 +338,9 @@ parseNote c l a = do
|
||||
pos <- getPosition
|
||||
parseNoteAt pos c l a
|
||||
|
||||
parseNoteAt pos c l a = addParseNote $ ParseNote pos c l a
|
||||
parseNoteAt pos c l a = addParseNote $ ParseNote pos pos c l a
|
||||
|
||||
parseNoteAtWithEnd start end c l a = addParseNote $ ParseNote start end c l a
|
||||
|
||||
--------- Convenient combinators
|
||||
thenSkip main follow = do
|
||||
@@ -406,7 +411,7 @@ readConditionContents single =
|
||||
pos <- getPosition
|
||||
s <- many1 letter
|
||||
when (s `elem` commonCommands) $
|
||||
parseProblemAt pos WarningC 1009 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")
|
||||
parseProblemAt pos WarningC 1014 "Use 'if cmd; then ..' to check exit code, or 'if [[ $(cmd) == .. ]]' to check output.")
|
||||
|
||||
where
|
||||
spacingOrLf = condSpacing True
|
||||
@@ -442,7 +447,7 @@ readConditionContents single =
|
||||
c <- oneOf "'\""
|
||||
s <- anyOp
|
||||
char c
|
||||
return s
|
||||
return $ escaped s
|
||||
|
||||
anyOp = flagOp <|> flaglessOp <|> fail
|
||||
"Expected comparison operator (don't wrap commands in []/[[]])"
|
||||
@@ -645,6 +650,7 @@ prop_a19= isOk readArithmeticContents "\\\n3 +\\\n 2"
|
||||
prop_a20= isOk readArithmeticContents "a ? b ? c : d : e"
|
||||
prop_a21= isOk readArithmeticContents "a ? b : c ? d : e"
|
||||
prop_a22= isOk readArithmeticContents "!!a"
|
||||
readArithmeticContents :: Monad m => SCParser m Token
|
||||
readArithmeticContents =
|
||||
readSequence
|
||||
where
|
||||
@@ -658,12 +664,40 @@ readArithmeticContents =
|
||||
id <- getNextId
|
||||
op <- choice (map (\x -> try $ do
|
||||
s <- string x
|
||||
notFollowedBy2 $ oneOf "&|<>="
|
||||
failIfIncompleteOp
|
||||
return s
|
||||
) op)
|
||||
spacing
|
||||
return $ token id op
|
||||
|
||||
failIfIncompleteOp = notFollowedBy2 $ oneOf "&|<>="
|
||||
|
||||
-- Read binary minus, but also check for -lt, -gt and friends:
|
||||
readMinusOp = do
|
||||
id <- getNextId
|
||||
pos <- getPosition
|
||||
try $ do
|
||||
char '-'
|
||||
failIfIncompleteOp
|
||||
optional $ do
|
||||
(str, alt) <- lookAhead . choice $ map tryOp [
|
||||
("lt", "<"),
|
||||
("gt", ">"),
|
||||
("le", "<="),
|
||||
("ge", ">="),
|
||||
("eq", "=="),
|
||||
("ne", "!=")
|
||||
]
|
||||
parseProblemAt pos ErrorC 1106 $ "In arithmetic contexts, use " ++ alt ++ " instead of -" ++ str
|
||||
spacing
|
||||
return $ TA_Binary id "-"
|
||||
where
|
||||
tryOp (str, alt) = try $ do
|
||||
string str
|
||||
spacing1
|
||||
return (str, alt)
|
||||
|
||||
|
||||
readArrayIndex = do
|
||||
id <- getNextId
|
||||
char '['
|
||||
@@ -733,7 +767,7 @@ readArithmeticContents =
|
||||
readEquated = readCompared `splitBy` ["==", "!="]
|
||||
readCompared = readShift `splitBy` ["<=", ">=", "<", ">"]
|
||||
readShift = readAddition `splitBy` ["<<", ">>"]
|
||||
readAddition = readMultiplication `splitBy` ["+", "-"]
|
||||
readAddition = chainl1 readMultiplication (readBinary ["+"] <|> readMinusOp)
|
||||
readMultiplication = readExponential `splitBy` ["*", "/", "%"]
|
||||
readExponential = readAnyNegated `splitBy` ["**"]
|
||||
|
||||
@@ -810,7 +844,7 @@ readCondition = called "test expression" $ do
|
||||
pos <- getPosition
|
||||
space <- allspacing
|
||||
when (null space) $
|
||||
parseProblemAt pos ErrorC 1035 $ "You need a space after the " ++
|
||||
parseProblemAtWithEnd opos pos ErrorC 1035 $ "You need a space after the " ++
|
||||
if single
|
||||
then "[ and before the ]."
|
||||
else "[[ and before the ]]."
|
||||
@@ -835,10 +869,11 @@ readAnnotationPrefix = do
|
||||
prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n"
|
||||
prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\n"
|
||||
prop_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n"
|
||||
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
|
||||
readAnnotation = called "shellcheck annotation" $ do
|
||||
try readAnnotationPrefix
|
||||
many1 linewhitespace
|
||||
values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride)
|
||||
values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride <|> anyKey)
|
||||
linefeed
|
||||
many linewhitespace
|
||||
return $ concat values
|
||||
@@ -870,6 +905,13 @@ readAnnotation = called "shellcheck annotation" $ do
|
||||
many linewhitespace
|
||||
return value
|
||||
|
||||
anyKey = do
|
||||
pos <- getPosition
|
||||
anyChar `reluctantlyTill1` whitespace
|
||||
many linewhitespace
|
||||
parseNoteAt pos WarningC 1107 "This directive is unknown. It will be ignored."
|
||||
return []
|
||||
|
||||
readAnnotations = do
|
||||
annotations <- many (readAnnotation `thenSkip` allspacing)
|
||||
return $ concat annotations
|
||||
@@ -897,6 +939,20 @@ readNormalishWord end = do
|
||||
checkPossibleTermination pos x
|
||||
return $ T_NormalWord id x
|
||||
|
||||
readIndexSpan = do
|
||||
id <- getNextId
|
||||
x <- many (readNormalWordPart "]" <|> someSpace <|> otherLiteral)
|
||||
return $ T_NormalWord id x
|
||||
where
|
||||
someSpace = do
|
||||
id <- getNextId
|
||||
str <- spacing1
|
||||
return $ T_Literal id str
|
||||
otherLiteral = do
|
||||
id <- getNextId
|
||||
str <- many1 $ oneOf quotableChars
|
||||
return $ T_Literal id str
|
||||
|
||||
checkPossibleTermination pos [T_Literal _ x] =
|
||||
when (x `elem` ["do", "done", "then", "fi", "esac"]) $
|
||||
parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
|
||||
@@ -1060,13 +1116,18 @@ subParse pos parser input = do
|
||||
setPosition lastPosition
|
||||
return result
|
||||
|
||||
inSeparateContext parser = do
|
||||
-- Parse something, but forget all parseProblems
|
||||
inSeparateContext = parseForgettingContext True
|
||||
-- Parse something, but forget all parseProblems on failure
|
||||
forgetOnFailure = parseForgettingContext False
|
||||
|
||||
parseForgettingContext alsoOnSuccess parser = do
|
||||
context <- Ms.get
|
||||
success context <|> failure context
|
||||
where
|
||||
success c = do
|
||||
res <- try parser
|
||||
Ms.put c
|
||||
when alsoOnSuccess $ Ms.put c
|
||||
return res
|
||||
failure c = do
|
||||
Ms.put c
|
||||
@@ -1303,7 +1364,17 @@ readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
|
||||
|
||||
prop_readDollarExpression1 = isOk readDollarExpression "$(((1) && 3))"
|
||||
prop_readDollarExpression2 = isWarning readDollarExpression "$(((1)) && 3)"
|
||||
readDollarExpression = readTripleParenthesis "$" readDollarArithmetic readDollarExpansion <|> readDollarArithmetic <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable
|
||||
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
|
||||
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.")
|
||||
|
||||
prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
|
||||
readDollarSingleQuote = called "$'..' expression" $ do
|
||||
@@ -1349,25 +1420,20 @@ readArithmeticExpression = called "((..)) command" $ do
|
||||
string "))"
|
||||
return (T_Arithmetic id c)
|
||||
|
||||
-- Check if maybe ((( was intended as ( (( rather than (( (
|
||||
readTripleParenthesis prefix expected alternative = do
|
||||
pos <- try . lookAhead $ do
|
||||
string prefix
|
||||
p <- getPosition
|
||||
string "(((" -- should optimally be "((" but it's noisy and rarely helpful
|
||||
return p
|
||||
|
||||
-- If the next characters match prefix, try two different parsers and warn if the alternate parser had to be used
|
||||
readAmbiguous :: Monad m => String -> SCParser m p -> SCParser m p -> (SourcePos -> SCParser m ()) -> SCParser m p
|
||||
readAmbiguous prefix expected alternative warner = do
|
||||
pos <- getPosition
|
||||
try . lookAhead $ string prefix
|
||||
-- If the expected parser fails, try the alt.
|
||||
-- If the alt fails, run the expected one again for the errors.
|
||||
try expected <|> tryAlt pos <|> expected
|
||||
try expected <|> try (withAlt pos) <|> expected
|
||||
where
|
||||
tryAlt pos = do
|
||||
t <- try alternative
|
||||
parseNoteAt pos WarningC 1102 $
|
||||
"Shells differ in parsing ambiguous " ++ prefix ++ "(((. Use spaces: " ++ prefix ++ "( (( ."
|
||||
withAlt pos = do
|
||||
t <- forgetOnFailure alternative
|
||||
warner pos
|
||||
return t
|
||||
|
||||
|
||||
prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }"
|
||||
prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}"
|
||||
readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do
|
||||
@@ -1481,7 +1547,7 @@ readHereDoc = called "here document" $ do
|
||||
-- add empty tokens for now, read the rest in readPendingHereDocs
|
||||
let doc = T_HereDoc hid dashed quoted endToken []
|
||||
addPendingHereDoc doc
|
||||
return $ T_FdRedirect fid "" doc
|
||||
return doc
|
||||
where
|
||||
stripLiteral (T_Literal _ x) = x
|
||||
stripLiteral (T_SingleQuoted _ x) = x
|
||||
@@ -1552,7 +1618,13 @@ readPendingHereDocs = do
|
||||
|
||||
|
||||
readFilename = readNormalWord
|
||||
readIoFileOp = choice [g_LESSAND, g_GREATAND, g_DGREAT, g_LESSGREAT, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ]
|
||||
readIoFileOp = choice [g_DGREAT, g_LESSGREAT, g_GREATAND, g_LESSAND, g_CLOBBER, redirToken '<' T_Less, redirToken '>' T_Greater ]
|
||||
|
||||
readIoDuplicate = try $ do
|
||||
id <- getNextId
|
||||
op <- g_GREATAND <|> g_LESSAND
|
||||
target <- readIoVariable <|> many1 digit <|> string "-"
|
||||
return $ T_IoDuplicate id op target
|
||||
|
||||
prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\""
|
||||
readIoFile = called "redirection" $ do
|
||||
@@ -1560,35 +1632,31 @@ readIoFile = called "redirection" $ do
|
||||
op <- readIoFileOp
|
||||
spacing
|
||||
file <- readFilename
|
||||
return $ T_FdRedirect id "" $ T_IoFile id op file
|
||||
return $ T_IoFile id op file
|
||||
|
||||
readIoVariable = try $ do
|
||||
char '{'
|
||||
x <- readVariableName
|
||||
char '}'
|
||||
lookAhead readIoFileOp
|
||||
return $ "{" ++ x ++ "}"
|
||||
|
||||
readIoNumber = try $ do
|
||||
x <- many1 digit <|> string "&"
|
||||
lookAhead readIoFileOp
|
||||
readIoSource = try $ do
|
||||
x <- string "&" <|> readIoVariable <|> many digit
|
||||
lookAhead $ void readIoFileOp <|> void (string "<<")
|
||||
return x
|
||||
|
||||
prop_readIoNumberRedirect = isOk readIoNumberRedirect "3>&2"
|
||||
prop_readIoNumberRedirect2 = isOk readIoNumberRedirect "2> lol"
|
||||
prop_readIoNumberRedirect3 = isOk readIoNumberRedirect "4>&-"
|
||||
prop_readIoNumberRedirect4 = isOk readIoNumberRedirect "&> lol"
|
||||
prop_readIoNumberRedirect5 = isOk readIoNumberRedirect "{foo}>&2"
|
||||
prop_readIoNumberRedirect6 = isOk readIoNumberRedirect "{foo}<&-"
|
||||
readIoNumberRedirect = do
|
||||
prop_readIoRedirect = isOk readIoRedirect "3>&2"
|
||||
prop_readIoRedirect2 = isOk readIoRedirect "2> lol"
|
||||
prop_readIoRedirect3 = isOk readIoRedirect "4>&-"
|
||||
prop_readIoRedirect4 = isOk readIoRedirect "&> lol"
|
||||
prop_readIoRedirect5 = isOk readIoRedirect "{foo}>&2"
|
||||
prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-"
|
||||
readIoRedirect = do
|
||||
id <- getNextId
|
||||
n <- readIoVariable <|> readIoNumber
|
||||
op <- readHereString <|> readHereDoc <|> readIoFile
|
||||
let actualOp = case op of T_FdRedirect _ "" x -> x
|
||||
n <- readIoSource
|
||||
redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile
|
||||
spacing
|
||||
return $ T_FdRedirect id n actualOp
|
||||
|
||||
readIoRedirect = choice [ readIoNumberRedirect, readHereString, readHereDoc, readIoFile ] `thenSkip` spacing
|
||||
return $ T_FdRedirect id n redir
|
||||
|
||||
readRedirectList = many1 readIoRedirect
|
||||
|
||||
@@ -1599,7 +1667,7 @@ readHereString = called "here string" $ do
|
||||
spacing
|
||||
id2 <- getNextId
|
||||
word <- readNormalWord
|
||||
return $ T_FdRedirect id "" $ T_HereString id2 word
|
||||
return $ T_HereString id2 word
|
||||
|
||||
readNewlineList = many1 ((linefeed <|> carriageReturn) `thenSkip` spacing)
|
||||
readLineBreak = optional readNewlineList
|
||||
@@ -1878,7 +1946,7 @@ readElifPart = called "elif clause" $ do
|
||||
pos <- getPosition
|
||||
correctElif <- elif
|
||||
unless correctElif $
|
||||
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if'."
|
||||
parseProblemAt pos ErrorC 1075 "Use 'elif' instead of 'else if' (or put 'if' on new line if nesting)."
|
||||
allspacing
|
||||
condition <- readTerm
|
||||
|
||||
@@ -2176,8 +2244,8 @@ readCompoundCommand = do
|
||||
id <- getNextId
|
||||
cmd <- choice [
|
||||
readBraceGroup,
|
||||
readTripleParenthesis "" readArithmeticExpression readSubshell,
|
||||
readArithmeticExpression,
|
||||
readAmbiguous "((" readArithmeticExpression readSubshell (\pos ->
|
||||
parseNoteAt pos WarningC 1105 "Shells disambiguate (( differently or not at all. For subshell, add spaces around ( . For ((, fix parsing errors."),
|
||||
readSubshell,
|
||||
readCondition,
|
||||
readWhileClause,
|
||||
@@ -2251,7 +2319,7 @@ readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback)
|
||||
|
||||
-- Get whatever a parser would parse as a string
|
||||
readStringForParser parser = do
|
||||
pos <- lookAhead (parser >> getPosition)
|
||||
pos <- inSeparateContext $ lookAhead (parser >> getPosition)
|
||||
readUntil pos
|
||||
where
|
||||
readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))
|
||||
@@ -2278,7 +2346,7 @@ readAssignmentWord = try $ do
|
||||
variable <- readVariableName
|
||||
optional (readNormalDollar >> parseNoteAt pos ErrorC
|
||||
1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")
|
||||
index <- optionMaybe readArrayIndex
|
||||
indices <- many readArrayIndex
|
||||
hasLeftSpace <- liftM (not . null) spacing
|
||||
pos <- getPosition
|
||||
op <- readAssignmentOp
|
||||
@@ -2290,13 +2358,13 @@ readAssignmentWord = try $ do
|
||||
parseNoteAt pos WarningC 1007
|
||||
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
|
||||
value <- readEmptyLiteral
|
||||
return $ T_Assignment id op variable index value
|
||||
return $ T_Assignment id op variable indices value
|
||||
else do
|
||||
when (hasLeftSpace || hasRightSpace) $
|
||||
parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments."
|
||||
value <- readArray <|> readNormalWord
|
||||
spacing
|
||||
return $ T_Assignment id op variable index value
|
||||
return $ T_Assignment id op variable indices value
|
||||
where
|
||||
readAssignmentOp = do
|
||||
pos <- getPosition
|
||||
@@ -2316,12 +2384,14 @@ readAssignmentWord = try $ do
|
||||
return $ T_Literal id ""
|
||||
|
||||
readArrayIndex = do
|
||||
id <- getNextId
|
||||
char '['
|
||||
optional space
|
||||
x <- readArithmeticContents
|
||||
pos <- getPosition
|
||||
str <- readStringForParser readIndexSpan
|
||||
char ']'
|
||||
return x
|
||||
return $ T_UnparsedIndex id pos str
|
||||
|
||||
readArray :: Monad m => SCParser m Token
|
||||
readArray = called "array assignment" $ do
|
||||
id <- getNextId
|
||||
char '('
|
||||
@@ -2334,7 +2404,7 @@ readArray = called "array assignment" $ do
|
||||
readIndexed = do
|
||||
id <- getNextId
|
||||
index <- try $ do
|
||||
x <- readArrayIndex
|
||||
x <- many1 readArrayIndex
|
||||
char '='
|
||||
return x
|
||||
value <- readNormalWord <|> nothing
|
||||
@@ -2477,12 +2547,12 @@ verifyEof = eof <|> choice [
|
||||
try (lookAhead p)
|
||||
action
|
||||
|
||||
prop_readScript1 = isOk readScript "#!/bin/bash\necho hello world\n"
|
||||
prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"
|
||||
prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
|
||||
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
||||
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
|
||||
readScript = do
|
||||
prop_readScript1 = isOk readScriptFile "#!/bin/bash\necho hello world\n"
|
||||
prop_readScript2 = isWarning readScriptFile "#!/bin/bash\r\necho hello world\n"
|
||||
prop_readScript3 = isWarning readScriptFile "#!/bin/bash\necho hello\xA0world"
|
||||
prop_readScript4 = isWarning readScriptFile "#!/usr/bin/perl\nfoo=("
|
||||
prop_readScript5 = isOk readScriptFile "#!/bin/bash\n#This is an empty script\n\n"
|
||||
readScriptFile = do
|
||||
id <- getNextId
|
||||
pos <- getPosition
|
||||
optional $ do
|
||||
@@ -2497,7 +2567,8 @@ readScript = do
|
||||
annotations <- readAnnotations
|
||||
commands <- withAnnotations annotations readCompoundListOrEmpty
|
||||
verifyEof
|
||||
return $ T_Annotation annotationId annotations $ T_Script id sb commands
|
||||
let script = T_Annotation annotationId annotations $ T_Script id sb commands
|
||||
reparseIndices script
|
||||
else do
|
||||
many anyChar
|
||||
return $ T_Script id sb []
|
||||
@@ -2549,6 +2620,9 @@ readScript = do
|
||||
|
||||
readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF"
|
||||
|
||||
readScript = do
|
||||
script <- readScriptFile
|
||||
reparseIndices script
|
||||
|
||||
isWarning p s = parsesCleanly p s == Just False
|
||||
isOk p s = parsesCleanly p s == Just True
|
||||
@@ -2571,13 +2645,15 @@ parseWithNotes parser = do
|
||||
state <- getState
|
||||
return (item, state)
|
||||
|
||||
compareNotes (ParseNote pos1 level1 _ s1) (ParseNote pos2 level2 _ s2) = compare (pos1, level1) (pos2, level2)
|
||||
compareNotes (ParseNote pos1 pos1' level1 _ s1) (ParseNote pos2 pos2' level2 _ s2) = compare (pos1, pos1', level1) (pos2, pos2', level2)
|
||||
sortNotes = sortBy compareNotes
|
||||
|
||||
|
||||
makeErrorFor parsecError =
|
||||
ParseNote (errorPos parsecError) ErrorC 1072 $
|
||||
ParseNote pos pos ErrorC 1072 $
|
||||
getStringFromParsec $ errorMessages parsecError
|
||||
where
|
||||
pos = errorPos parsecError
|
||||
|
||||
getStringFromParsec errors =
|
||||
case map f errors of
|
||||
@@ -2630,11 +2706,46 @@ parseShell sys name contents = do
|
||||
isName (ContextName _ _) = True
|
||||
isName _ = False
|
||||
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
||||
first (ContextName pos str) = ParseNote pos ErrorC 1073 $
|
||||
first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
|
||||
"Couldn't parse this " ++ str ++ "."
|
||||
second (ContextName pos str) = ParseNote pos InfoC 1009 $
|
||||
second (ContextName pos str) = ParseNote pos pos InfoC 1009 $
|
||||
"The mentioned parser error was in this " ++ str ++ "."
|
||||
|
||||
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
|
||||
-- depending on declare -A statements.
|
||||
reparseIndices root =
|
||||
analyze blank blank f root
|
||||
where
|
||||
associative = getAssociativeArrays root
|
||||
isAssociative s = s `elem` associative
|
||||
f (T_Assignment id mode name indices value) = do
|
||||
newIndices <- mapM (fixAssignmentIndex name) indices
|
||||
newValue <- case value of
|
||||
(T_Array id2 words) -> do
|
||||
newWords <- mapM (fixIndexElement name) words
|
||||
return $ T_Array id2 newWords
|
||||
x -> return x
|
||||
return $ T_Assignment id mode name newIndices newValue
|
||||
f t = return t
|
||||
|
||||
fixIndexElement name word =
|
||||
case word of
|
||||
T_IndexedElement id indices value -> do
|
||||
new <- mapM (fixAssignmentIndex name) indices
|
||||
return $ T_IndexedElement id new value
|
||||
otherwise -> return word
|
||||
|
||||
fixAssignmentIndex name word =
|
||||
case word of
|
||||
T_UnparsedIndex id pos src -> do
|
||||
parsed name pos src
|
||||
otherwise -> return word
|
||||
|
||||
parsed name pos src =
|
||||
if isAssociative name
|
||||
then subParse pos (called "associative array index" $ readIndexSpan) src
|
||||
else subParse pos (called "arithmetic array index expression" $ optional space >> readArithmeticContents) src
|
||||
|
||||
reattachHereDocs root map =
|
||||
doTransform f root
|
||||
where
|
||||
@@ -2644,8 +2755,8 @@ reattachHereDocs root map =
|
||||
f t = t
|
||||
|
||||
toPositionedComment :: ParseNote -> PositionedComment
|
||||
toPositionedComment (ParseNote pos severity code message) =
|
||||
PositionedComment (posToPos pos) $ Comment severity code message
|
||||
toPositionedComment (ParseNote start end severity code message) =
|
||||
PositionedComment (posToPos start) (posToPos end) $ Comment severity code message
|
||||
|
||||
posToPos :: SourcePos -> Position
|
||||
posToPos sp = Position {
|
||||
|
Reference in New Issue
Block a user