Skip to content

Commit 394f8c8

Browse files
Add initial cut of determining whether a file is a shell file.
I'm hoping the algorithm in the code itself should be quite self-explanatory but here it is anyway. - If the file has a .sh extension then it is a shell file. - If the file has no extension and a whitelisted interpretter, then it is a shell file. - Else it is not a shell file.
1 parent c29f881 commit 394f8c8

File tree

4 files changed

+96
-3
lines changed

4 files changed

+96
-3
lines changed

codeclimate-shellcheck.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ executable engine
2626
CC.Types
2727
build-depends:
2828
aeson >= 0.9.0.1 && <= 0.10.0.0
29+
, attoparsec >= 0.13.0.0 && <= 0.14.0.0
2930
, base >= 4.7 && <= 5
3031
, containers >= 0.5.6.0 && <= 0.5.7.0
3132
, bytestring >= 0.10.4.0 && <= 0.11.0.0
3233
, directory >= 1.2.1.0 && <= 1.3.0.0
34+
, filepath >= 1.3.0.0 && <= 1.4.0.0
3335
, Glob >= 0.7.5 && <= 0.8.0
3436
, ShellCheck >= 0.4.1 && <= 0.5.0
3537
, text >= 1.1.0.0 && <= 1.2.1.3

src/CC/Types.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module CC.Types where
77
import Control.Applicative
88
import Data.Aeson
99
import Data.Aeson.Types
10+
import qualified Data.ByteString as BS
1011
import qualified Data.Map.Strict as DM
1112
import qualified Data.Text as T
1213
import GHC.Generics
@@ -148,3 +149,8 @@ instance FromJSON Mapping where
148149
-- | An env represents mappings between check names, content and remediation
149150
-- values.
150151
type Env = DM.Map T.Text Mapping
152+
153+
--------------------------------------------------------------------------------
154+
155+
-- | Represents a Linux shebang, i.e. #!interpreter [optional-arg].
156+
data Shebang = Shebang BS.ByteString BS.ByteString

src/Main.hs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,25 @@ module Main where
55
import CC.Analyze
66
import CC.Types
77
import Data.Aeson
8+
import Data.Attoparsec.ByteString.Lazy
9+
import qualified Data.ByteString as BS
810
import qualified Data.ByteString.Lazy as BSL
911
import qualified Data.Map.Strict as DM
1012
import Data.Maybe
1113
import Data.Monoid
14+
import Data.Word
1215
import qualified Data.Yaml as YML
1316
import System.Directory
1417
import System.FilePath.Glob
18+
import System.FilePath.Posix
1519

1620
--------------------------------------------------------------------------------
1721

1822
main :: IO ()
1923
main = do
2024
config <- loadConfig "/config.json"
2125
env <- fromMaybe DM.empty <$> YML.decodeFile "data/mapping.yml"
22-
paths <- shFiles $! _include_paths config
26+
paths <- shellScripts $! _include_paths config
2327
issues <- analyzeFiles env paths
2428
mapM_ printIssue issues
2529

@@ -38,9 +42,83 @@ printIssue = BSL.putStr . (<> "\0") . encode
3842

3943
--------------------------------------------------------------------------------
4044

41-
shFiles :: [FilePath] -> IO [FilePath]
42-
shFiles paths =
45+
shellScripts :: [FilePath] -> IO [FilePath]
46+
shellScripts paths =
4347
fmap concat $! sequence $! fmap (matched . globDir [compile "**/*.sh"]) paths
4448
where
4549
matched :: Functor f => f ([[a]], [b]) -> f [a]
4650
matched x = (concat . fst) <$> x
51+
52+
--------------------------------------------------------------------------------
53+
54+
-- | Determines whether a file is a shell script that we can work with.
55+
isShellScript :: FilePath -> IO Bool
56+
isShellScript path =
57+
if hasExtension path
58+
then return hasShellExtension
59+
else do
60+
header <- readHeader
61+
if hasShebang header
62+
then case readShebang header of
63+
Just (Shebang x y) -> return $ hasValidInterpretter x y
64+
Nothing -> return False
65+
else return False
66+
where
67+
----------------------------------------------------------------------------
68+
69+
carriageReturn :: Word8 -> Bool
70+
carriageReturn = (== 13)
71+
72+
endOfLine :: Word8 -> Bool
73+
endOfLine x = newline x || carriageReturn x
74+
75+
newline :: Word8 -> Bool
76+
newline = (== 10)
77+
78+
whitespace :: Word8 -> Bool
79+
whitespace = (== 32)
80+
81+
----------------------------------------------------------------------------
82+
83+
whiteList :: [BS.ByteString]
84+
whiteList = [ "sh"
85+
, "ash"
86+
, "dash"
87+
, "bash"
88+
, "ksh"
89+
]
90+
91+
----------------------------------------------------------------------------
92+
93+
hasShebang :: BSL.ByteString -> Bool
94+
hasShebang x = BSL.take 2 x == "#!"
95+
96+
hasShellExtension :: Bool
97+
hasShellExtension = takeExtension path == ".sh"
98+
99+
hasValidInterpretter :: BS.ByteString -> BS.ByteString -> Bool
100+
hasValidInterpretter interpretter arguments =
101+
if BS.isSuffixOf "env" interpretter
102+
then any (`BS.isPrefixOf` arguments) whiteList
103+
else any (`BS.isSuffixOf` interpretter) whiteList
104+
105+
----------------------------------------------------------------------------
106+
107+
readHeader :: IO BSL.ByteString
108+
readHeader = do
109+
contents <- BSL.readFile path
110+
return $ BSL.takeWhile (not . endOfLine) contents
111+
112+
readShebang :: BSL.ByteString -> Maybe Shebang
113+
readShebang x = maybeResult $ parse shebang x
114+
115+
----------------------------------------------------------------------------
116+
117+
shebang :: Parser Shebang
118+
shebang = do
119+
_ <- string "#!"
120+
interpretter <- takeTill whitespace
121+
arguments <- option "" $ do
122+
_ <- string " "
123+
takeTill endOfLine
124+
return $ Shebang interpretter arguments

test/example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
## Example of a broken script. Hit the Down Arrow button to ShellCheck it!
3+
for f in $(ls *.m3u)
4+
do
5+
grep -qi hq.*mp3 $f \
6+
&& echo -e 'Playlist $f contains a HQ file in mp3 format'
7+
done

0 commit comments

Comments
 (0)