mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-09-30 08:49:20 +08:00
Compare commits
81 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 | ||
|
1d04754b37 | ||
|
13ff0a7432 | ||
|
40136fe249 | ||
|
86999ded1f | ||
|
7551a241ad | ||
|
2f0ae44de4 | ||
|
51d8caf2c9 | ||
|
f835c2d4c1 | ||
|
db0c8c2dc9 | ||
|
9911470d67 | ||
|
a5821c3a4d | ||
|
c91083354f | ||
|
2957fb64c9 | ||
|
459e30804f | ||
|
49569e10e6 | ||
|
ba0221a1da | ||
|
944313c6ba | ||
|
6af1aeb259 | ||
|
b7c9d23452 | ||
|
e792d69293 | ||
|
4d8f2eb707 | ||
|
8a3bd25f7c | ||
|
825c1b5d22 | ||
|
92473b512a | ||
|
7e75d12ce1 | ||
|
7d278c3ca1 | ||
|
5f1175fb58 | ||
|
257b794322 | ||
|
89572d3a96 | ||
|
15edcbd4d5 | ||
|
736febaa3c | ||
|
a21df2d88f | ||
|
d473fb8867 | ||
|
f754363733 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,4 +12,4 @@ cabal-dev
|
|||||||
.cabal-sandbox/
|
.cabal-sandbox/
|
||||||
cabal.sandbox.config
|
cabal.sandbox.config
|
||||||
cabal.config
|
cabal.config
|
||||||
|
.stack-work
|
||||||
|
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/"]
|
20
README.md
20
README.md
@@ -7,10 +7,10 @@ ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell
|
|||||||
The goals of ShellCheck are
|
The goals of ShellCheck are
|
||||||
|
|
||||||
- To point out and clarify typical beginner's syntax issues
|
- To point out and clarify typical beginner's syntax issues
|
||||||
that causes a shell to give cryptic error messages.
|
that cause a shell to give cryptic error messages.
|
||||||
|
|
||||||
- To point out and clarify typical intermediate level semantic problems
|
- To point out and clarify typical intermediate level semantic problems
|
||||||
that causes a shell to behave strangely and counter-intuitively.
|
that cause a shell to behave strangely and counter-intuitively.
|
||||||
|
|
||||||
- To point out subtle caveats, corner cases and pitfalls that may cause an
|
- To point out subtle caveats, corner cases and pitfalls that may cause an
|
||||||
advanced user's otherwise working script to fail under future circumstances.
|
advanced user's otherwise working script to fail under future circumstances.
|
||||||
@@ -48,13 +48,13 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
|||||||
|
|
||||||
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
||||||
|
|
||||||
* Most other editors, through [GCC error compatibility](blob/master/shellcheck.1.md#user-content-formats).
|
* Most other editors, through [GCC error compatibility](shellcheck.1.md#user-content-formats).
|
||||||
|
|
||||||
|
|
||||||
#### In your build or test suites
|
#### In your build or test suites
|
||||||
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
|
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
|
||||||
|
|
||||||
Use ShellCheck's exit code, or it's [CheckStyle compatible XML output](blob/master/shellcheck.1.md#user-content-formats). There's also a simple JSON output format for easy integration.
|
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.
|
||||||
|
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
@@ -70,6 +70,10 @@ On Debian based distros:
|
|||||||
|
|
||||||
apt-get install shellcheck
|
apt-get install shellcheck
|
||||||
|
|
||||||
|
On Gentoo based distros:
|
||||||
|
|
||||||
|
emerge --ask shellcheck
|
||||||
|
|
||||||
On Fedora based distros:
|
On Fedora based distros:
|
||||||
|
|
||||||
dnf install ShellCheck
|
dnf install ShellCheck
|
||||||
@@ -98,7 +102,7 @@ or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
|||||||
|
|
||||||
## Compiling from source
|
## Compiling from source
|
||||||
|
|
||||||
This sections describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
|
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
|
||||||
|
|
||||||
|
|
||||||
#### Installing Cabal
|
#### Installing Cabal
|
||||||
@@ -279,7 +283,7 @@ ShellCheck recognizes a menagerie of other issues:
|
|||||||
#!/bin/bash -x -e # Common shebang errors
|
#!/bin/bash -x -e # Common shebang errors
|
||||||
echo $((n/180*100)) # Unnecessary loss of precision
|
echo $((n/180*100)) # Unnecessary loss of precision
|
||||||
ls *[:digit:].txt # Bad character class globs
|
ls *[:digit:].txt # Bad character class globs
|
||||||
sed 's/foo/bar/ file > file # Redirecting to input
|
sed 's/foo/bar/' file > file # Redirecting to input
|
||||||
|
|
||||||
|
|
||||||
## Testimonials
|
## Testimonials
|
||||||
@@ -292,14 +296,14 @@ Alexander Tarasikov,
|
|||||||
|
|
||||||
## Reporting bugs
|
## Reporting bugs
|
||||||
|
|
||||||
Please use the Github issue tracker for any bugs or feature suggestions:
|
Please use the GitHub issue tracker for any bugs or feature suggestions:
|
||||||
|
|
||||||
https://github.com/koalaman/shellcheck/issues
|
https://github.com/koalaman/shellcheck/issues
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please submit patches to code or documentation as Github pull requests!
|
Please submit patches to code or documentation as GitHub pull requests!
|
||||||
|
|
||||||
Contributions must be licensed under the GNU GPLv3.
|
Contributions must be licensed under the GNU GPLv3.
|
||||||
The contributor retains the copyright.
|
The contributor retains the copyright.
|
||||||
|
15
Setup.hs
15
Setup.hs
@@ -8,21 +8,13 @@ import Distribution.Simple (
|
|||||||
simpleUserHooks )
|
simpleUserHooks )
|
||||||
import Distribution.Simple.Setup ( SDistFlags )
|
import Distribution.Simple.Setup ( SDistFlags )
|
||||||
|
|
||||||
-- | This requires the process package from,
|
import System.Process ( system )
|
||||||
--
|
|
||||||
-- https://hackage.haskell.org/package/process
|
|
||||||
--
|
|
||||||
import System.Process ( callCommand )
|
|
||||||
|
|
||||||
|
|
||||||
-- | This will use almost the default implementation, except we switch
|
|
||||||
-- out the default pre-sdist hook with our own, 'myPreSDist'.
|
|
||||||
--
|
|
||||||
main = defaultMainWithHooks myHooks
|
main = defaultMainWithHooks myHooks
|
||||||
where
|
where
|
||||||
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
myHooks = simpleUserHooks { preSDist = myPreSDist }
|
||||||
|
|
||||||
|
|
||||||
-- | This hook will be executed before e.g. @cabal sdist@. It runs
|
-- | This hook will be executed before e.g. @cabal sdist@. It runs
|
||||||
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
|
-- pandoc to create the man page from shellcheck.1.md. If the pandoc
|
||||||
-- command is not found, this will fail with an error message:
|
-- command is not found, this will fail with an error message:
|
||||||
@@ -35,9 +27,10 @@ main = defaultMainWithHooks myHooks
|
|||||||
--
|
--
|
||||||
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
|
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
|
||||||
myPreSDist _ _ = do
|
myPreSDist _ _ = do
|
||||||
putStrLn "Building the man page..."
|
putStrLn "Building the man page (shellcheck.1) with pandoc..."
|
||||||
putStrLn pandoc_cmd
|
putStrLn pandoc_cmd
|
||||||
callCommand pandoc_cmd
|
result <- system pandoc_cmd
|
||||||
|
putStrLn $ "pandoc exited with " ++ show result
|
||||||
return emptyHookedBuildInfo
|
return emptyHookedBuildInfo
|
||||||
where
|
where
|
||||||
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
Version: 0.4.2
|
Version: 0.4.5
|
||||||
Synopsis: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
License: GPL-3
|
License: GPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
@@ -44,13 +44,17 @@ library
|
|||||||
mtl >= 2.2.1,
|
mtl >= 2.2.1,
|
||||||
parsec,
|
parsec,
|
||||||
regex-tdfa,
|
regex-tdfa,
|
||||||
QuickCheck >= 2.7.4
|
QuickCheck >= 2.7.4,
|
||||||
|
-- When cabal supports it, move this to setup-depends:
|
||||||
|
process
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
ShellCheck.AST
|
ShellCheck.AST
|
||||||
ShellCheck.ASTLib
|
ShellCheck.ASTLib
|
||||||
ShellCheck.Analytics
|
ShellCheck.Analytics
|
||||||
ShellCheck.Analyzer
|
ShellCheck.Analyzer
|
||||||
|
ShellCheck.AnalyzerLib
|
||||||
ShellCheck.Checker
|
ShellCheck.Checker
|
||||||
|
ShellCheck.Checks.Commands
|
||||||
ShellCheck.Data
|
ShellCheck.Data
|
||||||
ShellCheck.Formatter.Format
|
ShellCheck.Formatter.Format
|
||||||
ShellCheck.Formatter.CheckStyle
|
ShellCheck.Formatter.CheckStyle
|
||||||
|
@@ -21,6 +21,7 @@ module ShellCheck.AST where
|
|||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
|
import Text.Parsec
|
||||||
import qualified ShellCheck.Regex as Re
|
import qualified ShellCheck.Regex as Re
|
||||||
|
|
||||||
data Id = Id Int deriving (Show, Eq, Ord)
|
data Id = Id Int deriving (Show, Eq, Ord)
|
||||||
@@ -34,6 +35,7 @@ data CaseType = CaseBreak | CaseFallThrough | CaseContinue deriving (Show, Eq)
|
|||||||
|
|
||||||
data Token =
|
data Token =
|
||||||
TA_Binary Id String Token Token
|
TA_Binary Id String Token Token
|
||||||
|
| TA_Assignment Id String Token Token
|
||||||
| TA_Expansion Id [Token]
|
| TA_Expansion Id [Token]
|
||||||
| TA_Index Id Token
|
| TA_Index Id Token
|
||||||
| TA_Sequence Id [Token]
|
| TA_Sequence Id [Token]
|
||||||
@@ -49,8 +51,10 @@ data Token =
|
|||||||
| T_AndIf Id (Token) (Token)
|
| T_AndIf Id (Token) (Token)
|
||||||
| T_Arithmetic Id Token
|
| T_Arithmetic Id Token
|
||||||
| T_Array Id [Token]
|
| T_Array Id [Token]
|
||||||
| T_IndexedElement Id Token Token
|
| T_IndexedElement Id [Token] Token
|
||||||
| T_Assignment Id AssignmentMode String (Maybe 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_Backgrounded Id Token
|
||||||
| T_Backticked Id [Token]
|
| T_Backticked Id [Token]
|
||||||
| T_Bang Id
|
| T_Bang Id
|
||||||
@@ -95,6 +99,7 @@ data Token =
|
|||||||
| T_IfExpression Id [([Token],[Token])] [Token]
|
| T_IfExpression Id [([Token],[Token])] [Token]
|
||||||
| T_In Id
|
| T_In Id
|
||||||
| T_IoFile Id Token Token
|
| T_IoFile Id Token Token
|
||||||
|
| T_IoDuplicate Id Token String
|
||||||
| T_LESSAND Id
|
| T_LESSAND Id
|
||||||
| T_LESSGREAT Id
|
| T_LESSGREAT Id
|
||||||
| T_Lbrace Id
|
| T_Lbrace Id
|
||||||
@@ -129,7 +134,11 @@ data Token =
|
|||||||
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
|
| T_Include Id Token Token -- . & source: SimpleCommand T_Script
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data Annotation = DisableComment Integer | SourceOverride String deriving (Show, Eq)
|
data Annotation =
|
||||||
|
DisableComment Integer
|
||||||
|
| SourceOverride String
|
||||||
|
| ShellOverride String
|
||||||
|
deriving (Show, Eq)
|
||||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||||
|
|
||||||
-- This is an abomination.
|
-- This is an abomination.
|
||||||
@@ -140,7 +149,7 @@ tokenEquals a b = kludge a == kludge b
|
|||||||
instance Eq Token where
|
instance Eq Token where
|
||||||
(==) = tokenEquals
|
(==) = 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 =
|
analyze f g i =
|
||||||
round
|
round
|
||||||
where
|
where
|
||||||
@@ -148,7 +157,7 @@ analyze f g i =
|
|||||||
f t
|
f t
|
||||||
newT <- delve t
|
newT <- delve t
|
||||||
g t
|
g t
|
||||||
return . i $ newT
|
i newT
|
||||||
roundAll = mapM round
|
roundAll = mapM round
|
||||||
|
|
||||||
roundMaybe Nothing = return Nothing
|
roundMaybe Nothing = return Nothing
|
||||||
@@ -162,7 +171,7 @@ analyze f g i =
|
|||||||
dll l m v = do
|
dll l m v = do
|
||||||
x <- roundAll l
|
x <- roundAll l
|
||||||
y <- roundAll m
|
y <- roundAll m
|
||||||
return $ v x m
|
return $ v x y
|
||||||
d1 t v = do
|
d1 t v = do
|
||||||
x <- round t
|
x <- round t
|
||||||
return $ v x
|
return $ v x
|
||||||
@@ -181,14 +190,18 @@ analyze f g i =
|
|||||||
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
|
||||||
delve (T_DollarBracket id c) = d1 c $ T_DollarBracket 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_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_HereString id word) = d1 word $ T_HereString id
|
||||||
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
delve (T_FdRedirect id v t) = d1 t $ T_FdRedirect id v
|
||||||
delve (T_Assignment id mode var index value) = do
|
delve (T_Assignment id mode var indices value) = do
|
||||||
a <- roundMaybe index
|
a <- roundAll indices
|
||||||
b <- round value
|
b <- round value
|
||||||
return $ T_Assignment id mode var a b
|
return $ T_Assignment id mode var a b
|
||||||
delve (T_Array id t) = dl t $ T_Array id
|
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
|
delve (T_Redirecting id redirs cmd) = do
|
||||||
a <- roundAll redirs
|
a <- roundAll redirs
|
||||||
b <- round cmd
|
b <- round cmd
|
||||||
@@ -246,6 +259,7 @@ analyze f g i =
|
|||||||
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
|
delve (TC_Noary id typ token) = d1 token $ TC_Noary id typ
|
||||||
|
|
||||||
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
|
delve (TA_Binary id op t1 t2) = d2 t1 t2 $ TA_Binary id op
|
||||||
|
delve (TA_Assignment id op t1 t2) = d2 t1 t2 $ TA_Assignment id op
|
||||||
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
|
delve (TA_Unary id op t1) = d1 t1 $ TA_Unary id op
|
||||||
delve (TA_Sequence id l) = dl l $ TA_Sequence id
|
delve (TA_Sequence id l) = dl l $ TA_Sequence id
|
||||||
delve (TA_Trinary id t1 t2 t3) = do
|
delve (TA_Trinary id t1 t2 t3) = do
|
||||||
@@ -306,6 +320,7 @@ getId t = case t of
|
|||||||
T_BraceExpansion id _ -> id
|
T_BraceExpansion id _ -> id
|
||||||
T_DollarBraceCommandExpansion id _ -> id
|
T_DollarBraceCommandExpansion id _ -> id
|
||||||
T_IoFile id _ _ -> id
|
T_IoFile id _ _ -> id
|
||||||
|
T_IoDuplicate id _ _ -> id
|
||||||
T_HereDoc id _ _ _ _ -> id
|
T_HereDoc id _ _ _ _ -> id
|
||||||
T_HereString id _ -> id
|
T_HereString id _ -> id
|
||||||
T_FdRedirect id _ _ -> id
|
T_FdRedirect id _ _ -> id
|
||||||
@@ -340,6 +355,7 @@ getId t = case t of
|
|||||||
TC_Unary id _ _ _ -> id
|
TC_Unary id _ _ _ -> id
|
||||||
TC_Noary id _ _ -> id
|
TC_Noary id _ _ -> id
|
||||||
TA_Binary id _ _ _ -> id
|
TA_Binary id _ _ _ -> id
|
||||||
|
TA_Assignment id _ _ _ -> id
|
||||||
TA_Unary id _ _ -> id
|
TA_Unary id _ _ -> id
|
||||||
TA_Sequence id _ -> id
|
TA_Sequence id _ -> id
|
||||||
TA_Trinary id _ _ _ -> id
|
TA_Trinary id _ _ _ -> id
|
||||||
@@ -356,10 +372,11 @@ getId t = case t of
|
|||||||
T_CoProc id _ _ -> id
|
T_CoProc id _ _ -> id
|
||||||
T_CoProcBody id _ -> id
|
T_CoProcBody id _ -> id
|
||||||
T_Include id _ _ -> id
|
T_Include id _ _ -> id
|
||||||
|
T_UnparsedIndex id _ _ -> id
|
||||||
|
|
||||||
blank :: Monad m => Token -> m ()
|
blank :: Monad m => Token -> m ()
|
||||||
blank = const $ return ()
|
blank = const $ return ()
|
||||||
doAnalysis f = analyze f blank id
|
doAnalysis f = analyze f blank (return . id)
|
||||||
doStackAnalysis startToken endToken = analyze startToken endToken id
|
doStackAnalysis startToken endToken = analyze startToken endToken (return . id)
|
||||||
doTransform i = runIdentity . analyze blank blank i
|
doTransform i = runIdentity . analyze blank blank (return . i)
|
||||||
|
|
||||||
|
@@ -21,6 +21,7 @@ module ShellCheck.ASTLib where
|
|||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
|
|
||||||
|
import Control.Monad.Writer
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
@@ -54,6 +55,8 @@ isGlob _ = False
|
|||||||
-- Is this shell word a constant?
|
-- Is this shell word a constant?
|
||||||
isConstant token =
|
isConstant token =
|
||||||
case token of
|
case token of
|
||||||
|
-- This ignores some cases like ~"foo":
|
||||||
|
T_NormalWord _ (T_Literal _ ('~':_) : _) -> False
|
||||||
T_NormalWord _ l -> all isConstant l
|
T_NormalWord _ l -> all isConstant l
|
||||||
T_DoubleQuoted _ l -> all isConstant l
|
T_DoubleQuoted _ l -> all isConstant l
|
||||||
T_SingleQuoted _ _ -> True
|
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]
|
-- 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_NormalWord _ l) = concatMap getWordParts l
|
||||||
getWordParts (T_DoubleQuoted _ l) = 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]
|
getWordParts other = [other]
|
||||||
|
|
||||||
-- Return a list of NormalWords that would result from brace expansion
|
-- 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
|
braceExpand item
|
||||||
part x = return x
|
part x = return x
|
||||||
|
|
||||||
-- Maybe get the command name of a token representing a command
|
-- Maybe get a SimpleCommand from immediate wrappers like T_Redirections
|
||||||
getCommandName t =
|
getCommand t =
|
||||||
case t of
|
case t of
|
||||||
T_Redirecting _ _ w -> getCommandName w
|
T_Redirecting _ _ w -> getCommand w
|
||||||
T_SimpleCommand _ _ (w:_) -> getLiteralString w
|
T_SimpleCommand _ _ (w:_) -> return t
|
||||||
T_Annotation _ _ t -> getCommandName t
|
T_Annotation _ _ t -> getCommand t
|
||||||
otherwise -> Nothing
|
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
|
-- Get the basename of a token representing a command
|
||||||
getCommandBasename = liftM basename . getCommandName
|
getCommandBasename = liftM basename . getCommandName
|
||||||
where
|
where
|
||||||
@@ -237,8 +260,8 @@ isOnlyRedirection t =
|
|||||||
|
|
||||||
isFunction t = case t of T_Function {} -> True; _ -> False
|
isFunction t = case t of T_Function {} -> True; _ -> False
|
||||||
|
|
||||||
-- Get the list of commands from tokens that contain them, such as
|
-- Get the lists of commands from tokens that contain them, such as
|
||||||
-- the body of while loops and if statements.
|
-- the body of while loops or branches of if statements.
|
||||||
getCommandSequences t =
|
getCommandSequences t =
|
||||||
case t of
|
case t of
|
||||||
T_Script _ _ cmds -> [cmds]
|
T_Script _ _ cmds -> [cmds]
|
||||||
@@ -251,3 +274,22 @@ getCommandSequences t =
|
|||||||
T_IfExpression _ thens elses -> map snd thens ++ [elses]
|
T_IfExpression _ thens elses -> map snd thens ++ [elses]
|
||||||
otherwise -> []
|
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
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -19,9 +19,18 @@
|
|||||||
-}
|
-}
|
||||||
module ShellCheck.Analyzer (analyzeScript) where
|
module ShellCheck.Analyzer (analyzeScript) where
|
||||||
|
|
||||||
import ShellCheck.Interface
|
|
||||||
import ShellCheck.Analytics
|
import ShellCheck.Analytics
|
||||||
|
import ShellCheck.AnalyzerLib
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import Data.List
|
||||||
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
|
||||||
|
|
||||||
-- TODO: Clean up the cruft this is layered on
|
-- TODO: Clean up the cruft this is layered on
|
||||||
analyzeScript :: AnalysisSpec -> AnalysisResult
|
analyzeScript :: AnalysisSpec -> AnalysisResult
|
||||||
analyzeScript = runAnalytics
|
analyzeScript spec = AnalysisResult {
|
||||||
|
arComments =
|
||||||
|
filterByAnnotation (asScript spec) . nub $
|
||||||
|
runAnalytics spec
|
||||||
|
++ ShellCheck.Checks.Commands.runChecks spec
|
||||||
|
}
|
||||||
|
668
ShellCheck/AnalyzerLib.hs
Normal file
668
ShellCheck/AnalyzerLib.hs
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 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.AnalyzerLib where
|
||||||
|
import ShellCheck.AST
|
||||||
|
import ShellCheck.ASTLib
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Regex
|
||||||
|
|
||||||
|
import Control.Arrow (first)
|
||||||
|
import Control.Monad.Identity
|
||||||
|
import Control.Monad.Reader
|
||||||
|
import Control.Monad.State
|
||||||
|
import Control.Monad.Writer
|
||||||
|
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)
|
||||||
|
|
||||||
|
type Analysis = ReaderT Parameters (Writer [TokenComment]) ()
|
||||||
|
|
||||||
|
|
||||||
|
data Parameters = Parameters {
|
||||||
|
variableFlow :: [StackData],
|
||||||
|
parentMap :: Map.Map Id Token,
|
||||||
|
shellType :: Shell,
|
||||||
|
shellTypeSpecified :: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
data Scope = SubshellScope String | NoneScope deriving (Show, Eq)
|
||||||
|
data StackData =
|
||||||
|
StackScope Scope
|
||||||
|
| StackScopeEnd
|
||||||
|
-- (Base expression, specific position, var name, assigned values)
|
||||||
|
| Assignment (Token, Token, String, DataType)
|
||||||
|
| Reference (Token, Token, String)
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data DataType = DataString DataSource | DataArray DataSource
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data DataSource = SourceFrom [Token] | SourceExternal | SourceDeclaration | SourceInteger
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data VariableState = Dead Token String | Alive deriving (Show)
|
||||||
|
|
||||||
|
defaultSpec root = AnalysisSpec {
|
||||||
|
asScript = root,
|
||||||
|
asShellType = Nothing,
|
||||||
|
asExecutionMode = Executed
|
||||||
|
}
|
||||||
|
|
||||||
|
pScript s =
|
||||||
|
let
|
||||||
|
pSpec = ParseSpec {
|
||||||
|
psFilename = "script",
|
||||||
|
psScript = s
|
||||||
|
}
|
||||||
|
in prRoot . runIdentity $ parseScript (mockedSystemInterface []) pSpec
|
||||||
|
|
||||||
|
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||||
|
makeComment severity id code note =
|
||||||
|
TokenComment id $ Comment severity code note
|
||||||
|
|
||||||
|
addComment note = tell [note]
|
||||||
|
|
||||||
|
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
|
||||||
|
warn id code str = addComment $ makeComment WarningC id code str
|
||||||
|
err id code str = addComment $ makeComment ErrorC id code str
|
||||||
|
info id code str = addComment $ makeComment InfoC id code str
|
||||||
|
style id code str = addComment $ makeComment StyleC id code str
|
||||||
|
|
||||||
|
makeParameters spec =
|
||||||
|
let params = Parameters {
|
||||||
|
shellType = fromMaybe (determineShell root) $ asShellType spec,
|
||||||
|
shellTypeSpecified = isJust $ asShellType spec,
|
||||||
|
parentMap = getParentTree root,
|
||||||
|
variableFlow =
|
||||||
|
getVariableFlow (shellType params) (parentMap params) root
|
||||||
|
} in params
|
||||||
|
where root = asScript spec
|
||||||
|
|
||||||
|
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
|
||||||
|
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
|
||||||
|
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
|
||||||
|
prop_determineShell3 = determineShell (fromJust $ pScript "#!/bin/sh -e") == Sh
|
||||||
|
prop_determineShell4 = determineShell (fromJust $ pScript
|
||||||
|
"#!/bin/ksh\n#shellcheck shell=sh\nfoo") == Sh
|
||||||
|
prop_determineShell5 = determineShell (fromJust $ pScript
|
||||||
|
"#shellcheck shell=sh\nfoo") == Sh
|
||||||
|
prop_determineShell6 = determineShell (fromJust $ pScript "#! /bin/sh") == Sh
|
||||||
|
determineShell t = fromMaybe Bash $ do
|
||||||
|
shellString <- foldl mplus Nothing $ getCandidates t
|
||||||
|
shellForExecutable shellString
|
||||||
|
where
|
||||||
|
forAnnotation t =
|
||||||
|
case t of
|
||||||
|
(ShellOverride s) -> return s
|
||||||
|
_ -> fail ""
|
||||||
|
getCandidates :: Token -> [Maybe String]
|
||||||
|
getCandidates t@(T_Script {}) = [Just $ fromShebang t]
|
||||||
|
getCandidates (T_Annotation _ annotations s) =
|
||||||
|
map forAnnotation annotations ++
|
||||||
|
[Just $ fromShebang s]
|
||||||
|
fromShebang (T_Script _ s t) = shellFor s
|
||||||
|
|
||||||
|
shellFor s | "/env " `isInfixOf` s = head (drop 1 (words s)++[""])
|
||||||
|
shellFor s | ' ' `elem` s = shellFor $ takeWhile (/= ' ') s
|
||||||
|
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
||||||
|
|
||||||
|
|
||||||
|
--- Context seeking
|
||||||
|
|
||||||
|
getParentTree t =
|
||||||
|
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
||||||
|
where
|
||||||
|
pre t = modify (first ((:) t))
|
||||||
|
post t = do
|
||||||
|
(_:rest, map) <- get
|
||||||
|
case rest of [] -> put (rest, map)
|
||||||
|
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||||
|
|
||||||
|
getTokenMap t =
|
||||||
|
execState (doAnalysis f t) Map.empty
|
||||||
|
where
|
||||||
|
f t = modify (Map.insert (getId t) t)
|
||||||
|
|
||||||
|
|
||||||
|
-- Is this node self quoting for a regular element?
|
||||||
|
isQuoteFree = isQuoteFreeNode False
|
||||||
|
|
||||||
|
-- Is this node striclty self quoting, for array expansions
|
||||||
|
isStrictlyQuoteFree = isQuoteFreeNode True
|
||||||
|
|
||||||
|
|
||||||
|
isQuoteFreeNode strict tree t =
|
||||||
|
(isQuoteFreeElement t == Just True) ||
|
||||||
|
head (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t) ++ [False])
|
||||||
|
where
|
||||||
|
-- Is this node self-quoting in itself?
|
||||||
|
isQuoteFreeElement t =
|
||||||
|
case t of
|
||||||
|
T_Assignment {} -> return True
|
||||||
|
T_FdRedirect {} -> return True
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
-- Are any subnodes inherently self-quoting?
|
||||||
|
isQuoteFreeContext t =
|
||||||
|
case t of
|
||||||
|
TC_Noary _ 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_DoubleQuoted _ _ -> return True
|
||||||
|
T_DollarDoubleQuoted _ _ -> return True
|
||||||
|
T_CaseExpression {} -> return True
|
||||||
|
T_HereDoc {} -> return True
|
||||||
|
T_DollarBraced {} -> return True
|
||||||
|
-- When non-strict, pragmatically assume it's desirable to split here
|
||||||
|
T_ForIn {} -> return (not strict)
|
||||||
|
T_SelectIn {} -> return (not strict)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
isParamTo tree cmd =
|
||||||
|
go
|
||||||
|
where
|
||||||
|
go x = case Map.lookup (getId x) tree of
|
||||||
|
Nothing -> False
|
||||||
|
Just parent -> check parent
|
||||||
|
check t =
|
||||||
|
case t of
|
||||||
|
T_SingleQuoted _ _ -> go t
|
||||||
|
T_DoubleQuoted _ _ -> go t
|
||||||
|
T_NormalWord _ _ -> go t
|
||||||
|
T_SimpleCommand {} -> isCommand t cmd
|
||||||
|
T_Redirecting {} -> isCommand t cmd
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
getClosestCommand tree t =
|
||||||
|
msum . map getCommand $ getPath tree t
|
||||||
|
where
|
||||||
|
getCommand t@(T_Redirecting {}) = return t
|
||||||
|
getCommand _ = Nothing
|
||||||
|
|
||||||
|
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||||
|
where
|
||||||
|
go currentId (T_NormalWord id [word]:rest)
|
||||||
|
| currentId == getId word = go id rest
|
||||||
|
go currentId (T_DoubleQuoted id [word]:rest)
|
||||||
|
| currentId == getId word = go id rest
|
||||||
|
go currentId (T_SimpleCommand _ _ (word:_):_)
|
||||||
|
| currentId == getId word = True
|
||||||
|
go _ _ = False
|
||||||
|
|
||||||
|
-- A list of the element and all its parents
|
||||||
|
getPath tree t = t :
|
||||||
|
case Map.lookup (getId t) tree of
|
||||||
|
Nothing -> []
|
||||||
|
Just parent -> getPath tree parent
|
||||||
|
|
||||||
|
isParentOf tree parent child =
|
||||||
|
elem (getId parent) . map getId $ getPath tree child
|
||||||
|
|
||||||
|
parents params = getPath (parentMap params)
|
||||||
|
|
||||||
|
pathTo t = do
|
||||||
|
parents <- reader parentMap
|
||||||
|
return $ getPath parents t
|
||||||
|
|
||||||
|
-- Check whether a word is entirely output from a single command
|
||||||
|
tokenIsJustCommandOutput t = case t of
|
||||||
|
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
|
||||||
|
T_NormalWord id [T_DoubleQuoted _ [T_DollarExpansion _ cmds]] -> check cmds
|
||||||
|
T_NormalWord id [T_Backticked _ cmds] -> check cmds
|
||||||
|
T_NormalWord id [T_DoubleQuoted _ [T_Backticked _ cmds]] -> check cmds
|
||||||
|
_ -> False
|
||||||
|
where
|
||||||
|
check [x] = not $ isOnlyRedirection x
|
||||||
|
check _ = False
|
||||||
|
|
||||||
|
-- TODO: Replace this with a proper Control Flow Graph
|
||||||
|
getVariableFlow shell parents t =
|
||||||
|
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
||||||
|
in reverse stack
|
||||||
|
where
|
||||||
|
startScope t =
|
||||||
|
let scopeType = leadType shell parents t
|
||||||
|
in do
|
||||||
|
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
|
||||||
|
when (assignFirst t) $ setWritten t
|
||||||
|
|
||||||
|
endScope t =
|
||||||
|
let scopeType = leadType shell parents t
|
||||||
|
in do
|
||||||
|
setRead t
|
||||||
|
unless (assignFirst t) $ setWritten t
|
||||||
|
when (scopeType /= NoneScope) $ modify (StackScopeEnd:)
|
||||||
|
|
||||||
|
assignFirst (T_ForIn {}) = True
|
||||||
|
assignFirst (T_SelectIn {}) = True
|
||||||
|
assignFirst _ = False
|
||||||
|
|
||||||
|
setRead t =
|
||||||
|
let read = getReferencedVariables parents t
|
||||||
|
in mapM_ (\v -> modify (Reference v:)) read
|
||||||
|
|
||||||
|
setWritten t =
|
||||||
|
let written = getModifiedVariables t
|
||||||
|
in mapM_ (\v -> modify (Assignment v:)) written
|
||||||
|
|
||||||
|
|
||||||
|
leadType shell parents t =
|
||||||
|
case t of
|
||||||
|
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
|
||||||
|
T_Backticked _ _ -> SubshellScope "`..` expansion"
|
||||||
|
T_Backgrounded _ _ -> SubshellScope "backgrounding &"
|
||||||
|
T_Subshell _ _ -> SubshellScope "(..) group"
|
||||||
|
T_CoProcBody _ _ -> SubshellScope "coproc"
|
||||||
|
T_Redirecting {} ->
|
||||||
|
if fromMaybe False causesSubshell
|
||||||
|
then SubshellScope "pipeline"
|
||||||
|
else NoneScope
|
||||||
|
_ -> NoneScope
|
||||||
|
where
|
||||||
|
parentPipeline = do
|
||||||
|
parent <- Map.lookup (getId t) parents
|
||||||
|
case parent of
|
||||||
|
T_Pipeline {} -> return parent
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
causesSubshell = do
|
||||||
|
(T_Pipeline _ _ list) <- parentPipeline
|
||||||
|
if length list <= 1
|
||||||
|
then return False
|
||||||
|
else if lastCreatesSubshell
|
||||||
|
then return True
|
||||||
|
else return . not $ (getId . head $ reverse list) == getId t
|
||||||
|
|
||||||
|
lastCreatesSubshell =
|
||||||
|
case shell of
|
||||||
|
Bash -> True
|
||||||
|
Dash -> True
|
||||||
|
Sh -> True
|
||||||
|
Ksh -> False
|
||||||
|
|
||||||
|
getModifiedVariables t =
|
||||||
|
case t of
|
||||||
|
T_SimpleCommand _ vars [] ->
|
||||||
|
concatMap (\x -> case x of
|
||||||
|
T_Assignment id _ name _ w ->
|
||||||
|
[(x, x, name, dataTypeFrom DataString w)]
|
||||||
|
_ -> []
|
||||||
|
) vars
|
||||||
|
c@(T_SimpleCommand {}) ->
|
||||||
|
getModifiedVariableCommand c
|
||||||
|
|
||||||
|
TA_Unary _ "++|" var -> maybeToList $ do
|
||||||
|
name <- getLiteralString var
|
||||||
|
return (t, t, name, DataString $ SourceFrom [t])
|
||||||
|
TA_Unary _ "|++" var -> maybeToList $ do
|
||||||
|
name <- getLiteralString var
|
||||||
|
return (t, t, name, DataString $ SourceFrom [t])
|
||||||
|
TA_Assignment _ op lhs rhs -> maybeToList $ do
|
||||||
|
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||||
|
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]
|
||||||
|
|
||||||
|
t@(T_CoProc _ name _) ->
|
||||||
|
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||||
|
|
||||||
|
--Points to 'for' rather than variable
|
||||||
|
T_ForIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||||
|
T_SelectIn id str words _ -> [(t, t, str, DataString $ SourceFrom words)]
|
||||||
|
_ -> []
|
||||||
|
|
||||||
|
isClosingFileOp op =
|
||||||
|
case op of
|
||||||
|
T_IoFile _ (T_GREATAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
|
||||||
|
T_IoFile _ (T_LESSAND _) (T_NormalWord _ [T_Literal _ "-"]) -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
|
||||||
|
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||||
|
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||||
|
case x of
|
||||||
|
"export" -> if "f" `elem` flags
|
||||||
|
then []
|
||||||
|
else concatMap getReference rest
|
||||||
|
"declare" -> if any (`elem` flags) ["x", "p"]
|
||||||
|
then concatMap getReference rest
|
||||||
|
else []
|
||||||
|
"readonly" ->
|
||||||
|
if any (`elem` flags) ["f", "p"]
|
||||||
|
then []
|
||||||
|
else concatMap getReference rest
|
||||||
|
"trap" ->
|
||||||
|
case rest of
|
||||||
|
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
|
||||||
|
_ -> []
|
||||||
|
_ -> []
|
||||||
|
where
|
||||||
|
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
|
||||||
|
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
|
||||||
|
getReference _ = []
|
||||||
|
flags = map snd $ getAllFlags base
|
||||||
|
|
||||||
|
getReferencedVariableCommand _ = []
|
||||||
|
|
||||||
|
getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||||
|
filter (\(_,_,s,_) -> not ("-" `isPrefixOf` s)) $
|
||||||
|
case x of
|
||||||
|
"read" ->
|
||||||
|
let params = map getLiteral rest in
|
||||||
|
catMaybes . takeWhile isJust . reverse $ params
|
||||||
|
"getopts" ->
|
||||||
|
case rest of
|
||||||
|
opts:var:_ -> maybeToList $ getLiteral var
|
||||||
|
_ -> []
|
||||||
|
|
||||||
|
"let" -> concatMap letParamToLiteral rest
|
||||||
|
|
||||||
|
"export" ->
|
||||||
|
if "f" `elem` flags then [] else concatMap getModifierParamString rest
|
||||||
|
|
||||||
|
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
|
||||||
|
"typeset" -> declaredVars
|
||||||
|
|
||||||
|
"local" -> 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)
|
||||||
|
|
||||||
|
"printf" -> maybeToList $ getPrintfVariable rest
|
||||||
|
|
||||||
|
"mapfile" -> maybeToList $ getMapfileArray base rest
|
||||||
|
"readarray" -> maybeToList $ getMapfileArray base rest
|
||||||
|
|
||||||
|
_ -> []
|
||||||
|
where
|
||||||
|
flags = map snd $ getAllFlags base
|
||||||
|
stripEquals s = let rest = dropWhile (/= '=') s in
|
||||||
|
if rest == "" then "" else tail rest
|
||||||
|
stripEqualsFrom (T_NormalWord id1 (T_Literal id2 s:rs)) =
|
||||||
|
T_NormalWord id1 (T_Literal id2 (stripEquals s):rs)
|
||||||
|
stripEqualsFrom (T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 s]]) =
|
||||||
|
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
|
||||||
|
stripEqualsFrom t = t
|
||||||
|
|
||||||
|
declaredVars = concatMap (getModifierParam defaultType) rest
|
||||||
|
where
|
||||||
|
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||||
|
|
||||||
|
getLiteral t = do
|
||||||
|
s <- getLiteralString t
|
||||||
|
when ("-" `isPrefixOf` s) $ fail "argument"
|
||||||
|
return (base, t, s, DataString SourceExternal)
|
||||||
|
|
||||||
|
getModifierParamString = getModifierParam DataString
|
||||||
|
|
||||||
|
getModifierParam def t@(T_Assignment _ _ name _ value) =
|
||||||
|
[(base, t, name, dataTypeFrom def value)]
|
||||||
|
getModifierParam def t@(T_NormalWord {}) = maybeToList $ do
|
||||||
|
name <- getLiteralString t
|
||||||
|
guard $ isVariableName name
|
||||||
|
return (base, t, name, def SourceDeclaration)
|
||||||
|
getModifierParam _ _ = []
|
||||||
|
|
||||||
|
letParamToLiteral token =
|
||||||
|
if var == ""
|
||||||
|
then []
|
||||||
|
else [(base, token, var, DataString $ SourceFrom [stripEqualsFrom token])]
|
||||||
|
where var = takeWhile isVariableChar $ dropWhile (`elem` "+-") $ concat $ oversimplify token
|
||||||
|
|
||||||
|
getSetParams (t:_:rest) | getLiteralString t == Just "-o" = getSetParams rest
|
||||||
|
getSetParams (t:rest) =
|
||||||
|
let s = getLiteralString t in
|
||||||
|
case s of
|
||||||
|
Just "--" -> return rest
|
||||||
|
Just ('-':_) -> getSetParams rest
|
||||||
|
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||||
|
getSetParams [] = Nothing
|
||||||
|
|
||||||
|
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
|
||||||
|
where
|
||||||
|
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
|
||||||
|
f (_:rest) = f rest
|
||||||
|
f [] = fail "not found"
|
||||||
|
|
||||||
|
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
||||||
|
-- where only the first non-option one is used if any. Here we cheat and
|
||||||
|
-- just get the last one, if it's a variable name.
|
||||||
|
getMapfileArray base arguments = do
|
||||||
|
lastArg <- listToMaybe (reverse arguments)
|
||||||
|
name <- getLiteralString lastArg
|
||||||
|
guard $ isVariableName name
|
||||||
|
return (base, lastArg, name, DataArray SourceExternal)
|
||||||
|
|
||||||
|
getModifiedVariableCommand _ = []
|
||||||
|
|
||||||
|
getIndexReferences s = fromMaybe [] $ do
|
||||||
|
match <- matchRegex re s
|
||||||
|
index <- match !!! 0
|
||||||
|
return $ matchAllStrings variableNameRegex index
|
||||||
|
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
|
||||||
|
++ getOffsetReferences (getBracedModifier str))
|
||||||
|
TA_Expansion id _ ->
|
||||||
|
if isArithmeticAssignment t
|
||||||
|
then []
|
||||||
|
else getIfReference t t
|
||||||
|
T_Assignment id mode str _ word ->
|
||||||
|
[(t, t, str) | mode == Append] ++ specialReferences str t word
|
||||||
|
|
||||||
|
TC_Unary id _ "-v" token -> getIfReference t token
|
||||||
|
TC_Unary id _ "-R" token -> getIfReference t token
|
||||||
|
TC_Binary id DoubleBracket op lhs rhs ->
|
||||||
|
if isDereferencing op
|
||||||
|
then concatMap (getIfReference t) [lhs, rhs]
|
||||||
|
else []
|
||||||
|
|
||||||
|
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
|
||||||
|
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
|
||||||
|
x -> getReferencedVariableCommand x
|
||||||
|
where
|
||||||
|
-- Try to reduce false positives for unused vars only referenced from evaluated vars
|
||||||
|
specialReferences name base word =
|
||||||
|
if name `elem` [
|
||||||
|
"PS1", "PS2", "PS3", "PS4",
|
||||||
|
"PROMPT_COMMAND"
|
||||||
|
]
|
||||||
|
then
|
||||||
|
map (\x -> (base, base, x)) $
|
||||||
|
getVariablesFromLiteralToken word
|
||||||
|
else []
|
||||||
|
|
||||||
|
literalizer (TA_Index {}) = return "" -- x[0] becomes a reference of x
|
||||||
|
literalizer _ = Nothing
|
||||||
|
|
||||||
|
getIfReference context token = maybeToList $ do
|
||||||
|
str <- getLiteralStringExt literalizer token
|
||||||
|
guard . not $ null str
|
||||||
|
when (isDigit $ head str) $ fail "is a number"
|
||||||
|
return (context, token, getBracedReference str)
|
||||||
|
|
||||||
|
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||||
|
|
||||||
|
isArithmeticAssignment t = case getPath parents t of
|
||||||
|
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||||
|
|
||||||
|
|
||||||
|
--- Command specific checks
|
||||||
|
|
||||||
|
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
|
||||||
|
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||||
|
|
||||||
|
isCommandMatch token matcher = fromMaybe False $ do
|
||||||
|
cmd <- getCommandName token
|
||||||
|
return $ matcher cmd
|
||||||
|
|
||||||
|
isConfusedGlobRegex ('*':_) = True
|
||||||
|
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
||||||
|
isConfusedGlobRegex _ = False
|
||||||
|
|
||||||
|
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||||
|
isVariableChar x = isVariableStartChar x || isDigit x
|
||||||
|
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||||
|
|
||||||
|
prop_isVariableName1 = isVariableName "_fo123"
|
||||||
|
prop_isVariableName2 = not $ isVariableName "4"
|
||||||
|
prop_isVariableName3 = not $ isVariableName "test: "
|
||||||
|
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||||
|
isVariableName _ = False
|
||||||
|
|
||||||
|
getVariablesFromLiteralToken token =
|
||||||
|
getVariablesFromLiteral (fromJust $ getLiteralStringExt (const $ return " ") token)
|
||||||
|
|
||||||
|
-- Try to get referenced variables from a literal string like "$foo"
|
||||||
|
-- Ignores tons of cases like arithmetic evaluation and array indices.
|
||||||
|
prop_getVariablesFromLiteral1 =
|
||||||
|
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
|
||||||
|
getVariablesFromLiteral string =
|
||||||
|
map (!! 0) $ matchAllSubgroups variableRegex string
|
||||||
|
where
|
||||||
|
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||||
|
|
||||||
|
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
||||||
|
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
||||||
|
prop_getBracedReference3 = getBracedReference "#" == "#"
|
||||||
|
prop_getBracedReference4 = getBracedReference "##" == "#"
|
||||||
|
prop_getBracedReference5 = getBracedReference "#!" == "!"
|
||||||
|
prop_getBracedReference6 = getBracedReference "!#" == "#"
|
||||||
|
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
|
||||||
|
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
||||||
|
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
|
||||||
|
noPrefix = dropPrefix s
|
||||||
|
dropPrefix (c:rest) = if c `elem` "!#" then rest else c:rest
|
||||||
|
dropPrefix "" = ""
|
||||||
|
takeName s = do
|
||||||
|
let name = takeWhile isVariableChar s
|
||||||
|
guard . not $ null name
|
||||||
|
return name
|
||||||
|
getSpecial (c:_) =
|
||||||
|
if c `elem` "*@#?-$!" then return [c] else fail "not special"
|
||||||
|
getSpecial _ = fail "empty"
|
||||||
|
|
||||||
|
nameExpansion ('!':rest) = do -- e.g. ${!foo*bar*}
|
||||||
|
let suffix = dropWhile isVariableChar rest
|
||||||
|
guard $ suffix /= rest -- e.g. ${!@}
|
||||||
|
first <- suffix !!! 0
|
||||||
|
guard $ first `elem` "*?"
|
||||||
|
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 ()
|
||||||
|
potentially = fromMaybe (return ())
|
||||||
|
|
||||||
|
headOrDefault _ (a:_) = a
|
||||||
|
headOrDefault def _ = def
|
||||||
|
|
||||||
|
(!!!) list i =
|
||||||
|
case drop i list of
|
||||||
|
[] -> Nothing
|
||||||
|
(r:_) -> Just r
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
filterByAnnotation token =
|
||||||
|
filter (not . shouldIgnore)
|
||||||
|
where
|
||||||
|
idFor (TokenComment id _) = id
|
||||||
|
shouldIgnore note =
|
||||||
|
any (shouldIgnoreFor (getCode note)) $
|
||||||
|
getPath parents (T_Bang $ idFor note)
|
||||||
|
shouldIgnoreFor num (T_Annotation _ anns _) =
|
||||||
|
any hasNum anns
|
||||||
|
where
|
||||||
|
hasNum (DisableComment ts) = num == ts
|
||||||
|
hasNum _ = False
|
||||||
|
shouldIgnoreFor _ (T_Include {}) = True -- Ignore included files
|
||||||
|
shouldIgnoreFor _ _ = False
|
||||||
|
parents = getParentTree token
|
||||||
|
getCode (TokenComment _ (Comment _ c _)) = c
|
||||||
|
|
||||||
|
|
||||||
|
return []
|
||||||
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
@@ -39,7 +39,7 @@ import Test.QuickCheck.All
|
|||||||
|
|
||||||
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
||||||
position <- Map.lookup id map
|
position <- Map.lookup id map
|
||||||
return $ PositionedComment position c
|
return $ PositionedComment position position c
|
||||||
where
|
where
|
||||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||||
|
|
||||||
@@ -65,13 +65,13 @@ checkScript sys spec = do
|
|||||||
return . nub . sortMessages . filter shouldInclude $
|
return . nub . sortMessages . filter shouldInclude $
|
||||||
(parseMessages ++ map translator analysisMessages)
|
(parseMessages ++ map translator analysisMessages)
|
||||||
|
|
||||||
shouldInclude (PositionedComment _ (Comment _ code _)) =
|
shouldInclude (PositionedComment _ _ (Comment _ code _)) =
|
||||||
code `notElem` csExcludedWarnings spec
|
code `notElem` csExcludedWarnings spec
|
||||||
|
|
||||||
sortMessages = sortBy (comparing order)
|
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)
|
(posFile pos, posLine pos, posColumn pos, severity, code, message)
|
||||||
getPosition (PositionedComment pos _) = pos
|
getPosition (PositionedComment pos _ _) = pos
|
||||||
|
|
||||||
analysisSpec root =
|
analysisSpec root =
|
||||||
AnalysisSpec {
|
AnalysisSpec {
|
||||||
@@ -84,7 +84,7 @@ getErrors sys spec =
|
|||||||
sort . map getCode . crComments $
|
sort . map getCode . crComments $
|
||||||
runIdentity (checkScript sys spec)
|
runIdentity (checkScript sys spec)
|
||||||
where
|
where
|
||||||
getCode (PositionedComment _ (Comment _ code _)) = code
|
getCode (PositionedComment _ _ (Comment _ code _)) = code
|
||||||
|
|
||||||
check = checkWithIncludes []
|
check = checkWithIncludes []
|
||||||
|
|
||||||
|
623
ShellCheck/Checks/Commands.hs
Normal file
623
ShellCheck/Checks/Commands.hs
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
{-
|
||||||
|
Copyright 2012-2015 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 #-}
|
||||||
|
|
||||||
|
-- This module contains checks that examine specific commands by name.
|
||||||
|
module ShellCheck.Checks.Commands (runChecks
|
||||||
|
, ShellCheck.Checks.Commands.runTests
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ShellCheck.AST
|
||||||
|
import ShellCheck.ASTLib
|
||||||
|
import ShellCheck.AnalyzerLib
|
||||||
|
import ShellCheck.Data
|
||||||
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Regex
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Reader
|
||||||
|
import Control.Monad.Writer
|
||||||
|
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 CommandName = Exactly String | Basename String
|
||||||
|
deriving (Eq, Ord)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||||
|
|
||||||
|
commandChecks :: [CommandCheck]
|
||||||
|
commandChecks = [
|
||||||
|
checkTr
|
||||||
|
,checkFindNameGlob
|
||||||
|
,checkNeedlessExpr
|
||||||
|
,checkGrepRe
|
||||||
|
,checkTrapQuotes
|
||||||
|
,checkReturn
|
||||||
|
,checkFindExecWithSingleArgument
|
||||||
|
,checkUnusedEchoEscapes
|
||||||
|
,checkInjectableFindSh
|
||||||
|
,checkFindActionPrecedence
|
||||||
|
,checkMkdirDashPM
|
||||||
|
,checkNonportableSignals
|
||||||
|
,checkInteractiveSu
|
||||||
|
,checkSshCommandString
|
||||||
|
,checkPrintfVar
|
||||||
|
,checkUuoeCmd
|
||||||
|
,checkSetAssignment
|
||||||
|
,checkExportedExpansions
|
||||||
|
,checkAliasesUsesArgs
|
||||||
|
,checkAliasesExpandEarly
|
||||||
|
,checkUnsetGlobs
|
||||||
|
,checkFindWithoutPath
|
||||||
|
]
|
||||||
|
|
||||||
|
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||||
|
buildCommandMap = foldl' addCheck Map.empty
|
||||||
|
where
|
||||||
|
addCheck map (CommandCheck name function) =
|
||||||
|
Map.insertWith' composeChecks name function map
|
||||||
|
|
||||||
|
|
||||||
|
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||||
|
checkCommand map t@(T_SimpleCommand id _ (cmd:rest)) = fromMaybe (return ()) $ do
|
||||||
|
name <- getLiteralString cmd
|
||||||
|
return $
|
||||||
|
if '/' `elem` name
|
||||||
|
then
|
||||||
|
Map.findWithDefault nullCheck (Basename $ basename name) map t
|
||||||
|
else do
|
||||||
|
Map.findWithDefault nullCheck (Exactly name) map t
|
||||||
|
Map.findWithDefault nullCheck (Basename name) map t
|
||||||
|
|
||||||
|
where
|
||||||
|
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
|
||||||
|
|
||||||
|
runChecks spec = runList spec commandChecks
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||||
|
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||||
|
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
|
||||||
|
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
|
||||||
|
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
|
||||||
|
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
|
||||||
|
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
|
||||||
|
prop_checkTr5 = verify checkTr "tr foo bar"
|
||||||
|
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
|
||||||
|
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
||||||
|
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||||
|
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||||
|
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
||||||
|
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||||
|
where
|
||||||
|
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
||||||
|
warn (getId w) 2060 "Quote parameters to tr to prevent glob expansion."
|
||||||
|
f word =
|
||||||
|
case getLiteralString word of
|
||||||
|
Just "a-z" -> info (getId word) 2018 "Use '[:lower:]' to support accents and foreign alphabets."
|
||||||
|
Just "A-Z" -> info (getId word) 2019 "Use '[:upper:]' to support accents and foreign alphabets."
|
||||||
|
Just s -> do -- Eliminate false positives by only looking for dupes in SET2?
|
||||||
|
when (not ("-" `isPrefixOf` s || "[:" `isInfixOf` s) && duplicated s) $
|
||||||
|
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 classes in tr, it replaces literal square brackets."
|
||||||
|
Nothing -> return ()
|
||||||
|
|
||||||
|
duplicated s =
|
||||||
|
let relevant = filter isAlpha s
|
||||||
|
in relevant /= nub relevant
|
||||||
|
|
||||||
|
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php"
|
||||||
|
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)"
|
||||||
|
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'"
|
||||||
|
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
||||||
|
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
|
||||||
|
acceptsGlob _ = False
|
||||||
|
f [] = return ()
|
||||||
|
f [x] = return ()
|
||||||
|
f (a:b:r) = do
|
||||||
|
when (acceptsGlob (getLiteralString a) && isGlob b) $ do
|
||||||
|
let (Just s) = getLiteralString a
|
||||||
|
warn (getId b) 2061 $ "Quote the parameter to " ++ s ++ " so the shell won't interpret it."
|
||||||
|
f (b:r)
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
||||||
|
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
|
||||||
|
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
|
||||||
|
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
|
||||||
|
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
||||||
|
f t =
|
||||||
|
when (all (`notElem` exceptions) (words $ arguments t)) $
|
||||||
|
style (getId t) 2003
|
||||||
|
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
||||||
|
-- These operators are hard to replicate in POSIX
|
||||||
|
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
||||||
|
words = mapMaybe getLiteralString
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
|
||||||
|
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
|
||||||
|
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file"
|
||||||
|
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3"
|
||||||
|
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *"
|
||||||
|
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
|
||||||
|
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
|
||||||
|
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"
|
||||||
|
|
||||||
|
checkGrepRe = CommandCheck (Basename "grep") (f . arguments) where
|
||||||
|
-- --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
|
||||||
|
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 ++ "'."
|
||||||
|
|
||||||
|
wordStartingWith c =
|
||||||
|
head . filter ([c] `isPrefixOf`) $ candidates
|
||||||
|
where
|
||||||
|
candidates =
|
||||||
|
sampleWords ++ map (\(x:r) -> toUpper x : r) sampleWords ++ [c:"test"]
|
||||||
|
|
||||||
|
getSuspiciousRegexWildcard str =
|
||||||
|
if not $ str `matches` contra
|
||||||
|
then do
|
||||||
|
match <- matchRegex suspicious str
|
||||||
|
str <- match !!! 0
|
||||||
|
str !!! 0
|
||||||
|
else
|
||||||
|
fail "looks good"
|
||||||
|
where
|
||||||
|
suspicious = mkRegex "([A-Za-z1-9])\\*"
|
||||||
|
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
|
||||||
|
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
|
||||||
|
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
|
||||||
|
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
|
||||||
|
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
||||||
|
f (x:_) = checkTrap x
|
||||||
|
f _ = return ()
|
||||||
|
checkTrap (T_NormalWord _ [T_DoubleQuoted _ rs]) = mapM_ checkExpansions rs
|
||||||
|
checkTrap _ = return ()
|
||||||
|
warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
|
||||||
|
checkExpansions (T_DollarExpansion id _) = warning id
|
||||||
|
checkExpansions (T_Backticked id _) = warning id
|
||||||
|
checkExpansions (T_DollarBraced id _) = warning id
|
||||||
|
checkExpansions (T_DollarArithmetic id _) = warning id
|
||||||
|
checkExpansions _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkReturn1 = verifyNot checkReturn "return"
|
||||||
|
prop_checkReturn2 = verifyNot checkReturn "return 1"
|
||||||
|
prop_checkReturn3 = verifyNot checkReturn "return $var"
|
||||||
|
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
|
||||||
|
prop_checkReturn5 = verify checkReturn "return -1"
|
||||||
|
prop_checkReturn6 = verify checkReturn "return 1000"
|
||||||
|
prop_checkReturn7 = verify checkReturn "return 'hello world'"
|
||||||
|
checkReturn = CommandCheck (Exactly "return") (f . arguments)
|
||||||
|
where
|
||||||
|
f (first:second:_) =
|
||||||
|
err (getId second) 2151
|
||||||
|
"Only one integer 0-255 can be returned. Use stdout for other data."
|
||||||
|
f [value] =
|
||||||
|
when (isInvalid $ literal value) $
|
||||||
|
err (getId value) 2152
|
||||||
|
"Can only return 0-255. Other data should be written to stdout."
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
isInvalid s = s == "" || any (not . isDigit) s || length s > 5
|
||||||
|
|| let value = (read s :: Integer) in value > 255
|
||||||
|
|
||||||
|
literal token = fromJust $ getLiteralStringExt lit token
|
||||||
|
lit (T_DollarBraced {}) = return "0"
|
||||||
|
lit (T_DollarArithmetic {}) = return "0"
|
||||||
|
lit (T_DollarExpansion {}) = return "0"
|
||||||
|
lit (T_Backticked {}) = return "0"
|
||||||
|
lit _ = return "WTF"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
|
||||||
|
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
|
||||||
|
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
|
||||||
|
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
|
||||||
|
where
|
||||||
|
f = void . sequence . mapMaybe check . tails
|
||||||
|
check (exec:arg:term:_) = do
|
||||||
|
execS <- getLiteralString exec
|
||||||
|
termS <- getLiteralString term
|
||||||
|
cmdS <- getLiteralStringExt (const $ return " ") arg
|
||||||
|
|
||||||
|
guard $ execS `elem` ["-exec", "-execdir"] && termS `elem` [";", "+"]
|
||||||
|
guard $ cmdS `matches` commandRegex
|
||||||
|
return $ warn (getId exec) 2150 "-exec does not invoke a shell. Rewrite or use -exec sh -c .. ."
|
||||||
|
check _ = Nothing
|
||||||
|
commandRegex = mkRegex "[ |;]"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
|
||||||
|
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
|
||||||
|
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||||
|
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||||
|
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||||
|
checkUnusedEchoEscapes = CommandCheck (Basename "echo") (f . arguments)
|
||||||
|
where
|
||||||
|
isDashE = mkRegex "^-.*e"
|
||||||
|
hasEscapes = mkRegex "\\\\[rnt]"
|
||||||
|
f args | concat (concatMap oversimplify allButLast) `matches` isDashE =
|
||||||
|
return ()
|
||||||
|
where allButLast = reverse . drop 1 . reverse $ args
|
||||||
|
f args = mapM_ checkEscapes args
|
||||||
|
|
||||||
|
checkEscapes (T_NormalWord _ args) =
|
||||||
|
mapM_ checkEscapes args
|
||||||
|
checkEscapes (T_DoubleQuoted id args) =
|
||||||
|
mapM_ checkEscapes args
|
||||||
|
checkEscapes (T_Literal id str) = examine id str
|
||||||
|
checkEscapes (T_SingleQuoted id str) = examine id str
|
||||||
|
checkEscapes _ = return ()
|
||||||
|
|
||||||
|
examine id str =
|
||||||
|
when (str `matches` hasEscapes) $
|
||||||
|
info id 2028 "echo won't expand escape sequences. Consider printf."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
|
||||||
|
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
|
||||||
|
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
|
||||||
|
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
|
||||||
|
where
|
||||||
|
check args = do
|
||||||
|
let idStrings = map (\x -> (getId x, onlyLiteralString x)) args
|
||||||
|
match pattern idStrings
|
||||||
|
|
||||||
|
match _ [] = return ()
|
||||||
|
match [] (next:_) = action next
|
||||||
|
match (p:tests) ((id, arg):args) = do
|
||||||
|
when (p arg) $ match tests args
|
||||||
|
match (p:tests) args
|
||||||
|
|
||||||
|
pattern = [
|
||||||
|
(`elem` ["-exec", "-execdir"]),
|
||||||
|
(`elem` ["sh", "bash", "ksh"]),
|
||||||
|
(== "-c")
|
||||||
|
]
|
||||||
|
action (id, arg) =
|
||||||
|
when ("{}" `isInfixOf` arg) $
|
||||||
|
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
|
||||||
|
prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
|
||||||
|
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
|
||||||
|
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
|
||||||
|
where
|
||||||
|
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
|
||||||
|
f list | length list < length pattern = return ()
|
||||||
|
f list@(_:rest) =
|
||||||
|
if and (zipWith ($) pattern list)
|
||||||
|
then warnFor (list !! (length pattern - 1))
|
||||||
|
else f rest
|
||||||
|
isMatch = isParam [ "-name", "-regex", "-iname", "-iregex", "-wholename", "-iwholename" ]
|
||||||
|
isAction = isParam [ "-exec", "-execdir", "-delete", "-print", "-print0" ]
|
||||||
|
isParam strs t = fromMaybe False $ do
|
||||||
|
param <- getLiteralString t
|
||||||
|
return $ param `elem` strs
|
||||||
|
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir"
|
||||||
|
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
|
||||||
|
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
|
||||||
|
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
|
||||||
|
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b"
|
||||||
|
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b"
|
||||||
|
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
|
||||||
|
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
|
||||||
|
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
|
||||||
|
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
|
||||||
|
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
||||||
|
where
|
||||||
|
check t = potentially $ do
|
||||||
|
let flags = getAllFlags t
|
||||||
|
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
|
||||||
|
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
|
||||||
|
guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
|
||||||
|
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
||||||
|
couldHaveSubdirs t = fromMaybe True $ do
|
||||||
|
name <- getLiteralString t
|
||||||
|
return $ '/' `elem` name
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
|
||||||
|
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0"
|
||||||
|
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14"
|
||||||
|
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL"
|
||||||
|
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9"
|
||||||
|
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop"
|
||||||
|
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
|
||||||
|
where
|
||||||
|
f = mapM_ check
|
||||||
|
check param = potentially $ do
|
||||||
|
str <- getLiteralString param
|
||||||
|
let id = getId param
|
||||||
|
return $ sequence_ $ mapMaybe (\f -> f id str) [
|
||||||
|
checkNumeric,
|
||||||
|
checkUntrappable
|
||||||
|
]
|
||||||
|
|
||||||
|
checkNumeric id str = do
|
||||||
|
guard $ not (null str)
|
||||||
|
guard $ all isDigit str
|
||||||
|
guard $ str /= "0" -- POSIX exit trap
|
||||||
|
guard $ str `notElem` ["1", "2", "3", "6", "9", "14", "15" ] -- XSI
|
||||||
|
return $ warn id 2172
|
||||||
|
"Trapping signals by number is not well defined. Prefer signal names."
|
||||||
|
|
||||||
|
checkUntrappable id str = do
|
||||||
|
guard $ map toLower str `elem` ["kill", "9", "sigkill", "stop", "sigstop"]
|
||||||
|
return $ err id 2173
|
||||||
|
"SIGKILL/SIGSTOP can not be trapped."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER"
|
||||||
|
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit"
|
||||||
|
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo"
|
||||||
|
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script"
|
||||||
|
checkInteractiveSu = CommandCheck (Basename "su") f
|
||||||
|
where
|
||||||
|
f cmd = when (length (arguments cmd) <= 1) $ do
|
||||||
|
path <- pathTo cmd
|
||||||
|
when (all undirected path) $
|
||||||
|
info (getId cmd) 2117
|
||||||
|
"To run commands as another user, use su -c or sudo."
|
||||||
|
|
||||||
|
undirected (T_Pipeline _ _ l) = length l <= 1
|
||||||
|
-- This should really just be modifications to stdin, but meh
|
||||||
|
undirected (T_Redirecting _ list _) = null list
|
||||||
|
undirected _ = True
|
||||||
|
|
||||||
|
|
||||||
|
-- This is hard to get right without properly parsing ssh args
|
||||||
|
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\""
|
||||||
|
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\""
|
||||||
|
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\""
|
||||||
|
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
|
||||||
|
where
|
||||||
|
nonOptions =
|
||||||
|
filter (\x -> not $ "-" `isPrefixOf` concat (oversimplify x))
|
||||||
|
f args =
|
||||||
|
case nonOptions args of
|
||||||
|
(hostport:r@(_:_)) -> checkArg $ last r
|
||||||
|
_ -> return ()
|
||||||
|
checkArg (T_NormalWord _ [T_DoubleQuoted id parts]) =
|
||||||
|
case filter (not . isConstant) parts of
|
||||||
|
[] -> return ()
|
||||||
|
(x:_) -> info (getId x) 2029
|
||||||
|
"Note that, unescaped, this expands on the client side."
|
||||||
|
checkArg _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
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 params
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
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) $
|
||||||
|
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)\""
|
||||||
|
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\""
|
||||||
|
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\""
|
||||||
|
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\""
|
||||||
|
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
|
||||||
|
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
|
||||||
|
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42"
|
||||||
|
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42"
|
||||||
|
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42"
|
||||||
|
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null"
|
||||||
|
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'"
|
||||||
|
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set"
|
||||||
|
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
|
||||||
|
where
|
||||||
|
f (var:value:rest) =
|
||||||
|
let str = literal var in
|
||||||
|
when (isVariableName str || isAssignment str) $
|
||||||
|
msg (getId var)
|
||||||
|
f (var:_) =
|
||||||
|
when (isAssignment $ literal var) $
|
||||||
|
msg (getId var)
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
msg id = warn id 2121 "To assign a variable, use just 'var=value', no 'set ..'."
|
||||||
|
|
||||||
|
isAssignment str = '=' `elem` str
|
||||||
|
literal (T_NormalWord _ l) = concatMap literal l
|
||||||
|
literal (T_Literal _ str) = str
|
||||||
|
literal _ = "*"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo"
|
||||||
|
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\""
|
||||||
|
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo"
|
||||||
|
checkExportedExpansions = CommandCheck (Exactly "export") (check . arguments)
|
||||||
|
where
|
||||||
|
check = mapM_ checkForVariables
|
||||||
|
checkForVariables f =
|
||||||
|
case getWordParts f of
|
||||||
|
[t@(T_DollarBraced {})] ->
|
||||||
|
warn (getId t) 2163 "Exporting an expansion rather than a variable."
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'"
|
||||||
|
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'"
|
||||||
|
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
|
||||||
|
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
|
||||||
|
where
|
||||||
|
re = mkRegex "\\$\\{?[0-9*@]"
|
||||||
|
f = mapM_ checkArg
|
||||||
|
checkArg arg =
|
||||||
|
let string = fromJust $ getLiteralStringExt (const $ return "_") arg in
|
||||||
|
when ('=' `elem` string && string `matches` re) $
|
||||||
|
err (getId arg) 2142
|
||||||
|
"Aliases can't use positional parameters. Use a function."
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
|
||||||
|
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p"
|
||||||
|
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
|
||||||
|
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
||||||
|
where
|
||||||
|
f = mapM_ checkArg
|
||||||
|
checkArg arg | '=' `elem` concat (oversimplify arg) =
|
||||||
|
forM_ (take 1 $ filter (not . isLiteral) $ getWordParts arg) $
|
||||||
|
\x -> warn (getId x) 2139 "This expands when defined, not when used. Consider escaping."
|
||||||
|
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 ()
|
footer :: IO ()
|
||||||
}
|
}
|
||||||
|
|
||||||
lineNo (PositionedComment pos _) = posLine pos
|
lineNo (PositionedComment pos _ _) = posLine pos
|
||||||
colNo (PositionedComment pos _) = posColumn pos
|
endLineNo (PositionedComment _ end _) = posLine end
|
||||||
codeNo (PositionedComment _ (Comment _ code _)) = code
|
colNo (PositionedComment pos _ _) = posColumn pos
|
||||||
messageText (PositionedComment _ (Comment _ _ t)) = t
|
endColNo (PositionedComment _ end _) = posColumn end
|
||||||
|
codeNo (PositionedComment _ _ (Comment _ code _)) = code
|
||||||
|
messageText (PositionedComment _ _ (Comment _ _ t)) = t
|
||||||
|
|
||||||
severityText :: PositionedComment -> String
|
severityText :: PositionedComment -> String
|
||||||
severityText (PositionedComment _ (Comment c _ _)) =
|
severityText (PositionedComment _ _ (Comment c _ _)) =
|
||||||
case c of
|
case c of
|
||||||
ErrorC -> "error"
|
ErrorC -> "error"
|
||||||
WarningC -> "warning"
|
WarningC -> "warning"
|
||||||
@@ -48,12 +50,15 @@ makeNonVirtual comments contents =
|
|||||||
map fix comments
|
map fix comments
|
||||||
where
|
where
|
||||||
ls = lines contents
|
ls = lines contents
|
||||||
fix c@(PositionedComment pos comment) = PositionedComment pos {
|
fix c@(PositionedComment start end comment) = PositionedComment start {
|
||||||
posColumn =
|
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)
|
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
|
||||||
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
|
||||||
else colNo c
|
else colNo c
|
||||||
} comment
|
|
||||||
real _ r v target | target <= v = r
|
real _ r v target | target <= v = r
|
||||||
real [] r v _ = r -- should never happen
|
real [] r v _ = r -- should never happen
|
||||||
real ('\t':rest) r v target =
|
real ('\t':rest) r v target =
|
||||||
|
@@ -37,10 +37,12 @@ format = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
instance JSON (PositionedComment) where
|
instance JSON (PositionedComment) where
|
||||||
showJSON comment@(PositionedComment pos (Comment level code string)) = makeObj [
|
showJSON comment@(PositionedComment start end (Comment level code string)) = makeObj [
|
||||||
("file", showJSON $ posFile pos),
|
("file", showJSON $ posFile start),
|
||||||
("line", showJSON $ posLine pos),
|
("line", showJSON $ posLine start),
|
||||||
("column", showJSON $ posColumn pos),
|
("endLine", showJSON $ posLine end),
|
||||||
|
("column", showJSON $ posColumn start),
|
||||||
|
("endColumn", showJSON $ posColumn end),
|
||||||
("level", showJSON $ severityText comment),
|
("level", showJSON $ severityText comment),
|
||||||
("code", showJSON code),
|
("code", showJSON code),
|
||||||
("message", showJSON string)
|
("message", showJSON string)
|
||||||
|
@@ -94,7 +94,7 @@ data Position = Position {
|
|||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
data Comment = Comment Severity Code String 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 TokenComment = TokenComment Id Comment deriving (Show, Eq)
|
||||||
|
|
||||||
data ColorOption =
|
data ColorOption =
|
||||||
|
@@ -52,20 +52,24 @@ type SCParser m v = ParsecT String UserState (SCBase m) v
|
|||||||
|
|
||||||
backslash :: Monad m => SCParser m Char
|
backslash :: Monad m => SCParser m Char
|
||||||
backslash = char '\\'
|
backslash = char '\\'
|
||||||
linefeed = optional carriageReturn >> char '\n'
|
linefeed :: Monad m => SCParser m Char
|
||||||
|
linefeed = do
|
||||||
|
optional carriageReturn
|
||||||
|
c <- char '\n'
|
||||||
|
readPendingHereDocs
|
||||||
|
return c
|
||||||
singleQuote = char '\'' <|> unicodeSingleQuote
|
singleQuote = char '\'' <|> unicodeSingleQuote
|
||||||
doubleQuote = char '"' <|> unicodeDoubleQuote
|
doubleQuote = char '"' <|> unicodeDoubleQuote
|
||||||
variableStart = upper <|> lower <|> oneOf "_"
|
variableStart = upper <|> lower <|> oneOf "_"
|
||||||
variableChars = upper <|> lower <|> digit <|> oneOf "_"
|
variableChars = upper <|> lower <|> digit <|> oneOf "_"
|
||||||
functionChars = variableChars <|> oneOf ":+-.?"
|
functionChars = variableChars <|> oneOf ":+-.?"
|
||||||
specialVariable = oneOf "@*#?-$!"
|
specialVariable = oneOf "@*#?-$!"
|
||||||
tokenDelimiter = oneOf "&|;<> \t\n\r" <|> almostSpace
|
|
||||||
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
|
quotableChars = "|&;<>()\\ '\t\n\r\xA0" ++ doubleQuotableChars
|
||||||
quotable = almostSpace <|> unicodeDoubleQuote <|> oneOf quotableChars
|
quotable = almostSpace <|> unicodeDoubleQuote <|> oneOf quotableChars
|
||||||
bracedQuotable = oneOf "}\"$`'"
|
bracedQuotable = oneOf "}\"$`'"
|
||||||
doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars
|
doubleQuotableChars = "\"$`" ++ unicodeDoubleQuoteChars
|
||||||
doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars
|
doubleQuotable = unicodeDoubleQuote <|> oneOf doubleQuotableChars
|
||||||
whitespace = oneOf " \t\n" <|> carriageReturn <|> almostSpace
|
whitespace = oneOf " \t" <|> carriageReturn <|> almostSpace <|> linefeed
|
||||||
linewhitespace = oneOf " \t" <|> almostSpace
|
linewhitespace = oneOf " \t" <|> almostSpace
|
||||||
|
|
||||||
suspectCharAfterQuotes = variableChars <|> char '%'
|
suspectCharAfterQuotes = variableChars <|> char '%'
|
||||||
@@ -131,31 +135,39 @@ almostSpace =
|
|||||||
|
|
||||||
--------- Message/position annotation on top of user state
|
--------- Message/position annotation on top of user state
|
||||||
data Note = Note Id Severity Code String deriving (Show, Eq)
|
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 =
|
data Context =
|
||||||
ContextName SourcePos String
|
ContextName SourcePos String
|
||||||
| ContextAnnotation [Annotation]
|
| ContextAnnotation [Annotation]
|
||||||
| ContextSource String
|
| ContextSource String
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
data HereDocContext =
|
||||||
|
HereDocPending Token -- on linefeed, read this T_HereDoc
|
||||||
|
| HereDocBoundary -- but don't consider heredocs before this
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
data UserState = UserState {
|
data UserState = UserState {
|
||||||
lastId :: Id,
|
lastId :: Id,
|
||||||
positionMap :: Map.Map Id SourcePos,
|
positionMap :: Map.Map Id SourcePos,
|
||||||
parseNotes :: [ParseNote]
|
parseNotes :: [ParseNote],
|
||||||
|
hereDocMap :: Map.Map Id [Token],
|
||||||
|
pendingHereDocs :: [HereDocContext]
|
||||||
}
|
}
|
||||||
initialUserState = UserState {
|
initialUserState = UserState {
|
||||||
lastId = Id $ -1,
|
lastId = Id $ -1,
|
||||||
positionMap = Map.empty,
|
positionMap = Map.empty,
|
||||||
parseNotes = []
|
parseNotes = [],
|
||||||
|
hereDocMap = Map.empty,
|
||||||
|
pendingHereDocs = []
|
||||||
}
|
}
|
||||||
|
|
||||||
codeForParseNote (ParseNote _ _ code _) = code
|
codeForParseNote (ParseNote _ _ _ code _) = code
|
||||||
noteToParseNote map (Note id severity code message) =
|
noteToParseNote map (Note id severity code message) =
|
||||||
ParseNote pos severity code message
|
ParseNote pos pos severity code message
|
||||||
where
|
where
|
||||||
pos = fromJust $ Map.lookup id map
|
pos = fromJust $ Map.lookup id map
|
||||||
|
|
||||||
|
|
||||||
getLastId = lastId <$> getState
|
getLastId = lastId <$> getState
|
||||||
|
|
||||||
getNextIdAt sourcepos = do
|
getNextIdAt sourcepos = do
|
||||||
@@ -169,10 +181,63 @@ getNextIdAt sourcepos = do
|
|||||||
return newId
|
return newId
|
||||||
where incId (Id n) = Id $ n+1
|
where incId (Id n) = Id $ n+1
|
||||||
|
|
||||||
|
getNextId :: Monad m => SCParser m Id
|
||||||
getNextId = do
|
getNextId = do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
getNextIdAt pos
|
getNextIdAt pos
|
||||||
|
|
||||||
|
addToHereDocMap id list = do
|
||||||
|
state <- getState
|
||||||
|
let map = hereDocMap state
|
||||||
|
putState $ state {
|
||||||
|
hereDocMap = Map.insert id list map
|
||||||
|
}
|
||||||
|
|
||||||
|
withHereDocBoundary p = do
|
||||||
|
pushBoundary
|
||||||
|
do
|
||||||
|
v <- p
|
||||||
|
popBoundary
|
||||||
|
return v
|
||||||
|
<|> do
|
||||||
|
popBoundary
|
||||||
|
fail ""
|
||||||
|
where
|
||||||
|
pushBoundary = do
|
||||||
|
state <- getState
|
||||||
|
let docs = pendingHereDocs state
|
||||||
|
putState $ state {
|
||||||
|
pendingHereDocs = HereDocBoundary : docs
|
||||||
|
}
|
||||||
|
popBoundary = do
|
||||||
|
state <- getState
|
||||||
|
let docs = tail $ dropWhile (not . isHereDocBoundary) $
|
||||||
|
pendingHereDocs state
|
||||||
|
putState $ state {
|
||||||
|
pendingHereDocs = docs
|
||||||
|
}
|
||||||
|
|
||||||
|
addPendingHereDoc t = do
|
||||||
|
state <- getState
|
||||||
|
let docs = pendingHereDocs state
|
||||||
|
putState $ state {
|
||||||
|
pendingHereDocs = HereDocPending t : docs
|
||||||
|
}
|
||||||
|
|
||||||
|
popPendingHereDocs = do
|
||||||
|
state <- getState
|
||||||
|
let (pending, boundary) = break isHereDocBoundary $ pendingHereDocs state
|
||||||
|
putState $ state {
|
||||||
|
pendingHereDocs = boundary
|
||||||
|
}
|
||||||
|
return . map extract . reverse $ pendingHereDocs state
|
||||||
|
where
|
||||||
|
extract (HereDocPending t) = t
|
||||||
|
|
||||||
|
isHereDocBoundary x = case x of
|
||||||
|
HereDocBoundary -> True
|
||||||
|
otherwise -> False
|
||||||
|
|
||||||
getMap = positionMap <$> getState
|
getMap = positionMap <$> getState
|
||||||
getParseNotes = parseNotes <$> getState
|
getParseNotes = parseNotes <$> getState
|
||||||
|
|
||||||
@@ -256,14 +321,16 @@ pushContext c = do
|
|||||||
v <- getCurrentContexts
|
v <- getCurrentContexts
|
||||||
setCurrentContexts (c:v)
|
setCurrentContexts (c:v)
|
||||||
|
|
||||||
parseProblemAt pos level code msg = do
|
parseProblemAtWithEnd start end level code msg = do
|
||||||
irrelevant <- shouldIgnoreCode code
|
irrelevant <- shouldIgnoreCode code
|
||||||
unless irrelevant $
|
unless irrelevant $
|
||||||
Ms.modify (\state -> state {
|
Ms.modify (\state -> state {
|
||||||
parseProblems = note:parseProblems state
|
parseProblems = note:parseProblems state
|
||||||
})
|
})
|
||||||
where
|
where
|
||||||
note = ParseNote pos level code msg
|
note = ParseNote start end level code msg
|
||||||
|
|
||||||
|
parseProblemAt pos = parseProblemAtWithEnd pos pos
|
||||||
|
|
||||||
-- Store non-parse problems inside
|
-- Store non-parse problems inside
|
||||||
|
|
||||||
@@ -271,7 +338,9 @@ parseNote c l a = do
|
|||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
parseNoteAt pos c l a
|
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
|
--------- Convenient combinators
|
||||||
thenSkip main follow = do
|
thenSkip main follow = do
|
||||||
@@ -342,7 +411,7 @@ readConditionContents single =
|
|||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
s <- many1 letter
|
s <- many1 letter
|
||||||
when (s `elem` commonCommands) $
|
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
|
where
|
||||||
spacingOrLf = condSpacing True
|
spacingOrLf = condSpacing True
|
||||||
@@ -378,7 +447,7 @@ readConditionContents single =
|
|||||||
c <- oneOf "'\""
|
c <- oneOf "'\""
|
||||||
s <- anyOp
|
s <- anyOp
|
||||||
char c
|
char c
|
||||||
return s
|
return $ escaped s
|
||||||
|
|
||||||
anyOp = flagOp <|> flaglessOp <|> fail
|
anyOp = flagOp <|> flaglessOp <|> fail
|
||||||
"Expected comparison operator (don't wrap commands in []/[[]])"
|
"Expected comparison operator (don't wrap commands in []/[[]])"
|
||||||
@@ -522,8 +591,8 @@ readConditionContents single =
|
|||||||
where
|
where
|
||||||
readGlobLiteral = do
|
readGlobLiteral = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
s <- many1 (extglobStart <|> oneOf "{}[]$")
|
s <- extglobStart <|> oneOf "{}[]$"
|
||||||
return $ T_Literal id s
|
return $ T_Literal id [s]
|
||||||
readGroup = called "regex grouping" $ do
|
readGroup = called "regex grouping" $ do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
char '('
|
char '('
|
||||||
@@ -581,6 +650,7 @@ prop_a19= isOk readArithmeticContents "\\\n3 +\\\n 2"
|
|||||||
prop_a20= isOk readArithmeticContents "a ? b ? c : d : e"
|
prop_a20= isOk readArithmeticContents "a ? b ? c : d : e"
|
||||||
prop_a21= isOk readArithmeticContents "a ? b : c ? d : e"
|
prop_a21= isOk readArithmeticContents "a ? b : c ? d : e"
|
||||||
prop_a22= isOk readArithmeticContents "!!a"
|
prop_a22= isOk readArithmeticContents "!!a"
|
||||||
|
readArithmeticContents :: Monad m => SCParser m Token
|
||||||
readArithmeticContents =
|
readArithmeticContents =
|
||||||
readSequence
|
readSequence
|
||||||
where
|
where
|
||||||
@@ -594,12 +664,40 @@ readArithmeticContents =
|
|||||||
id <- getNextId
|
id <- getNextId
|
||||||
op <- choice (map (\x -> try $ do
|
op <- choice (map (\x -> try $ do
|
||||||
s <- string x
|
s <- string x
|
||||||
notFollowedBy2 $ oneOf "&|<>="
|
failIfIncompleteOp
|
||||||
return s
|
return s
|
||||||
) op)
|
) op)
|
||||||
spacing
|
spacing
|
||||||
return $ token id op
|
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
|
readArrayIndex = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
char '['
|
char '['
|
||||||
@@ -644,7 +742,9 @@ readArithmeticContents =
|
|||||||
l <- readAssignment `sepBy` (char ',' >> spacing)
|
l <- readAssignment `sepBy` (char ',' >> spacing)
|
||||||
return $ TA_Sequence id l
|
return $ TA_Sequence id l
|
||||||
|
|
||||||
readAssignment = readTrinary `splitBy` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
readAssignment = chainr1 readTrinary readAssignmentOp
|
||||||
|
readAssignmentOp = readComboOp ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] TA_Assignment
|
||||||
|
|
||||||
readTrinary = do
|
readTrinary = do
|
||||||
x <- readLogicalOr
|
x <- readLogicalOr
|
||||||
do
|
do
|
||||||
@@ -667,7 +767,7 @@ readArithmeticContents =
|
|||||||
readEquated = readCompared `splitBy` ["==", "!="]
|
readEquated = readCompared `splitBy` ["==", "!="]
|
||||||
readCompared = readShift `splitBy` ["<=", ">=", "<", ">"]
|
readCompared = readShift `splitBy` ["<=", ">=", "<", ">"]
|
||||||
readShift = readAddition `splitBy` ["<<", ">>"]
|
readShift = readAddition `splitBy` ["<<", ">>"]
|
||||||
readAddition = readMultiplication `splitBy` ["+", "-"]
|
readAddition = chainl1 readMultiplication (readBinary ["+"] <|> readMinusOp)
|
||||||
readMultiplication = readExponential `splitBy` ["*", "/", "%"]
|
readMultiplication = readExponential `splitBy` ["*", "/", "%"]
|
||||||
readExponential = readAnyNegated `splitBy` ["**"]
|
readExponential = readAnyNegated `splitBy` ["**"]
|
||||||
|
|
||||||
@@ -744,7 +844,7 @@ readCondition = called "test expression" $ do
|
|||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
space <- allspacing
|
space <- allspacing
|
||||||
when (null space) $
|
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
|
if single
|
||||||
then "[ and before the ]."
|
then "[ and before the ]."
|
||||||
else "[[ and before the ]]."
|
else "[[ and before the ]]."
|
||||||
@@ -769,10 +869,11 @@ readAnnotationPrefix = do
|
|||||||
prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n"
|
prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n"
|
||||||
prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\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_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
|
readAnnotation = called "shellcheck annotation" $ do
|
||||||
try readAnnotationPrefix
|
try readAnnotationPrefix
|
||||||
many1 linewhitespace
|
many1 linewhitespace
|
||||||
values <- many1 (readDisable <|> readSourceOverride)
|
values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride <|> anyKey)
|
||||||
linefeed
|
linefeed
|
||||||
many linewhitespace
|
many linewhitespace
|
||||||
return $ concat values
|
return $ concat values
|
||||||
@@ -789,6 +890,14 @@ readAnnotation = called "shellcheck annotation" $ do
|
|||||||
filename <- many1 $ noneOf " \n"
|
filename <- many1 $ noneOf " \n"
|
||||||
return [SourceOverride filename]
|
return [SourceOverride filename]
|
||||||
|
|
||||||
|
readShellOverride = forKey "shell" $ do
|
||||||
|
pos <- getPosition
|
||||||
|
shell <- many1 $ noneOf " \n"
|
||||||
|
when (isNothing $ shellForExecutable shell) $
|
||||||
|
parseNoteAt pos ErrorC 1103
|
||||||
|
"This shell type is unknown. Use e.g. sh or bash."
|
||||||
|
return [ShellOverride shell]
|
||||||
|
|
||||||
forKey s p = do
|
forKey s p = do
|
||||||
try $ string s
|
try $ string s
|
||||||
char '='
|
char '='
|
||||||
@@ -796,6 +905,13 @@ readAnnotation = called "shellcheck annotation" $ do
|
|||||||
many linewhitespace
|
many linewhitespace
|
||||||
return value
|
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
|
readAnnotations = do
|
||||||
annotations <- many (readAnnotation `thenSkip` allspacing)
|
annotations <- many (readAnnotation `thenSkip` allspacing)
|
||||||
return $ concat annotations
|
return $ concat annotations
|
||||||
@@ -811,6 +927,9 @@ prop_readNormalWord3 = isOk readNormalWord "foo#"
|
|||||||
prop_readNormalWord4 = isOk readNormalWord "$\"foo\"$'foo\nbar'"
|
prop_readNormalWord4 = isOk readNormalWord "$\"foo\"$'foo\nbar'"
|
||||||
prop_readNormalWord5 = isWarning readNormalWord "${foo}}"
|
prop_readNormalWord5 = isWarning readNormalWord "${foo}}"
|
||||||
prop_readNormalWord6 = isOk readNormalWord "foo/{}"
|
prop_readNormalWord6 = isOk readNormalWord "foo/{}"
|
||||||
|
prop_readNormalWord7 = isOk readNormalWord "foo\\\nbar"
|
||||||
|
prop_readNormalWord8 = isWarning readSubshell "(foo\\ \nbar)"
|
||||||
|
prop_readNormalWord9 = isOk readSubshell "(foo\\ ;\nbar)"
|
||||||
readNormalWord = readNormalishWord ""
|
readNormalWord = readNormalishWord ""
|
||||||
|
|
||||||
readNormalishWord end = do
|
readNormalishWord end = do
|
||||||
@@ -820,6 +939,20 @@ readNormalishWord end = do
|
|||||||
checkPossibleTermination pos x
|
checkPossibleTermination pos x
|
||||||
return $ T_NormalWord id 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] =
|
checkPossibleTermination pos [T_Literal _ x] =
|
||||||
when (x `elem` ["do", "done", "then", "fi", "esac"]) $
|
when (x `elem` ["do", "done", "then", "fi", "esac"]) $
|
||||||
parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
|
parseProblemAt pos WarningC 1010 $ "Use semicolon or linefeed before '" ++ x ++ "' (or quote to make it literal)."
|
||||||
@@ -983,13 +1116,18 @@ subParse pos parser input = do
|
|||||||
setPosition lastPosition
|
setPosition lastPosition
|
||||||
return result
|
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
|
context <- Ms.get
|
||||||
success context <|> failure context
|
success context <|> failure context
|
||||||
where
|
where
|
||||||
success c = do
|
success c = do
|
||||||
res <- try parser
|
res <- try parser
|
||||||
Ms.put c
|
when alsoOnSuccess $ Ms.put c
|
||||||
return res
|
return res
|
||||||
failure c = do
|
failure c = do
|
||||||
Ms.put c
|
Ms.put c
|
||||||
@@ -1090,6 +1228,7 @@ readNormalEscaped = called "escaped char" $ do
|
|||||||
backslash
|
backslash
|
||||||
do
|
do
|
||||||
next <- quotable <|> oneOf "?*@!+[]{}.,~#"
|
next <- quotable <|> oneOf "?*@!+[]{}.,~#"
|
||||||
|
when (next == ' ') $ checkTrailingSpaces pos <|> return ()
|
||||||
return $ if next == '\n' then "" else [next]
|
return $ if next == '\n' then "" else [next]
|
||||||
<|>
|
<|>
|
||||||
do
|
do
|
||||||
@@ -1106,6 +1245,11 @@ readNormalEscaped = called "escaped char" $ do
|
|||||||
escapedChar 'r' = Just "carriage return"
|
escapedChar 'r' = Just "carriage return"
|
||||||
escapedChar _ = Nothing
|
escapedChar _ = Nothing
|
||||||
|
|
||||||
|
checkTrailingSpaces pos = lookAhead . try $ do
|
||||||
|
many linewhitespace
|
||||||
|
void linefeed <|> eof
|
||||||
|
parseProblemAt pos ErrorC 1101 "Delete trailing spaces after \\ to break line (or use quotes for literal space)."
|
||||||
|
|
||||||
|
|
||||||
prop_readExtglob1 = isOk readExtglob "!(*.mp3)"
|
prop_readExtglob1 = isOk readExtglob "!(*.mp3)"
|
||||||
prop_readExtglob2 = isOk readExtglob "!(*.mp3|*.wmv)"
|
prop_readExtglob2 = isOk readExtglob "!(*.mp3|*.wmv)"
|
||||||
@@ -1217,7 +1361,20 @@ readBraced = try braceExpansion
|
|||||||
|
|
||||||
readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
|
readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
|
||||||
readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
|
readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
|
||||||
readDollarExpression = readDollarArithmetic <|> readDollarBracket <|> readDollarBraceCommandExpansion <|> readDollarBraced <|> readDollarExpansion <|> readDollarVariable
|
|
||||||
|
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
|
||||||
|
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'"
|
prop_readDollarSingleQuote = isOk readDollarSingleQuote "$'foo\\\'lol'"
|
||||||
readDollarSingleQuote = called "$'..' expression" $ do
|
readDollarSingleQuote = called "$'..' expression" $ do
|
||||||
@@ -1243,7 +1400,9 @@ readDollarArithmetic = called "$((..)) expression" $ do
|
|||||||
id <- getNextId
|
id <- getNextId
|
||||||
try (string "$((")
|
try (string "$((")
|
||||||
c <- readArithmeticContents
|
c <- readArithmeticContents
|
||||||
string "))"
|
pos <- getPosition
|
||||||
|
char ')'
|
||||||
|
char ')' <|> fail "Expected a double )) to end the $((..))"
|
||||||
return (T_DollarArithmetic id c)
|
return (T_DollarArithmetic id c)
|
||||||
|
|
||||||
readDollarBracket = called "$[..] expression" $ do
|
readDollarBracket = called "$[..] expression" $ do
|
||||||
@@ -1261,6 +1420,20 @@ readArithmeticExpression = called "((..)) command" $ do
|
|||||||
string "))"
|
string "))"
|
||||||
return (T_Arithmetic id c)
|
return (T_Arithmetic id c)
|
||||||
|
|
||||||
|
-- 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 <|> try (withAlt pos) <|> expected
|
||||||
|
where
|
||||||
|
withAlt pos = do
|
||||||
|
t <- forgetOnFailure alternative
|
||||||
|
warner pos
|
||||||
|
return t
|
||||||
|
|
||||||
prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }"
|
prop_readDollarBraceCommandExpansion1 = isOk readDollarBraceCommandExpansion "${ ls; }"
|
||||||
prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}"
|
prop_readDollarBraceCommandExpansion2 = isOk readDollarBraceCommandExpansion "${\nls\n}"
|
||||||
readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do
|
readDollarBraceCommandExpansion = called "ksh ${ ..; } command expansion" $ do
|
||||||
@@ -1343,14 +1516,17 @@ readDollarLonely = do
|
|||||||
n <- lookAhead (anyChar <|> (eof >> return '_'))
|
n <- lookAhead (anyChar <|> (eof >> return '_'))
|
||||||
return $ T_Literal id "$"
|
return $ T_Literal id "$"
|
||||||
|
|
||||||
prop_readHereDoc = isOk readHereDoc "<< foo\nlol\ncow\nfoo"
|
prop_readHereDoc = isOk readScript "cat << foo\nlol\ncow\nfoo"
|
||||||
prop_readHereDoc2 = isWarning readHereDoc "<<- EOF\n cow\n EOF"
|
prop_readHereDoc2 = isWarning readScript "cat <<- EOF\n cow\n EOF"
|
||||||
prop_readHereDoc3 = isOk readHereDoc "<< foo\n$\"\nfoo"
|
prop_readHereDoc3 = isOk readScript "cat << foo\n$\"\nfoo"
|
||||||
prop_readHereDoc4 = isOk readHereDoc "<< foo\n`\nfoo"
|
prop_readHereDoc4 = isOk readScript "cat << foo\n`\nfoo"
|
||||||
prop_readHereDoc5 = isOk readHereDoc "<<- !foo\nbar\n!foo"
|
prop_readHereDoc5 = isOk readScript "cat <<- !foo\nbar\n!foo"
|
||||||
prop_readHereDoc6 = isOk readHereDoc "<< foo\\ bar\ncow\nfoo bar"
|
prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
|
||||||
prop_readHereDoc7 = isOk readHereDoc "<< foo\n\\$(f ())\nfoo"
|
prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
|
||||||
prop_readHereDoc8 = isOk readHereDoc "<<foo>>bar\netc\nfoo"
|
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"
|
||||||
readHereDoc = called "here document" $ do
|
readHereDoc = called "here document" $ do
|
||||||
fid <- getNextId
|
fid <- getNextId
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
@@ -1367,24 +1543,11 @@ readHereDoc = called "here document" $ do
|
|||||||
liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral
|
liftM (\ x -> (Quoted, stripLiteral x)) readDoubleQuotedLiteral
|
||||||
<|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral
|
<|> liftM (\ x -> (Quoted, x)) readSingleQuotedLiteral
|
||||||
<|> (readToken >>= (\x -> return (Unquoted, x)))
|
<|> (readToken >>= (\x -> return (Unquoted, x)))
|
||||||
spacing
|
|
||||||
|
|
||||||
startPos <- getPosition
|
|
||||||
hereData <- anyChar `reluctantlyTill` do
|
|
||||||
linefeed
|
|
||||||
spacing
|
|
||||||
string endToken
|
|
||||||
disregard linefeed <|> eof
|
|
||||||
|
|
||||||
do
|
|
||||||
linefeed
|
|
||||||
spaces <- spacing
|
|
||||||
verifyHereDoc dashed quoted spaces hereData
|
|
||||||
string endToken
|
|
||||||
parsedData <- parseHereData quoted startPos hereData
|
|
||||||
return $ T_FdRedirect fid "" $ T_HereDoc hid dashed quoted endToken parsedData
|
|
||||||
`attempting` (eof >> debugHereDoc tokenPosition endToken hereData)
|
|
||||||
|
|
||||||
|
-- add empty tokens for now, read the rest in readPendingHereDocs
|
||||||
|
let doc = T_HereDoc hid dashed quoted endToken []
|
||||||
|
addPendingHereDoc doc
|
||||||
|
return doc
|
||||||
where
|
where
|
||||||
stripLiteral (T_Literal _ x) = x
|
stripLiteral (T_Literal _ x) = x
|
||||||
stripLiteral (T_SingleQuoted _ x) = x
|
stripLiteral (T_SingleQuoted _ x) = x
|
||||||
@@ -1399,6 +1562,27 @@ readHereDoc = called "here document" $ do
|
|||||||
c <- anyChar
|
c <- anyChar
|
||||||
return [c]
|
return [c]
|
||||||
|
|
||||||
|
|
||||||
|
readPendingHereDocs = do
|
||||||
|
docs <- popPendingHereDocs
|
||||||
|
mapM_ readDoc docs
|
||||||
|
where
|
||||||
|
readDoc (T_HereDoc id dashed quoted endToken _) = do
|
||||||
|
pos <- getPosition
|
||||||
|
hereData <- anyChar `reluctantlyTill` do
|
||||||
|
spacing
|
||||||
|
string endToken
|
||||||
|
disregard (char '\n') <|> eof
|
||||||
|
do
|
||||||
|
spaces <- spacing
|
||||||
|
verifyHereDoc dashed quoted spaces hereData
|
||||||
|
string endToken
|
||||||
|
parsedData <- parseHereData quoted pos hereData
|
||||||
|
list <- parseHereData quoted pos hereData
|
||||||
|
addToHereDocMap id list
|
||||||
|
|
||||||
|
`attempting` (eof >> debugHereDoc pos endToken hereData)
|
||||||
|
|
||||||
parseHereData Quoted startPos hereData = do
|
parseHereData Quoted startPos hereData = do
|
||||||
id <- getNextIdAt startPos
|
id <- getNextIdAt startPos
|
||||||
return [T_Literal id hereData]
|
return [T_Literal id hereData]
|
||||||
@@ -1434,7 +1618,13 @@ readHereDoc = called "here document" $ do
|
|||||||
|
|
||||||
|
|
||||||
readFilename = readNormalWord
|
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)\""
|
prop_readIoFile = isOk readIoFile ">> \"$(date +%YYmmDD)\""
|
||||||
readIoFile = called "redirection" $ do
|
readIoFile = called "redirection" $ do
|
||||||
@@ -1442,35 +1632,31 @@ readIoFile = called "redirection" $ do
|
|||||||
op <- readIoFileOp
|
op <- readIoFileOp
|
||||||
spacing
|
spacing
|
||||||
file <- readFilename
|
file <- readFilename
|
||||||
return $ T_FdRedirect id "" $ T_IoFile id op file
|
return $ T_IoFile id op file
|
||||||
|
|
||||||
readIoVariable = try $ do
|
readIoVariable = try $ do
|
||||||
char '{'
|
char '{'
|
||||||
x <- readVariableName
|
x <- readVariableName
|
||||||
char '}'
|
char '}'
|
||||||
lookAhead readIoFileOp
|
|
||||||
return $ "{" ++ x ++ "}"
|
return $ "{" ++ x ++ "}"
|
||||||
|
|
||||||
readIoNumber = try $ do
|
readIoSource = try $ do
|
||||||
x <- many1 digit <|> string "&"
|
x <- string "&" <|> readIoVariable <|> many digit
|
||||||
lookAhead readIoFileOp
|
lookAhead $ void readIoFileOp <|> void (string "<<")
|
||||||
return x
|
return x
|
||||||
|
|
||||||
prop_readIoNumberRedirect = isOk readIoNumberRedirect "3>&2"
|
prop_readIoRedirect = isOk readIoRedirect "3>&2"
|
||||||
prop_readIoNumberRedirect2 = isOk readIoNumberRedirect "2> lol"
|
prop_readIoRedirect2 = isOk readIoRedirect "2> lol"
|
||||||
prop_readIoNumberRedirect3 = isOk readIoNumberRedirect "4>&-"
|
prop_readIoRedirect3 = isOk readIoRedirect "4>&-"
|
||||||
prop_readIoNumberRedirect4 = isOk readIoNumberRedirect "&> lol"
|
prop_readIoRedirect4 = isOk readIoRedirect "&> lol"
|
||||||
prop_readIoNumberRedirect5 = isOk readIoNumberRedirect "{foo}>&2"
|
prop_readIoRedirect5 = isOk readIoRedirect "{foo}>&2"
|
||||||
prop_readIoNumberRedirect6 = isOk readIoNumberRedirect "{foo}<&-"
|
prop_readIoRedirect6 = isOk readIoRedirect "{foo}<&-"
|
||||||
readIoNumberRedirect = do
|
readIoRedirect = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
n <- readIoVariable <|> readIoNumber
|
n <- readIoSource
|
||||||
op <- readHereString <|> readHereDoc <|> readIoFile
|
redir <- readHereString <|> readHereDoc <|> readIoDuplicate <|> readIoFile
|
||||||
let actualOp = case op of T_FdRedirect _ "" x -> x
|
|
||||||
spacing
|
spacing
|
||||||
return $ T_FdRedirect id n actualOp
|
return $ T_FdRedirect id n redir
|
||||||
|
|
||||||
readIoRedirect = choice [ readIoNumberRedirect, readHereString, readHereDoc, readIoFile ] `thenSkip` spacing
|
|
||||||
|
|
||||||
readRedirectList = many1 readIoRedirect
|
readRedirectList = many1 readIoRedirect
|
||||||
|
|
||||||
@@ -1481,9 +1667,9 @@ readHereString = called "here string" $ do
|
|||||||
spacing
|
spacing
|
||||||
id2 <- getNextId
|
id2 <- getNextId
|
||||||
word <- readNormalWord
|
word <- readNormalWord
|
||||||
return $ T_FdRedirect id "" $ T_HereString id2 word
|
return $ T_HereString id2 word
|
||||||
|
|
||||||
readNewlineList = many1 ((newline <|> carriageReturn) `thenSkip` spacing)
|
readNewlineList = many1 ((linefeed <|> carriageReturn) `thenSkip` spacing)
|
||||||
readLineBreak = optional readNewlineList
|
readLineBreak = optional readNewlineList
|
||||||
|
|
||||||
prop_readSeparator1 = isWarning readScript "a &; b"
|
prop_readSeparator1 = isWarning readScript "a &; b"
|
||||||
@@ -1657,6 +1843,7 @@ readTermOrNone = do
|
|||||||
eof
|
eof
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
prop_readTerm = isOk readTerm "time ( foo; bar; )"
|
||||||
readTerm = do
|
readTerm = do
|
||||||
allspacing
|
allspacing
|
||||||
m <- readAndOr
|
m <- readAndOr
|
||||||
@@ -1759,7 +1946,7 @@ readElifPart = called "elif clause" $ do
|
|||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
correctElif <- elif
|
correctElif <- elif
|
||||||
unless correctElif $
|
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
|
allspacing
|
||||||
condition <- readTerm
|
condition <- readTerm
|
||||||
|
|
||||||
@@ -2010,6 +2197,7 @@ readFunctionDefinition = called "function" $ do
|
|||||||
readWithoutFunction = try $ do
|
readWithoutFunction = try $ do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
name <- readFunctionName
|
name <- readFunctionName
|
||||||
|
guard $ name /= "time" -- Interfers with time ( foo )
|
||||||
spacing
|
spacing
|
||||||
readParens
|
readParens
|
||||||
return $ T_Function id (FunctionKeyword False) (FunctionParentheses True) name
|
return $ T_Function id (FunctionKeyword False) (FunctionParentheses True) name
|
||||||
@@ -2054,7 +2242,20 @@ readPattern = (readNormalWord `thenSkip` spacing) `sepBy1` (char '|' `thenSkip`
|
|||||||
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
|
prop_readCompoundCommand = isOk readCompoundCommand "{ echo foo; }>/dev/null"
|
||||||
readCompoundCommand = do
|
readCompoundCommand = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
cmd <- choice [ readBraceGroup, readArithmeticExpression, readSubshell, readCondition, readWhileClause, readUntilClause, readIfClause, readForClause, readSelectClause, readCaseClause, readFunctionDefinition]
|
cmd <- choice [
|
||||||
|
readBraceGroup,
|
||||||
|
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,
|
||||||
|
readUntilClause,
|
||||||
|
readIfClause,
|
||||||
|
readForClause,
|
||||||
|
readSelectClause,
|
||||||
|
readCaseClause,
|
||||||
|
readFunctionDefinition
|
||||||
|
]
|
||||||
spacing
|
spacing
|
||||||
redirs <- many readIoRedirect
|
redirs <- many readIoRedirect
|
||||||
unless (null redirs) $ optional $ do
|
unless (null redirs) $ optional $ do
|
||||||
@@ -2083,13 +2284,29 @@ readTimeSuffix = do
|
|||||||
lookAhead $ char '-'
|
lookAhead $ char '-'
|
||||||
readCmdWord
|
readCmdWord
|
||||||
|
|
||||||
-- Fixme: this is a hack that doesn't handle let '++c' or let a\>b
|
-- Fixme: this is a hack that doesn't handle let c='4'"5" or let a\>b
|
||||||
|
readLetSuffix :: Monad m => SCParser m [Token]
|
||||||
readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
|
readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
|
||||||
where
|
where
|
||||||
|
readLetExpression :: Monad m => SCParser m Token
|
||||||
readLetExpression = do
|
readLetExpression = do
|
||||||
startPos <- getPosition
|
startPos <- getPosition
|
||||||
expression <- readStringForParser readCmdWord
|
expression <- readStringForParser readCmdWord
|
||||||
subParse startPos readArithmeticContents expression
|
let (unQuoted, newPos) = kludgeAwayQuotes expression startPos
|
||||||
|
subParse newPos readArithmeticContents unQuoted
|
||||||
|
|
||||||
|
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
|
||||||
|
kludgeAwayQuotes s p =
|
||||||
|
case s of
|
||||||
|
first:rest@(_:_) ->
|
||||||
|
let (last:backwards) = reverse rest
|
||||||
|
middle = reverse backwards
|
||||||
|
in
|
||||||
|
if first `elem` "'\"" && first == last
|
||||||
|
then (middle, updatePosChar p first)
|
||||||
|
else (s, p)
|
||||||
|
x -> (s, p)
|
||||||
|
|
||||||
|
|
||||||
-- bash allows a=(b), ksh allows $a=(b). dash allows neither. Let's warn.
|
-- bash allows a=(b), ksh allows $a=(b). dash allows neither. Let's warn.
|
||||||
readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback)
|
readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback)
|
||||||
@@ -2102,7 +2319,7 @@ readEvalSuffix = many1 (readIoRedirect <|> readCmdWord <|> evalFallback)
|
|||||||
|
|
||||||
-- Get whatever a parser would parse as a string
|
-- Get whatever a parser would parse as a string
|
||||||
readStringForParser parser = do
|
readStringForParser parser = do
|
||||||
pos <- lookAhead (parser >> getPosition)
|
pos <- inSeparateContext $ lookAhead (parser >> getPosition)
|
||||||
readUntil pos
|
readUntil pos
|
||||||
where
|
where
|
||||||
readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))
|
readUntil endPos = anyChar `reluctantlyTill` (getPosition >>= guard . (== endPos))
|
||||||
@@ -2129,7 +2346,7 @@ readAssignmentWord = try $ do
|
|||||||
variable <- readVariableName
|
variable <- readVariableName
|
||||||
optional (readNormalDollar >> parseNoteAt pos ErrorC
|
optional (readNormalDollar >> parseNoteAt pos ErrorC
|
||||||
1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")
|
1067 "For indirection, use (associative) arrays or 'read \"var$n\" <<< \"value\"'")
|
||||||
index <- optionMaybe readArrayIndex
|
indices <- many readArrayIndex
|
||||||
hasLeftSpace <- liftM (not . null) spacing
|
hasLeftSpace <- liftM (not . null) spacing
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
op <- readAssignmentOp
|
op <- readAssignmentOp
|
||||||
@@ -2141,13 +2358,13 @@ readAssignmentWord = try $ do
|
|||||||
parseNoteAt pos WarningC 1007
|
parseNoteAt pos WarningC 1007
|
||||||
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
|
"Remove space after = if trying to assign a value (for empty string, use var='' ... )."
|
||||||
value <- readEmptyLiteral
|
value <- readEmptyLiteral
|
||||||
return $ T_Assignment id op variable index value
|
return $ T_Assignment id op variable indices value
|
||||||
else do
|
else do
|
||||||
when (hasLeftSpace || hasRightSpace) $
|
when (hasLeftSpace || hasRightSpace) $
|
||||||
parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments."
|
parseNoteAt pos ErrorC 1068 "Don't put spaces around the = in assignments."
|
||||||
value <- readArray <|> readNormalWord
|
value <- readArray <|> readNormalWord
|
||||||
spacing
|
spacing
|
||||||
return $ T_Assignment id op variable index value
|
return $ T_Assignment id op variable indices value
|
||||||
where
|
where
|
||||||
readAssignmentOp = do
|
readAssignmentOp = do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
@@ -2167,12 +2384,14 @@ readAssignmentWord = try $ do
|
|||||||
return $ T_Literal id ""
|
return $ T_Literal id ""
|
||||||
|
|
||||||
readArrayIndex = do
|
readArrayIndex = do
|
||||||
|
id <- getNextId
|
||||||
char '['
|
char '['
|
||||||
optional space
|
pos <- getPosition
|
||||||
x <- readArithmeticContents
|
str <- readStringForParser readIndexSpan
|
||||||
char ']'
|
char ']'
|
||||||
return x
|
return $ T_UnparsedIndex id pos str
|
||||||
|
|
||||||
|
readArray :: Monad m => SCParser m Token
|
||||||
readArray = called "array assignment" $ do
|
readArray = called "array assignment" $ do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
char '('
|
char '('
|
||||||
@@ -2185,7 +2404,7 @@ readArray = called "array assignment" $ do
|
|||||||
readIndexed = do
|
readIndexed = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
index <- try $ do
|
index <- try $ do
|
||||||
x <- readArrayIndex
|
x <- many1 readArrayIndex
|
||||||
char '='
|
char '='
|
||||||
return x
|
return x
|
||||||
value <- readNormalWord <|> nothing
|
value <- readNormalWord <|> nothing
|
||||||
@@ -2288,8 +2507,11 @@ ifParse p t f =
|
|||||||
|
|
||||||
prop_readShebang1 = isOk readShebang "#!/bin/sh\n"
|
prop_readShebang1 = isOk readShebang "#!/bin/sh\n"
|
||||||
prop_readShebang2 = isWarning readShebang "!# /bin/sh\n"
|
prop_readShebang2 = isWarning readShebang "!# /bin/sh\n"
|
||||||
|
prop_readShebang3 = isNotOk readShebang "#shellcheck shell=/bin/sh\n"
|
||||||
|
prop_readShebang4 = isWarning readShebang "! /bin/sh"
|
||||||
readShebang = do
|
readShebang = do
|
||||||
try readCorrect <|> try readSwapped
|
try readCorrect <|> try readSwapped <|> try readMissingHash
|
||||||
|
many linewhitespace
|
||||||
str <- many $ noneOf "\r\n"
|
str <- many $ noneOf "\r\n"
|
||||||
optional carriageReturn
|
optional carriageReturn
|
||||||
optional linefeed
|
optional linefeed
|
||||||
@@ -2302,6 +2524,15 @@ readShebang = do
|
|||||||
parseProblemAt pos ErrorC 1084
|
parseProblemAt pos ErrorC 1084
|
||||||
"Use #!, not !#, for the shebang."
|
"Use #!, not !#, for the shebang."
|
||||||
|
|
||||||
|
readMissingHash = do
|
||||||
|
pos <- getPosition
|
||||||
|
char '!'
|
||||||
|
lookAhead $ do
|
||||||
|
many linewhitespace
|
||||||
|
char '/'
|
||||||
|
parseProblemAt pos ErrorC 1104
|
||||||
|
"Use #!, not just !, for the shebang."
|
||||||
|
|
||||||
verifyEof = eof <|> choice [
|
verifyEof = eof <|> choice [
|
||||||
ifParsable g_Lparen $
|
ifParsable g_Lparen $
|
||||||
parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",
|
parseProblem ErrorC 1088 "Parsing stopped here. Invalid use of parentheses?",
|
||||||
@@ -2316,12 +2547,12 @@ verifyEof = eof <|> choice [
|
|||||||
try (lookAhead p)
|
try (lookAhead p)
|
||||||
action
|
action
|
||||||
|
|
||||||
prop_readScript1 = isOk readScript "#!/bin/bash\necho hello world\n"
|
prop_readScript1 = isOk readScriptFile "#!/bin/bash\necho hello world\n"
|
||||||
prop_readScript2 = isWarning readScript "#!/bin/bash\r\necho hello world\n"
|
prop_readScript2 = isWarning readScriptFile "#!/bin/bash\r\necho hello world\n"
|
||||||
prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
|
prop_readScript3 = isWarning readScriptFile "#!/bin/bash\necho hello\xA0world"
|
||||||
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
prop_readScript4 = isWarning readScriptFile "#!/usr/bin/perl\nfoo=("
|
||||||
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
|
prop_readScript5 = isOk readScriptFile "#!/bin/bash\n#This is an empty script\n\n"
|
||||||
readScript = do
|
readScriptFile = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
optional $ do
|
optional $ do
|
||||||
@@ -2332,9 +2563,12 @@ readScript = do
|
|||||||
verifyShell pos (getShell sb)
|
verifyShell pos (getShell sb)
|
||||||
if isValidShell (getShell sb) /= Just False
|
if isValidShell (getShell sb) /= Just False
|
||||||
then do
|
then do
|
||||||
commands <- readCompoundListOrEmpty
|
annotationId <- getNextId
|
||||||
|
annotations <- readAnnotations
|
||||||
|
commands <- withAnnotations annotations readCompoundListOrEmpty
|
||||||
verifyEof
|
verifyEof
|
||||||
return $ T_Script id sb commands
|
let script = T_Annotation annotationId annotations $ T_Script id sb commands
|
||||||
|
reparseIndices script
|
||||||
else do
|
else do
|
||||||
many anyChar
|
many anyChar
|
||||||
return $ T_Script id sb []
|
return $ T_Script id sb []
|
||||||
@@ -2386,6 +2620,9 @@ readScript = do
|
|||||||
|
|
||||||
readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF"
|
readUtf8Bom = called "Byte Order Mark" $ string "\xFEFF"
|
||||||
|
|
||||||
|
readScript = do
|
||||||
|
script <- readScriptFile
|
||||||
|
reparseIndices script
|
||||||
|
|
||||||
isWarning p s = parsesCleanly p s == Just False
|
isWarning p s = parsesCleanly p s == Just False
|
||||||
isOk p s = parsesCleanly p s == Just True
|
isOk p s = parsesCleanly p s == Just True
|
||||||
@@ -2405,17 +2642,18 @@ parsesCleanly parser string = runIdentity $ do
|
|||||||
|
|
||||||
parseWithNotes parser = do
|
parseWithNotes parser = do
|
||||||
item <- parser
|
item <- parser
|
||||||
map <- getMap
|
state <- getState
|
||||||
parseNotes <- getParseNotes
|
return (item, state)
|
||||||
return (item, map, nub . sortNotes $ parseNotes)
|
|
||||||
|
|
||||||
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
|
sortNotes = sortBy compareNotes
|
||||||
|
|
||||||
|
|
||||||
makeErrorFor parsecError =
|
makeErrorFor parsecError =
|
||||||
ParseNote (errorPos parsecError) ErrorC 1072 $
|
ParseNote pos pos ErrorC 1072 $
|
||||||
getStringFromParsec $ errorMessages parsecError
|
getStringFromParsec $ errorMessages parsecError
|
||||||
|
where
|
||||||
|
pos = errorPos parsecError
|
||||||
|
|
||||||
getStringFromParsec errors =
|
getStringFromParsec errors =
|
||||||
case map f errors of
|
case map f errors of
|
||||||
@@ -2447,11 +2685,12 @@ system = lift . lift . lift
|
|||||||
parseShell sys name contents = do
|
parseShell sys name contents = do
|
||||||
(result, state) <- runParser sys (parseWithNotes readScript) name contents
|
(result, state) <- runParser sys (parseWithNotes readScript) name contents
|
||||||
case result of
|
case result of
|
||||||
Right (script, tokenMap, notes) ->
|
Right (script, userstate) ->
|
||||||
return ParseResult {
|
return ParseResult {
|
||||||
prComments = map toPositionedComment $ nub $ notes ++ parseProblems state,
|
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
|
||||||
prTokenPositions = Map.map posToPos tokenMap,
|
prTokenPositions = Map.map posToPos (positionMap userstate),
|
||||||
prRoot = Just script
|
prRoot = Just $
|
||||||
|
reattachHereDocs script (hereDocMap userstate)
|
||||||
}
|
}
|
||||||
Left err ->
|
Left err ->
|
||||||
return ParseResult {
|
return ParseResult {
|
||||||
@@ -2467,15 +2706,57 @@ parseShell sys name contents = do
|
|||||||
isName (ContextName _ _) = True
|
isName (ContextName _ _) = True
|
||||||
isName _ = False
|
isName _ = False
|
||||||
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
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 ++ "."
|
"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 ++ "."
|
"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
|
||||||
|
f t@(T_HereDoc id dash quote string []) = fromMaybe t $ do
|
||||||
|
list <- Map.lookup id map
|
||||||
|
return $ T_HereDoc id dash quote string list
|
||||||
|
f t = t
|
||||||
|
|
||||||
toPositionedComment :: ParseNote -> PositionedComment
|
toPositionedComment :: ParseNote -> PositionedComment
|
||||||
toPositionedComment (ParseNote pos severity code message) =
|
toPositionedComment (ParseNote start end severity code message) =
|
||||||
PositionedComment (posToPos pos) $ Comment severity code message
|
PositionedComment (posToPos start) (posToPos end) $ Comment severity code message
|
||||||
|
|
||||||
posToPos :: SourcePos -> Position
|
posToPos :: SourcePos -> Position
|
||||||
posToPos sp = Position {
|
posToPos sp = Position {
|
||||||
|
10
quicktest
10
quicktest
@@ -4,12 +4,18 @@
|
|||||||
# 'cabal test' remains the source of truth.
|
# 'cabal test' remains the source of truth.
|
||||||
|
|
||||||
(
|
(
|
||||||
var=$(echo 'liftM and $ sequence [ShellCheck.Analytics.runTests, ShellCheck.Parser.runTests, ShellCheck.Checker.runTests]' | cabal repl | tee /dev/stderr)
|
var=$(echo 'liftM and $ sequence [
|
||||||
|
ShellCheck.Analytics.runTests
|
||||||
|
,ShellCheck.Parser.runTests
|
||||||
|
,ShellCheck.Checker.runTests
|
||||||
|
,ShellCheck.Checks.Commands.runTests
|
||||||
|
,ShellCheck.AnalyzerLib.runTests
|
||||||
|
]' | tr -d '\n' | cabal repl 2>&1 | tee /dev/stderr)
|
||||||
if [[ $var == *$'\nTrue'* ]]
|
if [[ $var == *$'\nTrue'* ]]
|
||||||
then
|
then
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
grep -C 3 "Fail" <<< "$var"
|
grep -C 3 -e "Fail" -e "Tracing" <<< "$var"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
) 2>&1
|
) 2>&1
|
||||||
|
@@ -34,7 +34,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||||||
|
|
||||||
**-C**[*WHEN*],\ **--color**[=*WHEN*]
|
**-C**[*WHEN*],\ **--color**[=*WHEN*]
|
||||||
|
|
||||||
: For TTY outut, enable colors *always*, *never* or *auto*. The default
|
: For TTY output, enable colors *always*, *never* or *auto*. The default
|
||||||
is *auto*. **--color** without an argument is equivalent to
|
is *auto*. **--color** without an argument is equivalent to
|
||||||
**--color=always**.
|
**--color=always**.
|
||||||
|
|
||||||
|
35
stack.yaml
Normal file
35
stack.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# This file was automatically generated by stack init
|
||||||
|
# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||||
|
|
||||||
|
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
|
||||||
|
resolver: lts-5.5
|
||||||
|
|
||||||
|
# Local packages, usually specified by relative directory name
|
||||||
|
packages:
|
||||||
|
- '.'
|
||||||
|
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
|
||||||
|
extra-deps: []
|
||||||
|
|
||||||
|
# Override default flag values for local packages and extra-deps
|
||||||
|
flags: {}
|
||||||
|
|
||||||
|
# Extra package databases containing global packages
|
||||||
|
extra-package-dbs: []
|
||||||
|
|
||||||
|
# Control whether we use the GHC we find on the path
|
||||||
|
# system-ghc: true
|
||||||
|
|
||||||
|
# Require a specific version of stack, using version ranges
|
||||||
|
# require-stack-version: -any # Default
|
||||||
|
# require-stack-version: >= 1.0.0
|
||||||
|
|
||||||
|
# Override the architecture used by stack, especially useful on Windows
|
||||||
|
# arch: i386
|
||||||
|
# arch: x86_64
|
||||||
|
|
||||||
|
# Extra directories used by stack for building
|
||||||
|
# extra-include-dirs: [/path/to/dir]
|
||||||
|
# extra-lib-dirs: [/path/to/dir]
|
||||||
|
|
||||||
|
# Allow a newer minor version of GHC than the snapshot specifies
|
||||||
|
# compiler-check: newer-minor
|
@@ -4,13 +4,17 @@ import Control.Monad
|
|||||||
import System.Exit
|
import System.Exit
|
||||||
import qualified ShellCheck.Checker
|
import qualified ShellCheck.Checker
|
||||||
import qualified ShellCheck.Analytics
|
import qualified ShellCheck.Analytics
|
||||||
|
import qualified ShellCheck.AnalyzerLib
|
||||||
import qualified ShellCheck.Parser
|
import qualified ShellCheck.Parser
|
||||||
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
|
||||||
main = do
|
main = do
|
||||||
putStrLn "Running ShellCheck tests..."
|
putStrLn "Running ShellCheck tests..."
|
||||||
results <- sequence [
|
results <- sequence [
|
||||||
ShellCheck.Checker.runTests,
|
ShellCheck.Checker.runTests,
|
||||||
|
ShellCheck.Checks.Commands.runTests,
|
||||||
ShellCheck.Analytics.runTests,
|
ShellCheck.Analytics.runTests,
|
||||||
|
ShellCheck.AnalyzerLib.runTests,
|
||||||
ShellCheck.Parser.runTests
|
ShellCheck.Parser.runTests
|
||||||
]
|
]
|
||||||
if and results
|
if and results
|
||||||
|
Reference in New Issue
Block a user