19 Commits

Author SHA1 Message Date
Vidar Holen
cb57b4a74f Stable version 0.6.0
This release is dedicated to Factorio. If this is how much fun it is to
build factories and oppress natives, then history makes a lot of sense.
2018-12-02 19:08:06 -08:00
Vidar Holen
e3f0243c0e Add 'striptests' script to Cabal package 2018-12-02 19:08:06 -08:00
Vidar Holen
66b5f13c6f Make wiki links fit in 80 columns 2018-12-02 19:08:06 -08:00
Vidar Holen
a7a404a5a8 Fill in missing bits in CHANGELOG 2018-12-02 14:49:04 -08:00
Vidar Holen
0761f5c923 Merge pull request #1397 from ngzhian/man-en-dash
Disable smart typography extension for markdown input
2018-12-02 13:56:14 -08:00
Vidar Holen
b55149b22d Add man page instructions (fixes #1347) 2018-12-02 12:29:42 -08:00
Ng Zhi An
4097bb5154 Disable smart typography extension for markdown input
Fixes #1392
2018-11-29 23:18:20 -08:00
Vidar Holen
1b207b3d43 Preemptively fix possible '-- |' breakage 2018-11-26 20:43:15 -08:00
Vidar Holen
135b4aa485 Add stack builds to distro test 2018-11-26 20:41:29 -08:00
Vidar Holen
cb76951ad2 Add warnings for 'exit' similar to 'return' (fixes #1388) 2018-11-24 23:05:40 -08:00
Vidar Holen
705e476e4c Merge pull request #1389 from romanzolotarev/patch-1
Add OpenBSD to "Installing" section of README.md
2018-11-15 19:58:30 +11:00
Roman Zolotarev
e705552c97 Update README.html
Add OpenBSD to "Installing" section.
2018-11-15 08:49:26 +00:00
Vidar Holen
198aa4fc3d Merge pull request #1383 from l2dy/master
Fix typo in CHANGELOG
2018-11-08 11:13:55 +08:00
Zero King
f4044fbcc7 Fix typo in CHANGELOG 2018-11-08 03:07:52 +00:00
Vidar Holen
2827b35696 SC2240: Warn about . script args.. in sh/dash (fixes #1373) 2018-11-07 18:04:18 -08:00
Vidar Holen
de95c376ea Merge pull request #1380 from PeterDaveHello/Update-Dockerfile
Update Docker build-only image to Ubuntu 18.04
2018-11-08 08:34:21 +08:00
Peter Dave Hello
5e1b1e010a Update Docker build-only image to Ubuntu 18.04
Ref:
> Ubuntu 17.10 (Artful Aardvark) End of Life reached on July 19 2018
https://fridge.ubuntu.com/2018/07/19/ubuntu-17-10-artful-aardvark-end-of-life-reached-on-july-19-2018/
2018-11-02 13:47:22 +08:00
Vidar Holen
620c9c2023 Also warn about glob matching with [ a != b* ] (fixes #1374) 2018-11-01 04:47:44 -07:00
Vidar Holen
359b1467a2 Work around snap's old cabal + new snapcraft proxy. 2018-10-24 21:33:42 -07:00
18 changed files with 174 additions and 247 deletions

14
.snapsquid.conf Normal file
View File

@@ -0,0 +1,14 @@
# In 2015, cabal-install had a http bug triggered when proxies didn't keep
# the connection open. This version made it into Ubuntu Xenial as used by
# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug.
#
# https://bugs.launchpad.net/launchpad-buildd/+bug/1797809
#
# Workaround: add more proxy
visible_hostname localhost
http_port 8888
cache_peer 10.10.10.1 parent 8222 0 no-query default
cache_peer_domain localhost !.internal
http_access allow all

View File

@@ -1,17 +1,26 @@
## ??? ## v0.6.0 - 2018-12-02
### Added ### Added
- Command line option --severity/-S for filtering by minimum severity - Command line option --severity/-S for filtering by minimum severity
- Command line option --wiki-link-count/-W for showing wiki links - Command line option --wiki-link-count/-W for showing wiki links
- SC2152/SC2151: Warn about bad `exit` values like `1234` and `"foo"`
- SC2236/SC2237: Suggest -n/-z instead of ! -z/-n - SC2236/SC2237: Suggest -n/-z instead of ! -z/-n
- SC2238: Warn when redirecting to a known command name, e.g. ls > rm - SC2238: Warn when redirecting to a known command name, e.g. ls > rm
- SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh - SC2239: Warn if the shebang is not an absolute path, e.g. #!bin/sh
- SC2240: Warn when passing additional arguments to dot (.) in sh/dash
- SC1133: Better diagnostics when starting a line with |/||/&& - SC1133: Better diagnostics when starting a line with |/||/&&
### Changed ### Changed
- Most warnings now have useful end positions - Most warnings now have useful end positions
- SC1117 about unknown double-quoted escape sequences has been retired - SC1117 about unknown double-quoted escape sequences has been retired
### Fixed ### Fixed
- SC2021 no longer triggers for equivalence classes like '[=e=]' - SC2021 no longer triggers for equivalence classes like `[=e=]`
- SC2221/SC2222 no longer mistriggers on fall-through case branches - SC2221/SC2222 no longer mistriggers on fall-through case branches
- SC2081 about glob matches in `[ .. ]` now also triggers for `!=`
- SC2086 no longer warns about spaces in `$#`
- SC2164 no longer suggests subshells for `cd ..; cmd; cd ..`
- `read -a` is now correctly considered an array assignment
- SC2039 no longer warns about LINENO now that it's POSIX
## v0.5.0 - 2018-05-31 ## v0.5.0 - 2018-05-31
### Added ### Added

View File

@@ -1,5 +1,5 @@
# Build-only image # Build-only image
FROM ubuntu:17.10 AS build FROM ubuntu:18.04 AS build
USER root USER root
WORKDIR /opt/shellCheck WORKDIR /opt/shellCheck

View File

@@ -133,6 +133,10 @@ On OS X with homebrew:
brew install shellcheck brew install shellcheck
On OpenBSD:
pkg_add shellcheck
On openSUSE On openSUSE
zypper in ShellCheck zypper in ShellCheck
@@ -168,6 +172,11 @@ Alternatively, you can download pre-compiled binaries for the latest release her
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds. or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
pandoc -s -t man shellcheck.1.md -o shellcheck.1
sudo mv shellcheck.1 /usr/share/man/man1
## Travis CI ## Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.

View File

@@ -33,4 +33,4 @@ myPreSDist _ _ = do
putStrLn $ "pandoc exited with " ++ show result 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 -f markdown-smart -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.5.0 Version: 0.6.0
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -28,6 +28,8 @@ Extra-Source-Files:
shellcheck.1.md shellcheck.1.md
-- built with a cabal sdist hook -- built with a cabal sdist hook
shellcheck.1 shellcheck.1
-- convenience script for stripping tests
striptests
-- tests -- tests
test/shellcheck.hs test/shellcheck.hs
@@ -53,7 +55,6 @@ library
base > 4.6.0.1 && < 5, base > 4.6.0.1 && < 5,
bytestring, bytestring,
containers >= 0.5, containers >= 0.5,
deepseq >= 1.4.0.0,
directory, directory,
mtl >= 2.2.1, mtl >= 2.2.1,
parsec, parsec,
@@ -90,7 +91,6 @@ executable shellcheck
aeson, aeson,
base >= 4 && < 5, base >= 4 && < 5,
bytestring, bytestring,
deepseq >= 1.4.0.0,
ShellCheck, ShellCheck,
containers, containers,
directory, directory,
@@ -106,7 +106,6 @@ test-suite test-shellcheck
aeson, aeson,
base >= 4 && < 5, base >= 4 && < 5,
bytestring, bytestring,
deepseq >= 1.4.0.0,
ShellCheck, ShellCheck,
containers, containers,
directory, directory,

View File

@@ -37,9 +37,16 @@ parts:
source: ./ source: ./
build-packages: build-packages:
- cabal-install - cabal-install
- squid3
build: | build: |
# See comments in .snapsquid.conf
[ "$http_proxy" ] && {
squid3 -f .snapsquid.conf
export http_proxy="http://localhost:8888"
sleep 3
}
cabal sandbox init cabal sandbox init
cabal update cabal update || cat /var/log/squid/*
cabal install -j cabal install -j
install: | install: |
install -d $SNAPCRAFT_PART_INSTALL/usr/bin install -d $SNAPCRAFT_PART_INSTALL/usr/bin

View File

@@ -17,17 +17,14 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module ShellCheck.AST where module ShellCheck.AST where
import GHC.Generics (Generic)
import Control.Monad.Identity import Control.Monad.Identity
import Control.DeepSeq
import Text.Parsec import Text.Parsec
import qualified ShellCheck.Regex as Re import qualified ShellCheck.Regex as Re
import Prelude hiding (id) import Prelude hiding (id)
newtype Id = Id Int deriving (Show, Eq, Ord, Generic, NFData) newtype Id = Id Int deriving (Show, Eq, Ord)
data Quoted = Quoted | Unquoted deriving (Show, Eq) data Quoted = Quoted | Unquoted deriving (Show, Eq)
data Dashed = Dashed | Undashed deriving (Show, Eq) data Dashed = Dashed | Undashed deriving (Show, Eq)

View File

@@ -241,39 +241,6 @@ isCondition (child:parent:rest) =
T_UntilExpression id c l -> take 1 . reverse $ c T_UntilExpression id c l -> take 1 . reverse $ c
_ -> [] _ -> []
-- helpers to build replacements
replaceStart id params n r =
let tp = tokenPositions params
(start, _) = tp Map.! id
new_end = start {
posColumn = posColumn start + n
}
in
newReplacement {
repStartPos = start,
repEndPos = new_end,
repString = r
}
replaceEnd id params n r =
-- because of the way we count columns 1-based
-- we have to offset end columns by 1
let tp = tokenPositions params
(_, end) = tp Map.! id
new_start = end {
posColumn = posColumn end - n + 1
}
new_end = end {
posColumn = posColumn end + 1
}
in
newReplacement {
repStartPos = new_start,
repEndPos = new_end,
repString = r
}
surroundWidth id params s = fixWith [replaceStart id params 0 s, replaceEnd id params 0 s]
fixWith fixes = newFix { fixReplacements = fixes }
prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)" prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)"
checkEchoWc _ (T_Pipeline id _ [a, b]) = checkEchoWc _ (T_Pipeline id _ [a, b]) =
when (acmd == ["echo", "${VAR}"]) $ when (acmd == ["echo", "${VAR}"]) $
@@ -1230,11 +1197,12 @@ prop_checkComparisonAgainstGlob2 = verifyNot checkComparisonAgainstGlob "[[ $cow
prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]" prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = *foo* ]"
prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]" prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]"
prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]" prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]"
prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]"
checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _])) checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _]))
| op `elem` ["=", "==", "!="] = | op `elem` ["=", "==", "!="] =
warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching." warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching."
checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word) checkComparisonAgainstGlob _ (TC_Binary _ SingleBracket op _ word)
| (op == "=" || op == "==") && isGlob word = | op `elem` ["=", "==", "!="] && isGlob word =
err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement." err (getId word) 2081 "[ .. ] can't match globs. Use [[ .. ]] or case statement."
checkComparisonAgainstGlob _ _ = return () checkComparisonAgainstGlob _ _ = return ()
@@ -1367,10 +1335,8 @@ checkPS1Assignments _ _ = return ()
prop_checkBackticks1 = verify checkBackticks "echo `foo`" prop_checkBackticks1 = verify checkBackticks "echo `foo`"
prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)" prop_checkBackticks2 = verifyNot checkBackticks "echo $(foo)"
prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo" prop_checkBackticks3 = verifyNot checkBackticks "echo `#inlined comment` foo"
checkBackticks params (T_Backticked id list) | not (null list) = checkBackticks _ (T_Backticked id list) | not (null list) =
addComment $ style id 2006 "Use $(...) notation instead of legacy backticked `...`."
makeCommentWithFix StyleC id 2006 "Use $(...) notation instead of legacy backticked `...`."
(fixWith [replaceStart id params 1 "$(", replaceEnd id params 1 ")"])
checkBackticks _ _ = return () checkBackticks _ _ = return ()
prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}" prop_checkIndirectExpansion1 = verify checkIndirectExpansion "${foo$n}"
@@ -1674,10 +1640,8 @@ checkSpacefulness params t =
makeComment InfoC (getId token) 2223 makeComment InfoC (getId token) 2223
"This default assignment may cause DoS due to globbing. Quote it." "This default assignment may cause DoS due to globbing. Quote it."
else else
makeCommentWithFix InfoC (getId token) 2086 makeComment InfoC (getId token) 2086
"Double quote to prevent globbing and word splitting." (surroundWidth (getId token) params "\"") "Double quote to prevent globbing and word splitting."
-- makeComment InfoC (getId token) 2086
-- "Double quote to prevent globbing and word splitting."
writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return [] writeF _ _ name (DataString SourceExternal) = setSpaces name True >> return []
writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return [] writeF _ _ name (DataString SourceInteger) = setSpaces name False >> return []
@@ -2574,8 +2538,7 @@ checkUncheckedCdPushdPopd params root =
&& not (isSafeDir t) && not (isSafeDir t)
&& not (name t `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t))) && not (name t `elem` ["pushd", "popd"] && ("n" `elem` map snd (getAllFlags t)))
&& not (isCondition $ getPath (parentMap params) t)) $ && not (isCondition $ getPath (parentMap params) t)) $
warnWithFix (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails." warn (getId t) 2164 "Use 'cd ... || exit' or 'cd ... || return' in case cd fails."
(fixWith [replaceEnd (getId t) params 0 " || exit"])
checkElement _ = return () checkElement _ = return ()
name t = fromMaybe "" $ getCommandName t name t = fromMaybe "" $ getCommandName t
isSafeDir t = case oversimplify t of isSafeDir t = case oversimplify t of
@@ -2732,7 +2695,7 @@ checkArrayAssignmentIndices params root =
T_Literal id str -> [(id,str)] T_Literal id str -> [(id,str)]
_ -> [] _ -> []
guard $ '=' `elem` str guard $ '=' `elem` str
return $ warnWithFix id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it." (surroundWidth id params "\"") return $ warn id 2191 "The = here is literal. To assign by index, use ( [index]=value ) with no spaces. To keep as literal, quote it."
in in
if null literalEquals && isAssociative if null literalEquals && isAssociative
then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ." then warn (getId t) 2190 "Elements in associative arrays need index, e.g. array=( [index]=value ) ."

View File

@@ -20,7 +20,6 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
module ShellCheck.AnalyzerLib where module ShellCheck.AnalyzerLib where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
import ShellCheck.Data import ShellCheck.Data
@@ -29,16 +28,15 @@ import ShellCheck.Parser
import ShellCheck.Regex import ShellCheck.Regex
import Control.Arrow (first) import Control.Arrow (first)
import Control.DeepSeq
import Control.Monad.Identity import Control.Monad.Identity
import Control.Monad.RWS import Control.Monad.RWS
import Control.Monad.State import Control.Monad.State
import Control.Monad.Writer import Control.Monad.Writer
import Data.Char import Data.Char
import Data.List import Data.List
import qualified Data.Map as Map
import Data.Maybe import Data.Maybe
import Data.Semigroup import Data.Semigroup
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs) import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
@@ -83,8 +81,7 @@ data Parameters = Parameters {
parentMap :: Map.Map Id Token, -- A map from Id to parent Token parentMap :: Map.Map Id Token, -- A map from Id to parent Token
shellType :: Shell, -- The shell type, such as Bash or Ksh shellType :: Shell, -- The shell type, such as Bash or Ksh
shellTypeSpecified :: Bool, -- True if shell type was forced via flags shellTypeSpecified :: Bool, -- True if shell type was forced via flags
rootNode :: Token, -- The root node of the AST rootNode :: Token -- The root node of the AST
tokenPositions :: Map.Map Id (Position, Position) -- map from token id to start and end position
} }
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
@@ -145,7 +142,7 @@ makeComment severity id code note =
} }
} }
addComment note = note `deepseq` tell [note] addComment note = tell [note]
warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m () warn :: MonadWriter [TokenComment] m => Id -> Code -> String -> m ()
warn id code str = addComment $ makeComment WarningC id code str warn id code str = addComment $ makeComment WarningC id code str
@@ -153,20 +150,6 @@ err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str style id code str = addComment $ makeComment StyleC id code str
warnWithFix id code str fix = addComment $
let comment = makeComment WarningC id code str in
comment {
tcFix = Just fix
}
makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix =
let comment = makeComment severity id code str
withFix = comment {
tcFix = Just fix
}
in withFix `deepseq` withFix
makeParameters spec = makeParameters spec =
let params = Parameters { let params = Parameters {
rootNode = root, rootNode = root,
@@ -181,8 +164,7 @@ makeParameters spec =
shellTypeSpecified = isJust $ asShellType spec, shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root, variableFlow = getVariableFlow params root
tokenPositions = asTokenPositions spec
} in params } in params
where root = asScript spec where root = asScript spec

View File

@@ -42,8 +42,7 @@ tokenToPosition startMap t = fromMaybe fail $ do
return $ newPositionedComment { return $ newPositionedComment {
pcStartPos = fst span, pcStartPos = fst span,
pcEndPos = snd span, pcEndPos = snd span,
pcComment = tcComment t, pcComment = tcComment t
pcFix = tcFix t
} }
where where
fail = error "Internal shellcheck error: id doesn't exist. Please report!" fail = error "Internal shellcheck error: id doesn't exist. Please report!"
@@ -64,20 +63,11 @@ checkScript sys spec = do
psShellTypeOverride = csShellTypeOverride spec psShellTypeOverride = csShellTypeOverride spec
} }
let parseMessages = prComments result let parseMessages = prComments result
let tokenPositions = prTokenPositions result
let analysisSpec root =
as {
asScript = root,
asShellType = csShellTypeOverride spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed,
asTokenPositions = tokenPositions
} where as = newAnalysisSpec root
let analysisMessages = let analysisMessages =
fromMaybe [] $ fromMaybe [] $
(arComments . analyzeScript . analysisSpec) (arComments . analyzeScript . analysisSpec)
<$> prRoot result <$> prRoot result
let translator = tokenToPosition tokenPositions let translator = tokenToPosition (prTokenPositions result)
return . nub . sortMessages . filter shouldInclude $ return . nub . sortMessages . filter shouldInclude $
(parseMessages ++ map translator analysisMessages) (parseMessages ++ map translator analysisMessages)
@@ -100,6 +90,13 @@ checkScript sys spec = do
cMessage comment) cMessage comment)
getPosition = pcStartPos getPosition = pcStartPos
analysisSpec root =
as {
asScript = root,
asShellType = csShellTypeOverride spec,
asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed
} where as = newAnalysisSpec root
getErrors sys spec = getErrors sys spec =
sort . map getCode . crComments $ sort . map getCode . crComments $

View File

@@ -61,6 +61,7 @@ commandChecks = [
,checkGrepRe ,checkGrepRe
,checkTrapQuotes ,checkTrapQuotes
,checkReturn ,checkReturn
,checkExit
,checkFindExecWithSingleArgument ,checkFindExecWithSingleArgument
,checkUnusedEchoEscapes ,checkUnusedEchoEscapes
,checkInjectableFindSh ,checkInjectableFindSh
@@ -92,6 +93,7 @@ commandChecks = [
,checkWhich ,checkWhich
,checkSudoRedirect ,checkSudoRedirect
,checkSudoArgs ,checkSudoArgs
,checkSourceArgs
] ]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -280,15 +282,28 @@ prop_checkReturn4 = verifyNot checkReturn "return $((a|b))"
prop_checkReturn5 = verify checkReturn "return -1" prop_checkReturn5 = verify checkReturn "return -1"
prop_checkReturn6 = verify checkReturn "return 1000" prop_checkReturn6 = verify checkReturn "return 1000"
prop_checkReturn7 = verify checkReturn "return 'hello world'" prop_checkReturn7 = verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (f . arguments) checkReturn = CommandCheck (Exactly "return") (returnOrExit
(\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")
(\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))
prop_checkExit1 = verifyNot checkExit "exit"
prop_checkExit2 = verifyNot checkExit "exit 1"
prop_checkExit3 = verifyNot checkExit "exit $var"
prop_checkExit4 = verifyNot checkExit "exit $((a|b))"
prop_checkExit5 = verify checkExit "exit -1"
prop_checkExit6 = verify checkExit "exit 1000"
prop_checkExit7 = verify checkExit "exit 'hello world'"
checkExit = CommandCheck (Exactly "exit") (returnOrExit
(\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")
(\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))
returnOrExit multi invalid = (f . arguments)
where where
f (first:second:_) = f (first:second:_) =
err (getId second) 2151 multi (getId first)
"Only one integer 0-255 can be returned. Use stdout for other data."
f [value] = f [value] =
when (isInvalid $ literal value) $ when (isInvalid $ literal value) $
err (getId value) 2152 invalid (getId value)
"Can only return 0-255. Other data should be written to stdout."
f _ = return () f _ = return ()
isInvalid s = s == "" || any (not . isDigit) s || length s > 5 isInvalid s = s == "" || any (not . isDigit) s || length s > 5
@@ -1008,5 +1023,16 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
-- This mess is why ShellCheck prefers not to know. -- This mess is why ShellCheck prefers not to know.
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:" parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg"
prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script"
prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg"
checkSourceArgs = CommandCheck (Exactly ".") f
where
f t = whenShell [Sh, Dash] $
case arguments t of
(file:arg1:_) -> warn (getId arg1) 2240 $
"The dot command does not support arguments in sh/dash. Set them as variables."
_ -> return ()
return [] return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -39,20 +39,7 @@ format = do
footer = finish ref footer = finish ref
} }
instance ToJSON Replacement where instance ToJSON (PositionedComment) where
toJSON replacement =
let start = repStartPos replacement
end = repEndPos replacement
str = repString replacement in
object [
"line" .= posLine start,
"endLine" .= posLine end,
"column" .= posColumn start,
"endColumn" .= posColumn end,
"replaceWith" .= str
]
instance ToJSON PositionedComment where
toJSON comment = toJSON comment =
let start = pcStartPos comment let start = pcStartPos comment
end = pcEndPos comment end = pcEndPos comment
@@ -65,8 +52,7 @@ instance ToJSON PositionedComment where
"endColumn" .= posColumn end, "endColumn" .= posColumn end,
"level" .= severityText comment, "level" .= severityText comment,
"code" .= cCode c, "code" .= cCode c,
"message" .= cMessage c, "message" .= cMessage c
"fix" .= pcFix comment
] ]
toEncoding comment = toEncoding comment =
@@ -82,14 +68,8 @@ instance ToJSON PositionedComment where
<> "level" .= severityText comment <> "level" .= severityText comment
<> "code" .= cCode c <> "code" .= cCode c
<> "message" .= cMessage c <> "message" .= cMessage c
<> "fix" .= pcFix comment
) )
instance ToJSON Fix where
toJSON fix = object [
"replacements" .= fixReplacements fix
]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref result _ = collectResult ref result _ =
modifyIORef ref (\x -> crComments result ++ x) modifyIORef ref (\x -> crComments result ++ x)
@@ -97,3 +77,4 @@ collectResult ref result _ =
finish ref = do finish ref = do
list <- readIORef ref list <- readIORef ref
BL.putStrLn $ encode list BL.putStrLn $ encode list

View File

@@ -25,7 +25,6 @@ import ShellCheck.Formatter.Format
import Control.Monad import Control.Monad
import Data.IORef import Data.IORef
import Data.List import Data.List
import Data.Maybe
import GHC.Exts import GHC.Exts
import System.IO import System.IO
import System.Info import System.Info
@@ -95,7 +94,7 @@ outputWiki errRef = do
where where
showErr (_, code, msg) = showErr (_, code, msg) =
putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg putStrLn $ " " ++ wikiLink ++ "SC" ++ show code ++ " -- " ++ shorten msg
limit = 40 limit = 36
shorten msg = shorten msg =
if length msg < limit if length msg < limit
then msg then msg
@@ -119,8 +118,8 @@ outputForFile color sys comments = do
let fileLines = lines contents let fileLines = lines contents
let lineCount = fromIntegral $ length fileLines let lineCount = fromIntegral $ length fileLines
let groups = groupWith lineNo comments let groups = groupWith lineNo comments
mapM_ (\commentsForLine -> do mapM_ (\x -> do
let lineNum = lineNo (head commentsForLine) let lineNum = lineNo (head x)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
else fileLines !! fromIntegral (lineNum - 1) else fileLines !! fromIntegral (lineNum - 1)
@@ -128,62 +127,10 @@ outputForFile color sys comments = do
putStrLn $ color "message" $ putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":" "In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line) putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) x
putStrLn "" putStrLn ""
-- FIXME: Enable when reasonably stable
-- showFixedString color comments lineNum line
) groups ) groups
hasApplicableFix lineNum comment = fromMaybe False $ do
replacements <- fixReplacements <$> pcFix comment
guard $ all (\c -> onSameLine (repStartPos c) && onSameLine (repEndPos c)) replacements
return True
where
onSameLine pos = posLine pos == lineNum
-- FIXME: Work correctly with multiple replacements
showFixedString color comments lineNum line =
case filter (hasApplicableFix lineNum) comments of
(first:_) -> do
-- in the spirit of error prone
putStrLn $ color "message" "Did you mean: "
putStrLn $ fixedString first line
putStrLn ""
_ -> return ()
-- need to do something smart about sorting by end index
fixedString :: PositionedComment -> String -> String
fixedString comment line =
case (pcFix comment) of
Nothing -> ""
Just rs ->
applyReplacement (fixReplacements rs) line 0
where
applyReplacement [] s _ = s
applyReplacement (rep:xs) s offset =
let replacementString = repString rep
start = (posColumn . repStartPos) rep
end = (posColumn . repEndPos) rep
z = doReplace start end s replacementString
len_r = (fromIntegral . length) replacementString in
applyReplacement xs z (offset + (end - start) + len_r)
-- FIXME: Work correctly with tabs
-- start and end comes from pos, which is 1 based
-- doReplace 0 0 "1234" "A" -> "A1234" -- technically not valid
-- doReplace 1 1 "1234" "A" -> "A1234"
-- doReplace 1 2 "1234" "A" -> "A234"
-- doReplace 3 3 "1234" "A" -> "12A34"
-- doReplace 4 4 "1234" "A" -> "123A4"
-- doReplace 5 5 "1234" "A" -> "1234A"
doReplace start end o r =
let si = fromIntegral (start-1)
ei = fromIntegral (end-1)
(x, xs) = splitAt si o
(y, z) = splitAt (ei - si) xs
in
x ++ r ++ z
cuteIndent :: PositionedComment -> String cuteIndent :: PositionedComment -> String
cuteIndent comment = cuteIndent comment =
replicate (fromIntegral $ colNo comment - 1) ' ' ++ replicate (fromIntegral $ colNo comment - 1) ' ' ++

View File

@@ -17,7 +17,6 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module ShellCheck.Interface module ShellCheck.Interface
( (
SystemInterface(..) SystemInterface(..)
@@ -25,7 +24,7 @@ module ShellCheck.Interface
, CheckResult(crFilename, crComments) , CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride) , ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot) , ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced, asTokenPositions) , AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced)
, AnalysisResult(arComments) , AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount) , FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash) , Shell(Ksh, Sh, Bash, Dash)
@@ -35,9 +34,9 @@ module ShellCheck.Interface
, Severity(ErrorC, WarningC, InfoC, StyleC) , Severity(ErrorC, WarningC, InfoC, StyleC)
, Position(posFile, posLine, posColumn) , Position(posFile, posLine, posColumn)
, Comment(cSeverity, cCode, cMessage) , Comment(cSeverity, cCode, cMessage)
, PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix) , PositionedComment(pcStartPos , pcEndPos , pcComment)
, ColorOption(ColorAuto, ColorAlways, ColorNever) , ColorOption(ColorAuto, ColorAlways, ColorNever)
, TokenComment(tcId, tcComment, tcFix) , TokenComment(tcId, tcComment)
, emptyCheckResult , emptyCheckResult
, newParseResult , newParseResult
, newAnalysisSpec , newAnalysisSpec
@@ -50,18 +49,10 @@ module ShellCheck.Interface
, emptyCheckSpec , emptyCheckSpec
, newPositionedComment , newPositionedComment
, newComment , newComment
, Fix(fixReplacements)
, newFix
, Replacement(repStartPos, repEndPos, repString)
, newReplacement
) where ) where
import ShellCheck.AST import ShellCheck.AST
import Control.DeepSeq
import Control.Monad.Identity import Control.Monad.Identity
import Data.Monoid
import GHC.Generics (Generic)
import qualified Data.Map as Map import qualified Data.Map as Map
@@ -135,16 +126,14 @@ data AnalysisSpec = AnalysisSpec {
asScript :: Token, asScript :: Token,
asShellType :: Maybe Shell, asShellType :: Maybe Shell,
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool, asCheckSourced :: Bool
asTokenPositions :: Map.Map Id (Position, Position)
} }
newAnalysisSpec token = AnalysisSpec { newAnalysisSpec token = AnalysisSpec {
asScript = token, asScript = token,
asShellType = Nothing, asShellType = Nothing,
asExecutionMode = Executed, asExecutionMode = Executed,
asCheckSourced = False, asCheckSourced = False
asTokenPositions = Map.empty
} }
newtype AnalysisResult = AnalysisResult { newtype AnalysisResult = AnalysisResult {
@@ -174,13 +163,12 @@ data ExecutionMode = Executed | Sourced deriving (Show, Eq)
type ErrorMessage = String type ErrorMessage = String
type Code = Integer type Code = Integer
data Severity = ErrorC | WarningC | InfoC | StyleC data Severity = ErrorC | WarningC | InfoC | StyleC deriving (Show, Eq, Ord)
deriving (Show, Eq, Ord, Generic, NFData)
data Position = Position { data Position = Position {
posFile :: String, -- Filename posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8 posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq, Generic, NFData) } deriving (Show, Eq)
newPosition :: Position newPosition :: Position
newPosition = Position { newPosition = Position {
@@ -193,7 +181,7 @@ data Comment = Comment {
cSeverity :: Severity, cSeverity :: Severity,
cCode :: Code, cCode :: Code,
cMessage :: String cMessage :: String
} deriving (Show, Eq, Generic, NFData) } deriving (Show, Eq)
newComment :: Comment newComment :: Comment
newComment = Comment { newComment = Comment {
@@ -202,52 +190,27 @@ newComment = Comment {
cMessage = "" cMessage = ""
} }
-- only support single line for now
data Replacement = Replacement {
repStartPos :: Position,
repEndPos :: Position,
repString :: String
} deriving (Show, Eq, Generic, NFData)
newReplacement = Replacement {
repStartPos = newPosition,
repEndPos = newPosition,
repString = ""
}
data Fix = Fix {
fixReplacements :: [Replacement]
} deriving (Show, Eq, Generic, NFData)
newFix = Fix {
fixReplacements = []
}
data PositionedComment = PositionedComment { data PositionedComment = PositionedComment {
pcStartPos :: Position, pcStartPos :: Position,
pcEndPos :: Position, pcEndPos :: Position,
pcComment :: Comment, pcComment :: Comment
pcFix :: Maybe Fix } deriving (Show, Eq)
} deriving (Show, Eq, Generic, NFData)
newPositionedComment :: PositionedComment newPositionedComment :: PositionedComment
newPositionedComment = PositionedComment { newPositionedComment = PositionedComment {
pcStartPos = newPosition, pcStartPos = newPosition,
pcEndPos = newPosition, pcEndPos = newPosition,
pcComment = newComment, pcComment = newComment
pcFix = Nothing
} }
data TokenComment = TokenComment { data TokenComment = TokenComment {
tcId :: Id, tcId :: Id,
tcComment :: Comment, tcComment :: Comment
tcFix :: Maybe Fix } deriving (Show, Eq)
} deriving (Show, Eq, Generic, NFData)
newTokenComment = TokenComment { newTokenComment = TokenComment {
tcId = Id 0, tcId = Id 0,
tcComment = newComment, tcComment = newComment
tcFix = Nothing
} }
data ColorOption = data ColorOption =

View File

@@ -1916,8 +1916,9 @@ readNewlineList =
where where
checkBadBreak = optional $ do checkBadBreak = optional $ do
pos <- getPosition pos <- getPosition
try $ lookAhead (oneOf "|&") -- |, || or && try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or &&
parseProblemAt pos ErrorC 1133 "Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one." parseProblemAt pos ErrorC 1133
"Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one."
readLineBreak = optional readNewlineList readLineBreak = optional readNewlineList
prop_readSeparator1 = isWarning readScript "a &; b" prop_readSeparator1 = isWarning readScript "a &; b"

View File

@@ -16,10 +16,14 @@ and is still highly experimental.
Make sure you're plugged in and have screen/tmux in place, Make sure you're plugged in and have screen/tmux in place,
then re-run with $0 --run to continue. then re-run with $0 --run to continue.
Also note that 'dist' will be deleted.
EOF EOF
exit 0 exit 0
} }
echo "Deleting 'dist'..."
rm -rf dist
log=$(mktemp) || die "Can't create temp file" log=$(mktemp) || die "Can't create temp file"
date >> "$log" || die "Can't write to log" date >> "$log" || die "Can't write to log"
@@ -63,7 +67,8 @@ opensuse:latest zypper install -y cabal-install ghc
ubuntu:18.04 apt-get update && apt-get install -y cabal-install ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:17.10 apt-get update && apt-get install -y cabal-install ubuntu:17.10 apt-get update && apt-get install -y cabal-install
# Misc # Misc Haskell including current and latest Stack build
ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
haskell:latest true haskell:latest true
# Known to currently fail # Known to currently fail

27
test/stacktest Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# This script builds ShellCheck through `stack` using
# various resolvers. It's run via distrotest.
resolvers=(
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
)
die() { echo "$*" >&2; exit 1; }
[ -e "ShellCheck.cabal" ] ||
die "ShellCheck.cabal not in current dir"
[ -e "stack.yaml" ] ||
die "stack.yaml not in current dir"
command -v stack ||
die "stack is missing"
stack setup || die "Failed to setup with default resolver"
stack build --test || die "Failed to build/test with default resolver"
for resolver in "${resolvers[@]}"
do
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
done
echo "Success"