mcm-0.6.8.1: mcmtags.hs
-- MCM - Machine Configuration Manager; manages the contents of files and directories
-- Copyright (c) 2013-2018 Anthony Doggett <mcm@interfaces.org.uk>
--
-- Licence:
-- This program 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.
--
-- This program 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/>.
-- Parses all .mcm files below the current directory
-- (If directories given as arguments, uses those instead)
-- Writes the results to "tags" file (in current directory)
-- Prints warnings for any files that fail to parse
module Main (main)
where
import Parser (mcmParse)
import ParserTypes (Define(..), Section(..), MCMFile(..), DefName(..))
import Paths_mcm (version)
import Control.Monad (filterM, unless)
import Data.Char (isAsciiUpper)
import Data.List (isSuffixOf, intercalate, sort, foldl', nub)
import Data.Version (showVersion)
import qualified Data.Map as Map
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.IO as TextIO
import System.Console.GetOpt
import System.Directory (getDirectoryContents)
import System.Environment (getArgs)
import System.Exit (exitSuccess)
import System.FilePath (joinPath)
import System.Posix.Files (getFileStatus, isDirectory)
usage :: String
usage = unlines ["Usage: mcmtags [DIR..]"
,"Parse all *.mcm files below the current directory,"
,"or if directories given as arguments, below those directories."
,"The resulting tags are written to ./tags."
,"Print warnings for any files that fail to parse."
,""
,"TAGTYPE\t\tsimple|qualified|both"
]
toWarn :: FilePath -> String
toWarn f = "WARNING: failed to parse " ++ f
tryParse :: FilePath -> IO (Maybe MCMFile)
tryParse f = do
ss <- TextIO.readFile f
case mcmParse ss of
Right a -> return $ Just a
Left _ -> return Nothing
makeTag :: FilePath -> String -> String -> String
makeTag f n ex_cmd = n ++ "\t" ++ f ++ "\t" ++ ex_cmd
toTags :: TagType -> (FilePath, MCMFile) -> [String]
toTags tagtype (f, MCMFile pp (Section _ _ ds)) = makePTags ++ concatMap makeDTags (Map.elems ds)
where
makePTags = case tagtype of
TTsimple -> [ppTag]
TTqualified -> [ppFullTag]
TTboth -> nub [ppTag, ppFullTag]
ppTag = makeTag f (lastBitOfPath (show pp)) "/^MCM/"
ppFullTag = makeTag f (show pp) "/^MCM/"
makeDTags :: Define -> [String]
makeDTags d = case tagtype of
TTsimple -> [dsTag d]
TTqualified -> [qualTag d]
TTboth -> [dsTag d, qualTag d]
dsTag :: Define -> String
dsTag d = makeTag f (strDefName d) (definesearch d)
qualTag :: Define -> String
qualTag d = makeTag f (show pp ++ '.':strDefName d) (definesearch d)
definesearch :: Define -> String
definesearch d = "/^define " ++ strDefName d ++ "(/"
lastBitOfPath :: String -> String
lastBitOfPath = reverse . takeWhile (/= '.') . reverse
strDefName = T.unpack . fromDefName . defName
isDirectory' :: FilePath -> IO Bool
isDirectory' f = do
s <- getFileStatus f
return $ isDirectory s
findMcm :: FilePath -> IO [FilePath]
findMcm p = do
allfiles <- getDirectoryContents p
let fs = [f | f <- allfiles, f `notElem` [".", ".."]]
ms = [joinPath [p,f] | f <- fs, ".mcm" `isSuffixOf` f]
ds = [joinPath [p,f] | f <- fs, isAsciiUpper (head f), '.' `notElem` f]
dirs <- filterM isDirectory' ds
children <- mapM findMcm dirs
return $ ms ++ concat children
splitParsed :: [(FilePath, Maybe MCMFile)] -> ([FilePath], [(FilePath, MCMFile)])
splitParsed [] = ([], [])
splitParsed ((fp, m):xs) =
let (as, bs) = splitParsed xs
in case m of
Nothing -> (fp:as, bs)
Just f -> (as, (fp, f):bs)
main :: IO ()
main = do
args <- getArgs
let (actions, nonOpts, msgs) = getOpt Permute options args
unless (null msgs) $ error $ concat msgs ++ usageInfo usage options
opts <- foldl' (>>=) (return defaultOptions) actions
let Options {optTagType = tagtype} = opts
let toRecurse = case nonOpts of
[] -> ["."]
ps -> ps
toParse <- mapM findMcm toRecurse
let toParse' = concat toParse
parsed <- mapM tryParse toParse'
let fparsed = zip toParse' parsed
let (bad, good) = splitParsed fparsed
tags = sort $ concatMap (toTags tagtype) good
mapM_ (putStrLn . toWarn) bad
writeFile "tags" $ intercalate "\n" tags
return ()
data TagType = TTsimple | TTqualified | TTboth
data Options = Options {optTagType :: TagType}
defaultOptions :: Options
defaultOptions = Options {optTagType = TTboth}
options :: [OptDescr (Options -> IO Options)]
options = [Option "V" ["version"] (NoArg displayVersion) "show version and exit"
,Option "h" ["help"] (NoArg justHelp) "show this help and exit"
,Option "t" ["tag-type"] (ReqArg tagType "TAGTYPE") "type of tag to output (default = \"both\")"
]
displayVersion :: Options -> IO Options
displayVersion _ = do
putStrLn $ "mcmtags " ++ showVersion version
exitSuccess
justHelp :: Options -> IO Options
justHelp _ = do
putStrLn $ usageInfo usage options
exitSuccess
tagType :: String -> Options -> IO Options
tagType "simple" opt = return opt {optTagType = TTsimple}
tagType "qualified" opt = return opt {optTagType = TTqualified}
tagType "both" opt = return opt {optTagType = TTboth}
tagType tt _ = error ("Unrecognised tag type: " ++ tt ++ "\n" ++ usageInfo usage options)