packages feed

cabal-gild-0.2.1.1: source/library/CabalGild/Main.hs

-- |
-- License: BSD-3-Clause
-- Copyright: Oleg Grenrus
module CabalGild.Main (main) where

import CabalGild (cabalGild)
import CabalGild.Error (Error (SomeError), renderError)
import CabalGild.Monad (runCabalGildIO)
import CabalGild.Options
import qualified Control.Monad as Monad
import qualified Control.Monad.Catch as Exception
import qualified Data.ByteString as BS
import qualified Data.Maybe as Maybe
import Data.Traversable (for)
import qualified Data.Version as Version
import Distribution.Utils.Generic (toUTF8BS)
import qualified Paths_cabal_gild as This
import qualified System.Console.GetOpt as GetOpt
import qualified System.Environment as Environment
import System.Exit (exitFailure)
import qualified System.Exit as Exit
import System.FilePath (takeDirectory)
import System.IO (hPutStrLn, stderr)
import qualified Text.Read as Read

main :: IO ()
main = do
  arguments <- Environment.getArgs
  let (flags, filepaths, unknowns, invalids) = GetOpt.getOpt' GetOpt.Permute flagOptions arguments
  mapM_ (Exception.throwM . userError . mappend "unknown option: ") unknowns
  mapM_ (Exception.throwM . userError . mappend "invalid option: ") invalids
  config <- flagsToConfig flags
  let opts = configOptions config

  Monad.when (configHelp config) $ do
    name <- Environment.getProgName
    putStr $ GetOpt.usageInfo name flagOptions
    Exit.exitSuccess

  Monad.when (configVersion config) $ do
    putStrLn $ Version.showVersion This.version
    Exit.exitSuccess

  notFormatted <-
    Maybe.catMaybes <$> case filepaths of
      [] -> fmap pure $ BS.getContents >>= main' opts Nothing
      (_ : _) -> for filepaths $ \filepath -> do
        contents <- BS.readFile filepath
        main' opts (Just filepath) contents

  Monad.when ((optMode opts == ModeCheck) && not (null notFormatted)) $ do
    Monad.forM_ notFormatted $ \filepath ->
      hPutStrLn stderr $ "error: Input " <> filepath <> " is not formatted."
    exitFailure

main' :: Options -> Maybe FilePath -> BS.ByteString -> IO (Maybe FilePath)
main' opts mfilepath input = do
  -- name of the input
  let filepath = Maybe.fromMaybe "<stdin>" mfilepath

  mroot <-
    fmap takeDirectory <$> case (mfilepath, optStdinInputFile opts) of
      (Just _, Just _) -> do
        renderError $ SomeError "cannot pass both --stdin-input-file and FILE"
        exitFailure
      (Just f, Nothing) -> pure $ Just f
      (Nothing, Just f) -> pure $ Just f
      (Nothing, Nothing) -> pure Nothing

  -- process
  res <- runCabalGildIO mroot opts (cabalGild filepath input)

  case res of
    Right output -> do
      let outputBS = toUTF8BS output
          formatted = outputBS == input

      case optMode opts of
        ModeStdout -> BS.putStr outputBS
        ModeInplace -> case mfilepath of
          Nothing -> BS.putStr outputBS
          Just _ -> Monad.unless formatted $ BS.writeFile filepath outputBS
        _ -> return ()

      return $ if formatted then Nothing else Just filepath
    Left err -> do
      renderError err
      exitFailure

-------------------------------------------------------------------------------
-- Options parser
-------------------------------------------------------------------------------

data Flag
  = FlagCabalFile Bool
  | FlagError Bool
  | FlagHelp
  | FlagIndent String
  | FlagMode Mode
  | FlagStdinInputFile String
  | FlagTabular Bool
  | FlagVersion
  deriving (Eq, Show)

flagOptions :: [GetOpt.OptDescr Flag]
flagOptions =
  [ GetOpt.Option [] ["Werror"] (GetOpt.NoArg $ FlagError True) "Treat warnings as errors",
    GetOpt.Option [] ["Wno-error"] (GetOpt.NoArg $ FlagError False) "",
    GetOpt.Option [] ["indent"] (GetOpt.ReqArg FlagIndent "N") "Indentation",
    GetOpt.Option [] ["tabular"] (GetOpt.NoArg $ FlagTabular True) "Tabular formatting",
    GetOpt.Option [] ["no-tabular"] (GetOpt.NoArg $ FlagTabular False) "",
    GetOpt.Option [] ["cabal-file"] (GetOpt.NoArg $ FlagCabalFile True) "",
    GetOpt.Option ['n'] ["no-cabal-file"] (GetOpt.NoArg $ FlagCabalFile False) "Don't parse as .cabal file",
    GetOpt.Option [] ["stdout"] (GetOpt.NoArg $ FlagMode ModeStdout) "Write output to stdout (default)",
    GetOpt.Option ['i'] ["inplace"] (GetOpt.NoArg $ FlagMode ModeInplace) "Process files in-place",
    GetOpt.Option ['c'] ["check"] (GetOpt.NoArg $ FlagMode ModeCheck) "Fail with non-zero exit code if input is not formatted",
    GetOpt.Option [] ["stdin-input-file"] (GetOpt.ReqArg FlagStdinInputFile "FILE") "When reading from STDIN, use this file path to resolve relative references",
    GetOpt.Option ['h'] ["help"] (GetOpt.NoArg FlagHelp) "Show this help text",
    GetOpt.Option [] ["version"] (GetOpt.NoArg FlagVersion) "Show version"
  ]

data Config = Config
  { configHelp :: Bool,
    configOptions :: Options,
    configVersion :: Bool
  }
  deriving (Show)

initialConfig :: Config
initialConfig =
  Config
    { configHelp = False,
      configOptions = defaultOptions,
      configVersion = False
    }

applyFlag :: (Exception.MonadThrow m) => Config -> Flag -> m Config
applyFlag c f = case f of
  FlagCabalFile b -> pure c {configOptions = (configOptions c) {optCabalFile = b}}
  FlagError b -> pure c {configOptions = (configOptions c) {optError = b}}
  FlagHelp -> pure c {configHelp = True}
  FlagIndent s -> do
    i <- case Read.readMaybe s of
      Nothing -> Exception.throwM . userError $ "invalid indent: " <> show s
      Just i -> pure i
    pure c {configOptions = (configOptions c) {optIndent = i}}
  FlagMode m -> pure c {configOptions = (configOptions c) {optMode = m}}
  FlagStdinInputFile s -> pure c {configOptions = (configOptions c) {optStdinInputFile = Just s}}
  FlagTabular b -> pure c {configOptions = (configOptions c) {optTabular = b}}
  FlagVersion -> pure c {configVersion = True}

flagsToConfig :: (Exception.MonadThrow m) => [Flag] -> m Config
flagsToConfig = Monad.foldM applyFlag initialConfig