packages feed

hunp-0.1: Hunp.hs

{-# LANGUAGE CPP #-}

-- Copyright (c) 2009 Deniz Dogan

module Main where

import Control.Applicative
import Control.Arrow
import Control.Monad.Reader
import Data.List
import Data.Maybe
import HunpParser
import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.FilePath.Posix
import System.IO
import System.Process
import Text.ParserCombinators.Parsec
import Text.Regex.PCRE.Light
import Text.Regex.PCRE.Light.Base
import qualified Data.ByteString.Char8 as B

#include "VERSION"

data HunpConf = HC { flags :: [Flag]
                   , targets :: [String]
                   , rules :: [Rule Regex]
                   }

type Hunp a = ReaderT HunpConf IO a

type Rule a = ([a], String)

data Flag = Quiet | STFU | Default | Version
            deriving Eq

-- | The different options available.
options :: [OptDescr Flag]
options = [ Option "q" ["quiet"]   (NoArg Quiet)   "suppress stdout"
          , Option "Q" ["stfu"]    (NoArg STFU)    "suppress stdout and stderr"
          , Option "d" ["default"] (NoArg Default) "prints the default config"
          , Option "v" ["version"] (NoArg Version) "prints the version of hunp"
          ]

-- | A string of usage information.
usage :: String
usage = let ls = ["Usage: hunp [options] [files/directories]", ""
                 , "Options:"]
        in unlines ls

-- | Reads the configuration file.  Tries to use the user's custom
--   configuration file.  If that fails, uses the default one.
readConf :: IO [Rule Regex]
readConf = do
  file <- (</> "hunp.conf") <$> getAppUserDataDirectory "hunp"
  exists <- doesFileExist file
  if not exists
    then return defaultConf
    else do p <- readable <$> getPermissions file
            if not p
              then do putErrLn "Cannot read configuration file. Using default configuration."
                      return defaultConf
              else do res <- parseFromFile parser file
                      case res of
                        Left e  -> do putErrLn $ "Configuration file parse failed: " ++ show e
                                      return defaultConf
                        Right x -> return $ map compileRegexen x

-- | Prints the given configuration to stdout.
printConfig :: [Rule Regex] -> IO ()
printConfig []           = return ()
printConfig ((r,cmd):rs) = do
  let parts = map (\(Regex _ x) -> B.unpack x) r
  putStrLn $ (concat $ intersperse "," parts) ++ "\t" ++ cmd
  printConfig rs

main :: IO ()
main = do
  args <- getArgs
  let (fl, tgts, errs) = getOpt Permute options args
  if Default `elem` fl
    then printConfig defaultConf
    else if Version `elem` fl
           then putStrLn hunpVersion
           else if null tgts || not (null errs)
                   then putStr $ usageInfo usage options
                   else do conf <- readConf
                           let c = HC fl tgts conf
                           runReaderT (mapM_ hunp tgts) c

-- | Given a FilePath, determines whether it is pointing to a file or
--   a directory and takes the appropriate action.
hunp :: FilePath -> Hunp ()
hunp fp = do
  isFile <- io $ doesFileExist fp
  isDir <- io $ doesDirectoryExist fp
  when isFile (hunpFile fp)
  when isDir (hunpDir fp)
  unless (isFile || isDir) (talkE $ "Could not find `" ++ fp ++ "'. Skipping.")

-- | Given a filename, tries to find a matching regular expression for
--   it.  If one is found, unpacks it using the corresponding rule.
hunpFile :: FilePath -> Hunp ()
hunpFile fp = do
  conf <- asks rules
  case lookupWith (any (\x -> match' x (B.pack fp) [])) conf of
    Nothing -> talkE $ "Unknown file: `" ++ takeFileName fp ++ "'. Skipping."
    Just cmd -> do
      talk $ "Unpacking `" ++ fp ++ "'..."
      unpack (map (replaceIt fp) $ words cmd) >>=
       \x -> case x of
               ExitSuccess -> talk $ "Successfully unpacked `" ++ takeFileName fp ++ "'."
               _ -> talkE $ "Something went wrong while unpacking `" ++ takeFileName fp ++ "'!"

-- | Hackish way of performing printf substitution.  This was needed
--   for some silly reason that I can't be bothered remembering.
replaceIt :: String -> String -> String
replaceIt fp "%s" = fp
replaceIt _  x    = x

-- | Given a directory path, looks for the first file in the directory
--   matching any of the file regexp rules.  If it finds any file
--   matching any rule, it stops and unpacks the first found file.  It
--   will never unpack several files in a directory.
hunpDir :: FilePath -> Hunp ()
hunpDir fp = do
  cont <- io $ getDirectoryContents fp
  conf <- asks rules
  case find (\f -> isJust $ lookupWith (any (\x -> match' x (B.pack f) [])) conf) cont of
    Just x -> hunpFile (fp </> x)
    Nothing -> talkE $ "Found nothing to unpack in " ++ fp

-- | Exactly like 'Text.Regex.PCRE.Light', but returns @True@ if there
--   was a match, otherwise @False@.
match' :: Regex -> B.ByteString -> [PCREExecOption] -> Bool
match' a b = isJust . match a b

-- | Generalization of 'lookup'.
lookupWith :: (a -> Bool) -> [(a, b)] -> Maybe b
lookupWith _ []          = Nothing
lookupWith p ((x, y):xs) | p x = Just y
                         | otherwise = lookupWith p xs

unpack :: [String] -> Hunp ExitCode
unpack (cmd:args) = io $ rawSystem cmd args
unpack _          = error "Error in configuration file."

-- | Prints an error to @stderr@.
putErrLn :: String -> IO ()
putErrLn = hPutStrLn stderr

defaultConf :: [Rule Regex]
defaultConf =
  let ls = [ (["\\.rar$", "\\.r00$"],      "unrar x %s")
           , (["\\.tar\\.gz$", "\\.tgz$"], "tar -zxvf %s")
           , (["\\.tar\\.bz2$"],           "tar -jxvf %s")
           , (["\\.bz2$"],                 "bunzip2 %s")
           , (["\\.zip$"],                 "unzip %s")
           , (["\\.arj$"],                 "unarj x %s")
           , (["\\.7z$"],                  "7z x %s")
           , (["\\.ace$"],                 "unace x %s")
           ]
  in map compileRegexen ls

compileRegexen :: Rule String -> Rule Regex
compileRegexen = first $ map (\x -> compile (B.pack x) [])

talk :: String -> Hunp ()
talk s = do
  fl <- asks flags
  unless (STFU `elem` fl || Quiet `elem` fl)
    (io $ putStrLn s)

talkE :: String -> Hunp ()
talkE s = do
  fl <- asks flags
  unless (STFU `elem` fl)
    (io $ hPutStrLn stderr s)

io :: MonadIO m => IO a -> m a
io = liftIO