---------------------------------------------------------------------- -- | -- Module : Main -- Copyright : (c) 2010 Daniel Fischer -- Licence : MIT -- -- Maintainer : Daniel Fischer -- Stability : experimental -- Portability : portable -- -- The executable to convert single files or entire directory trees -- between camel case and separated words style. ---------------------------------------------------------------------- module Main (main) where import System.Environment (getArgs) import System.Console.GetOpt import System.Directory import System.FilePath import qualified System.IO.UTF8 as S import Data.Char (isAlphaNum) import Data.Version import Data.List import Control.Monad (when) import Data.Transform.Camel import Data.Transform.UnCamel import Data.Transform.Separators main :: IO () main = do args <- getArgs case getWork args of Left str -> putStrLn str Right cf -> do let proc = worker cf shibboleth = case fileType cf of HTML -> (== ".html") . takeExtension HS -> (== ".hs") . takeExtension case input cf of Just (Left fn) -> let outfile = case output cf of Left ou -> ou Right d -> joinPath [d,fn] in do createDirectoryIfMissing True (takeDirectory outfile) transform proc fn outfile Just (Right dir) -> let look = case recursive cf of Just b -> b Nothing -> error "Insanity!!" odir = case output cf of Right d -> d in labour (transform proc) shibboleth look dir odir labour :: (FilePath -> FilePath -> IO ()) -> (FilePath -> Bool) -> Bool -> FilePath -> FilePath -> IO () labour tf test more ind odir = do isd <- doesDirectoryExist ind when isd $ do createDirectoryIfMissing True odir conts <- getDirectoryContents ind (dirs,files) <- partitionDirs (ind ) $ filter ((/= '.') . head) conts let rdirs | more = dirs | otherwise = [] (wrk,cpy) = partition test files mapM_ (\fn -> tf (ind fn) (odir fn)) wrk mapM_ (\fn -> copyFile (ind fn) (odir fn)) cpy mapM_ (\dr -> labour tf test more (ind dr) (odir dr)) rdirs transform :: (String -> String) -> FilePath -> FilePath -> IO () transform process infile outfile = S.readFile infile >>= S.writeFile outfile . process partitionDirs :: (FilePath -> FilePath) -> [FilePath] -> IO ([FilePath],[FilePath]) partitionDirs jn = partDirs jn [] [] partDirs :: (FilePath -> FilePath) -> [FilePath] -> [FilePath] -> [FilePath] -> IO ([FilePath],[FilePath]) partDirs _ dirs files [] = return (dirs,files) partDirs jn dirs files (fp : fps) = do isDir <- doesDirectoryExist (jn fp) if isDir then partDirs jn (fp:dirs) files fps else partDirs jn dirs (fp:files) fps myVersion :: Version myVersion = Version{ versionBranch = [0,1,0], versionTags = [] } worker :: Config -> String -> String worker cf = f s where s = case separator cf of Hyphen -> hyphen Under -> lowLine DoubleLow -> doubleLowLine WideLow -> wideLowLine Other c -> c f = case fileType cf of HTML -> unCamelHTML HS -> case target cf of Camel -> camelSource Sep -> unCamelSource data Config = Conf { info :: Bool , version :: Bool , fileType :: FileType , target :: Target , separator :: Separator , recursive :: Maybe Bool , input :: Maybe (Either FilePath Directory) , output :: Either FilePath Directory } defaultConf :: Config defaultConf = Conf { info = False , version = False , fileType = HS , target = Sep , separator = Hyphen , recursive = Just True , input = Nothing , output = Right "Restyled" } data FileType = HTML | HS data Target = Camel | Sep data Separator = Hyphen | Under | DoubleLow | WideLow | Other Char type Directory = FilePath sanitiseConfig :: Config -> Config sanitiseConfig cf | insane cf = cf{ info = True } | otherwise = cf insane :: Config -> Bool insane cf | info cf || version cf = False insane (Conf{ fileType = HTML, target = Camel }) = True insane (Conf{ input = Nothing }) = True insane (Conf{ input = Just (Left _), recursive = Just _ }) = True insane (Conf{ fileType = HTML, input = Just (Left fn) }) | takeExtension fn /= ".html" = True insane (Conf{ fileType = HS, input = Just (Left fn) }) | takeExtension fn /= ".hs" = True insane cf@(Conf{ input = Just (Right _) }) = case output cf of Left _ -> True _ -> case recursive cf of Nothing -> True _ -> False insane _ = False options :: [OptDescr (Config -> Config)] options = [ Option ['?','h'] ["help","usage","info"] (NoArg (\cf -> cf{ info = True })) "Print this message." , Option ['V'] ["version"] (NoArg (\cf -> cf{ version = True })) "Print version number and exit." , Option ['H'] ["HTML","html"] (NoArg (\cf -> cf{ fileType = HTML, target = Sep })) "UnCamel html file[s]" {- , Option ['S'] ["source","hs","haskell"] (NoArg (\cf -> cf{ fileType = HS })) "Process Haskell source code (default)." , Option ['t'] ["target"] (ReqArg parseT "c[amel]|u[ncamel]|s[eparate_words]") "Target of transformation. Default is separate_words. Target camel doesn't work with HTML files." -} , Option ['c'] ["camel"] (NoArg (\cf -> cf{ fileType = HS, target = Camel })) "Transform source to camel case. Inconsistent with --html." , Option ['s'] ["sep","separator"] (ReqArg parseS "SEP") "Separation character to insert or remove (default is Unicode hyphen [U+2010], must not be alphanumeric)." {- , Option ['h'] ["hyphen"] (NoArg (\cf -> cf{ separator = Hyphen })) "Hyphen as separation character (default)." -} , Option ['u'] ["underscore"] (NoArg (\cf -> cf{ separator = Under })) "Underscore as separation character." , Option ['d'] ["double"] (NoArg (\cf -> cf{ separator = DoubleLow })) "Double low line as separation character." , Option ['w'] ["wide"] (NoArg (\cf -> cf{ separator = WideLow })) "Wide low line as separation character." {- , Option ['r'] ["rec"] (NoArg (\cf -> cf{ recursive = Just True })) "Transform directory contents recursively (default)." -} , Option ['n'] ["nonrec"] (NoArg (\cf -> cf{ recursive = Just False })) "Ignore subdirectories and only treat files in INDIR. Default is recursively processing subdirectories." , Option ['f'] ["file"] (ReqArg parseF "INFILE") "Transform only specified file, which must have a .hs or .html extension." , Option ['i','D'] ["indir","dir"] (ReqArg (\dr cf -> cf{ input = Just (Right dr) }) "INDIR") "Directory whose contents is to be transformed. Either this option or the file option is mandatory." , Option ['o'] ["out","odir"] (ReqArg (\dr cf -> cf{ output = Right dr }) "OUTDIR") "Directory in which to write processed files. This better be not INDIR and in case of recursive processing neither a subdirectory thereof. Default is 'Restyled'." , Option ['t'] ["ofile","outfile"] (ReqArg (\fn cf -> cf{ output = Left fn }) "OUTFILE") "Name of file to write (only if a single file is processed). Must be different from INFILE. Inconsistent with --odir." ] parseT :: String -> Config -> Config parseT str cf = case str of ('c':_) -> cf{ fileType = HS, target = Camel } ('u':_) -> cf{ target = Sep } ('s':_) -> cf{ target = Sep } _ -> cf{ info = True } parseS :: String -> Config -> Config parseS str cf = case str of (h:_) | not (isAlphaNum h) -> cf{ separator = Other h } _ -> cf{ info = True } parseF :: String -> Config -> Config parseF str cf = case takeExtension str of ext | ext == ".hs" -> cf{ fileType = HS, recursive = Nothing, input = Just (Left str) } | ext == ".html" -> cf{ fileType = HTML, target = Sep, recursive = Nothing, input = Just (Left str) } _ -> cf{ info = True } generalUsage :: String generalUsage = unlines [ "Usage: restyle OPTIONS" , "At least one of the options --help, --version, --indir=INDIR, --file=INFILE or their equivalents must be given." , "Unless otherwise specified, restyle converts Haskell source files from camel case to separated words." , "If an entire directory (hierarchy) shall be converted, files of other types are copied to the target location to create a working source or documentation tree with minimal effort." , "Conflicting options may be resolved in an arbitrary manner. If the order of the conflicting options doesn't lead to an automatic resolution, this message is displayed." ] getWork :: [String] -> Either String Config getWork args = case getOpt RequireOrder options args of (o,n,e) | null n && null e -> case foldl (flip id) defaultConf o of cf | info cf -> Left usage | version cf -> Left (showVersion myVersion) | otherwise -> Right cf | otherwise -> Left (concat e ++ unlines n ++ usage) where usage = usageInfo generalUsage options