text-replace-0.1: app/text-replace.hs
import Text.Replace
-- base
import Control.Applicative ((<|>))
import Data.Foldable (asum)
import Data.Function ((&))
import Data.Functor (void)
import System.Exit (die)
import qualified System.IO as IO
-- optparse-applicative
import qualified Options.Applicative as Opt
-- parsec
import qualified Text.Parsec as P
import Text.Parsec.Text.Lazy as P (Parser)
-- text
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.IO as LT
(!) :: Monoid a => a -> a -> a
(!) = mappend
infixr 6 !
optsParserInfo :: Opt.ParserInfo Opts
optsParserInfo
= Opt.info (Opt.helper <*> optsParser)
$ Opt.header "Perform simple replacements in a text file, using a list \
\of search/replace pairs."
! Opt.footer "The search for strings to replace is performed left-to-right, \
\preferring longer matches to shorter ones. All streams are \
\assumed to be UTF-8 encoded."
enc :: IO.TextEncoding
enc = IO.utf8
main :: IO ()
main = do
opts <- Opt.execParser optsParserInfo
IO.hSetEncoding IO.stdout enc
IO.hSetEncoding IO.stderr enc
let d = delimiterOpt opts
argMapping <-
let
f arg = parseReplacementList' d "--mapping" arg
in
foldMap f (opt_mapping opts)
fileMapping <-
let
f path = IO.withFile path IO.ReadMode $ \h -> do
IO.hSetEncoding h enc
x <- LT.hGetContents h
parseReplacementList' d path x
in
foldMap f (opt_mapFile opts)
withInputH (opt_inFile opts) $ \inH ->
withOutputH (opt_outFile opts) $ \outH -> do
input <- LT.hGetContents inH
let output = replaceWithList (argMapping ++ fileMapping) input
LT.hPutStr outH output
withInputH :: Maybe FilePath -> (IO.Handle -> IO a) -> IO a
withInputH Nothing f = f IO.stdin
withInputH (Just path) f = IO.withFile path IO.ReadMode f
withOutputH :: Maybe FilePath -> (IO.Handle -> IO a) -> IO a
withOutputH Nothing f = f IO.stdout
withOutputH (Just path) f = IO.withFile path IO.WriteMode f
data Opts =
Opts
{ opt_inFile :: Maybe FilePath
, opt_outFile :: Maybe FilePath
, opt_mapping :: [LT.Text]
, opt_mapFile :: [FilePath]
, opt_delimiter :: [Delimiter]
, opt_newlineDelimiter :: Bool
}
optsParser :: Opt.Parser Opts
optsParser =
Opts
<$> inFileParser
<*> outFileParser
<*> mappingParser
<*> mapFileParser
<*> delimiterParser
<*> newlineDelimiterParser
inFileParser :: Opt.Parser (Maybe FilePath)
inFileParser
= Opt.optional
$ Opt.strOption
$ Opt.metavar "FILEPATH"
! Opt.long "in-file"
! Opt.short 'i'
! Opt.help "Input file to read (optional, defaults to stdin)"
outFileParser :: Opt.Parser (Maybe FilePath)
outFileParser
= Opt.optional
$ Opt.strOption
$ Opt.metavar "FILEPATH"
! Opt.long "out-file"
! Opt.short 'o'
! Opt.help "Output file to write (optional, defaults to stdout)"
mappingParser :: Opt.Parser [LT.Text]
mappingParser
= Opt.many
$ Opt.strOption
$ Opt.metavar "MAPPING"
! Opt.long "mapping"
! Opt.short 'm'
! Opt.help "A list of search/replace pairs, separated by any of the \
\delimiters"
mapFileParser :: Opt.Parser [FilePath]
mapFileParser
= Opt.many
$ Opt.strOption
$ Opt.metavar "FILEPATH"
! Opt.long "map-file"
! Opt.short 'f'
! Opt.help "A file containing a list of search/replace pairs, \
\separated by any of the delimiters"
type Delimiter = String
delimiterParser :: Opt.Parser [Delimiter]
delimiterParser
= Opt.many
$ Opt.strOption
$ Opt.metavar "DELIMITER"
! Opt.long "delimiter"
! Opt.short 'd'
! Opt.help "Add a delimiter that separates search/replace strings in \
\--mapping and in the contents of --map-file"
newlineDelimiterParser :: Opt.Parser Bool
newlineDelimiterParser
= Opt.switch
$ Opt.long "newline-delimiter"
! Opt.short 'n'
! Opt.help "Add newline as a delimiter"
delimiterOpt :: Opts -> [Delimiter]
delimiterOpt opts =
opt_delimiter opts
& (if opt_newlineDelimiter opts then ("\n" :) else id)
parseReplacementList :: [Delimiter] -> P.SourceName -> LT.Text
-> Either P.ParseError [Replace]
parseReplacementList delims sourceName input =
let
delimP :: P.Parser ()
delimP = delims & fmap (void . P.try . P.string) & asum
strP :: P.Parser T.Text
strP = T.pack <$> P.manyTill P.anyChar (delimP <|> P.eof)
strP' :: P.Parser Text'
strP' = do
x <- P.anyChar
xs <- T.pack <$> P.manyTill P.anyChar (delimP)
pure $ Text' x xs
replaceP :: P.Parser Replace
replaceP = Replace <$> strP' <*> strP
in
P.parse (P.many replaceP <* P.eof) sourceName input
parseReplacementList' :: [Delimiter] -> P.SourceName -> LT.Text -> IO [Replace]
parseReplacementList' delims sourceName input =
parseReplacementList delims sourceName input & either (die . show) pure