ruby-qq-0.1.0.0: src/RubyQQ.hs
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Provides some quasiquoters that mimic Ruby's percent-letter syntax.
module RubyQQ (
-- * Shell exec
x, xx
-- * Splitting on whitespace
, w, ww
-- * Strings
, q, qq
-- * Regular expressions
, r, ri, rx, rix, (=~)
) where
import Control.Applicative
import Control.Monad
import Data.ByteString.Char8 (ByteString, pack)
import Data.Char
import Data.Function
import Data.Monoid
import Language.Haskell.Exts (parseExp, ParseResult(..))
import Language.Haskell.Meta hiding (parseExp)
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import System.Process
import System.IO
import Prelude hiding (lex)
import Text.ParserCombinators.ReadP (readP_to_S)
import Text.Read.Lex
import Text.Regex.PCRE.Light
import Text.Trifecta
class Stringable a where asString :: a -> String
class Stringable' flag a where asString' :: flag -> a -> String
instance (StringPred flag a, Stringable' flag a) => Stringable a where
asString = asString' (undefined :: flag)
class StringPred flag a | a -> flag where {}
instance StringPred HString String
instance (flag ~ HShowable) => StringPred flag a
instance Stringable' HString String where asString' _ = id
instance Show a => Stringable' HShowable a where asString' _ = show
data HString
data HShowable
expQuoter :: (String -> Q Exp) -> QuasiQuoter
expQuoter lam = QuasiQuoter
lam
(error "this quoter cannot be used in pattern context")
(error "this quoter cannot be used in type context")
(error "this quoter cannot be used in declaration context")
-- | @[x|script|]@ executes @script@ and returns @(stdout,stderr)@. It has type @'IO' ('String','String')@.
--
-- >>> [x|ls -a | wc -l|]
-- (" 8\n","")
--
-- >>> [x|echo >&2 "Hello, world!"|]
-- ("","Hello, world!\n")
x :: QuasiQuoter
x = expQuoter $
\str -> [|do
(_, Just h, Just he, _) <- createProcess (shell $(stringE str))
{ std_out = CreatePipe, std_err = CreatePipe }
(liftM2 (,) `on` hGetContents) h he|]
-- | @[xx|script|]@ spawns a shell process running @script@ and returns
-- @(hndl_stdin, hndl_stdout, hndl_stderr)@. It has type @'IO'
-- ('Handle','Handle','Handle')@.
--
-- >>> [xx|ls|]
-- ({handle: fd:8},{handle: fd:9},{handle: fd:11})
xx :: QuasiQuoter
xx = expQuoter $
\str -> [|do
(Just hi, Just h, Just he, _) <- createProcess (shell $(stringE str))
{ std_in = CreatePipe, std_out = CreatePipe, std_err = CreatePipe }
return (hi, h, he)|]
-- | @[w|input|]@ is a list containing @input@ split on all whitespace
-- characters.
--
-- Note that no escape expansion occurs:
--
-- >>> [w|hello\n world!|]
-- ["hello\\n","world!"]
w :: QuasiQuoter
w = expQuoter $
\str -> case parseString safeWords mempty (' ':str) of
Success strs -> listE $ map stringE strs
n -> error $ show n
-- | @[ww|input|]@ is a list containing @input@ split on all whitespace
-- characters, with some extra features:
--
-- * Ruby-style interpolation is supported.
--
-- >>> [ww|foo bar #{ reverse "zab" }|]
-- ["foo","bar","baz"]
--
-- /This feature uses some typeclass magic to convert input into the/
-- /friendliest possible String format. If you encounter errors like "No/
-- /instance for (Stringable' flag [a0])", add a type signature to your/
-- /input./
--
-- * All escape sequences are interpreted.
--
-- >>> [ww|Here\n are\SOH some\x123 special\1132 characters\\!|]
-- ["Here\n","are\SOH","some\291","special\1132","characters\\!"]
ww :: QuasiQuoter
ww = expQuoter $
\str -> case parseString interpWords mempty (' ':str) of
Success strs -> listE strs
Failure n -> error $ show n
-- | @[q|input|]@ is @input@. This quoter is essentially the identity
-- function. It is however a handy shortcut for multiline strings.
q :: QuasiQuoter
q = expQuoter stringE
-- | @[qq|input|]@ is @input@, with interpolation and escape sequences
-- expanded.
--
-- >>> let name = "Brian" in [qq|Hello, #{name}!|]
-- "Hello, Brian!"
qq :: QuasiQuoter
qq = expQuoter interpStr'
-- | @[r|pat|]@ is a regular expression defined by @pat@.
r :: QuasiQuoter
r = expQuoter $ \s -> [|compile (pack $(stringE s)) []|]
-- | @[ri|pat|]@ is @[r|pat|]@, but is case-insensitive.
ri :: QuasiQuoter
ri = expQuoter $ \s -> [|compile (pack $(stringE s)) [caseless]|]
-- | @[rx|pat|]@ is @[r|pat|]@, but ignores whitespace and comments in the
-- regex body.
rx :: QuasiQuoter
rx = expQuoter $ \s -> [|compile (pack $(stringE s)) [extended]|]
-- | @[rix|pat|]@ is a combination of @ri@ and @rx@.
rix :: QuasiQuoter
rix = expQuoter $ \s -> [|compile (pack $(stringE s)) [extended, caseless]|]
-- | @(=~)@ is an infix synonym for 'Text.Regex.PCRE.Light.match', with no
-- exec options provided.
--
-- >>> (pack "foobar") =~ [r|((.)\2)b|]
-- Just ["oob", "oo", "o"]
(=~) :: ByteString -> Regex -> Maybe [ByteString]
s =~ re = Text.Regex.PCRE.Light.match re s []
safeWords :: Parser [String]
safeWords = filter (not . null) <$> many1 (many1 (satisfy isSpace) *> word) where
word = many $ try ('\\' <$ string "\\\\")
<|> try (char '\\' *> satisfy isSpace)
<|> satisfy (not . isSpace)
interpWords :: Parser [Q Exp]
interpWords = many1 (many1 (satisfy isSpace) *> (try (fst <$> expq) <|> word)) where
word = fmap (stringE . unString . concat) $
many $ try (string "\\\\")
<|> try (char '\\' *> string "#")
<|> try (fmap return $ char '\\' *> satisfy isSpace)
<|> fmap return (satisfy (not . isSpace))
interpStr' :: String -> Q Exp
interpStr' m = [|concat $(listE (go [] m)) :: String|] where
go n [] = [stringE n]
go acc y@('#':'{':_) = case parseString expq mempty y of
Success (qqq,s) -> stringE acc:qqq:go [] s
Failure _ -> error "failure"
go acc ('\\':'#':xs) = go (acc ++ "#") xs
go acc s = case readLitChar s of
[(y,xs)] -> go (acc ++ [y]) xs
_ -> error $ "could not read character literal in " ++ s
interpStr :: Parser [Q Exp]
interpStr = many1 (try (fst <$> expq) <|> word) where
word = stringE <$> many1 (notChar '\\')
expq :: Parser (Q Exp, String)
expq = do
_ <- string "#{"
ex <- expBody ""
_ <- char '}'
m <- many anyChar
return (ex, m)
where
expBody s = do
till <- many (notChar '}')
let conc = s ++ till
case parseExp conc of
ParseOk e -> let m = return $ toExp e in return [e|asString $(m)|]
_ -> char '}' *> expBody (conc ++ "}")
unString :: String -> String
unString s' = case readP_to_S lex $ '"' : concatMap escape s' ++ "\"" of
[(String str,"")] -> str
_ -> error $ "Invalid string literal \"" ++ s' ++ "\" (possibly a bad escape sequence)"
where escape '"' = "\\\""
escape m' = [m']
many1 :: Alternative f => f a -> f [a]
many1 f = liftA2 (:) f (many f)