Skip to content

Commit 08ae7ef

Browse files
committed
New IO interface to scan for Gentoo eclass vars
Uses the `portageq` command to scan for repositories, which in turn are scanned for eclasses, which are then scanned for eclass variables. The variables are scanned using a heuristic which looks for "# @ECLASS_VARIABLE: " at the start of each line, which means only properly documented variables will be found. Signed-off-by: hololeap <[email protected]>
1 parent e3d8483 commit 08ae7ef

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

ShellCheck.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ library
9797
ShellCheck.Regex
9898
other-modules:
9999
Paths_ShellCheck
100+
ShellCheck.PortageVariables
100101
ShellCheck.PortageAutoInternalVariables
101102
default-language: Haskell98
102103

src/ShellCheck/PortageVariables.hs

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
2+
module ShellCheck.PortageVariables
3+
( RepoName
4+
, RepoPath
5+
, EclassVar
6+
, Repository(..)
7+
, Eclass(..)
8+
, portageVariables
9+
, scanRepos
10+
) where
11+
12+
import Control.Applicative
13+
import Control.Monad
14+
import Control.Monad.Trans.Class
15+
import Control.Monad.Trans.Maybe
16+
import Data.Map (Map)
17+
import qualified Data.Map as M
18+
import System.Directory (listDirectory)
19+
import System.Exit (ExitCode(..))
20+
import System.FilePath
21+
import System.Process -- (readProcessWithExitCode)
22+
import Text.Parsec hiding ((<|>))
23+
import Text.Parsec.String
24+
25+
type RepoName = String
26+
type RepoPath = FilePath
27+
type EclassVar = String
28+
29+
data Repository = Repository
30+
{ repositoryName :: RepoName
31+
, repositoryLocation :: RepoPath
32+
, repositoryEclasses :: [Eclass]
33+
} deriving (Show, Eq, Ord)
34+
35+
data Eclass = Eclass
36+
{ eclassName :: String
37+
, eclassVars :: [EclassVar]
38+
} deriving (Show, Eq, Ord)
39+
40+
portageVariables :: [Repository] -> Map String [EclassVar]
41+
portageVariables = foldMap $ foldMap go . repositoryEclasses
42+
where
43+
go e = M.singleton (eclassName e) (eclassVars e)
44+
45+
-- | Run @portageq@ to gather a list of repo names and paths, then scan each
46+
-- one for eclasses and ultimately eclass metadata.
47+
scanRepos :: IO [Repository]
48+
scanRepos = do
49+
let cmd = "/usr/bin/portageq"
50+
let args = ["repos_config", "/"]
51+
out <- runOrDie cmd args
52+
case parse reposParser "scanRepos" out of
53+
Left pe -> fail $ show pe
54+
Right nps -> do
55+
forM nps $ \(n,p) -> Repository n p <$> getEclasses p
56+
57+
-- | Get the name of the repo and its path from blocks outputted by
58+
-- @portageq@. If the path doesn't exist, this will return @Nothing@.
59+
reposParser :: Parser [(RepoName, RepoPath)]
60+
reposParser =
61+
choice
62+
[ [] <$ eof
63+
, repoName >>= repoBlock
64+
]
65+
where
66+
-- Get the name of the repo at the top of the block
67+
repoName :: Parser RepoName
68+
repoName
69+
= char '['
70+
*> manyTill anyChar (try (char ']'))
71+
<* endOfLine
72+
73+
-- Parse the block for location field
74+
repoBlock :: RepoName -> Parser [(RepoName, RepoPath)]
75+
repoBlock n = choice
76+
[ try $ do
77+
l <- string "location = " *> takeLine
78+
-- Found the location, skip the rest of the block
79+
skipMany miscLine *> endOfBlock
80+
insert (n,l)
81+
-- Did not find the location, keep trying
82+
, try $ miscLine *> repoBlock n
83+
-- Reached the end of the block, no location field
84+
, endOfBlock *> ignore
85+
]
86+
87+
miscLine :: Parser ()
88+
miscLine = skipNonEmptyLine
89+
90+
-- A block ends with an eol or eof
91+
endOfBlock :: Parser ()
92+
endOfBlock = void endOfLine <|> eof
93+
94+
insert :: (RepoName, RepoPath) -> Parser [(RepoName, RepoPath)]
95+
insert r = (r:) <$> reposParser
96+
97+
ignore :: Parser [(RepoName, RepoPath)]
98+
ignore = reposParser
99+
100+
-- | Scan the repo path for @*.eclass@ files in @eclass/@, then run
101+
-- 'eclassParser' on each of them to produce @[Eclass]@.
102+
--
103+
-- If the @eclass/@ directory doesn't exist, the scan is skipped for that
104+
-- repo.
105+
getEclasses :: RepoPath -> IO [Eclass]
106+
getEclasses repoLoc = fmap (maybe [] id) $ runMaybeT $ do
107+
let eclassDir = repoLoc </> "eclass"
108+
109+
-- Silently fail if the repo doesn't have an eclass dir
110+
fs <- MaybeT $ Just <$> listDirectory eclassDir <|> pure Nothing
111+
let fs' = filter (\(_,e) -> e == ".eclass") $ map splitExtensions fs
112+
113+
forM fs' $ \(n,e) -> do
114+
evs <- lift $ parseFromFile eclassParser (eclassDir </> n <.> e)
115+
case evs of
116+
Left pe -> lift $ fail $ show pe
117+
Right vs -> pure $ Eclass n vs
118+
119+
eclassParser :: Parser [EclassVar]
120+
eclassParser = choice
121+
[ -- cons the EclassVar to the list and continue
122+
try $ liftA2 (:) eclassVar eclassParser
123+
-- or skip the line and continue
124+
, skipLine *> eclassParser
125+
-- or end the list on eof
126+
, [] <$ eof
127+
]
128+
where
129+
-- Scans for @ECLASS_VARIABLE comments rather than parsing the raw bash
130+
eclassVar :: Parser EclassVar
131+
eclassVar = string "# @ECLASS_VARIABLE: " *> takeLine
132+
133+
takeLine :: Parser String
134+
takeLine = manyTill anyChar (try endOfLine)
135+
136+
-- | Fails if next char is 'endOfLine'
137+
skipNonEmptyLine :: Parser ()
138+
skipNonEmptyLine = notFollowedBy endOfLine *> skipLine
139+
140+
skipLine :: Parser ()
141+
skipLine = void takeLine
142+
143+
-- | Run the command and return the full stdout string (stdin is ignored).
144+
--
145+
-- If the command exits with a non-zero exit code, this will throw an
146+
-- error including the captured contents of stdout and stderr.
147+
runOrDie :: FilePath -> [String] -> IO String
148+
runOrDie cmd args = do
149+
(ec, o, e) <- readProcessWithExitCode cmd args ""
150+
case ec of
151+
ExitSuccess -> pure o
152+
ExitFailure i -> fail $ unlines $ map unwords
153+
$ [ [ show cmd ]
154+
++ map show args
155+
++ [ "failed with exit code", show i]
156+
, [ "stdout:" ], [ o ]
157+
, [ "stderr:" ], [ e ]
158+
]

0 commit comments

Comments
 (0)