cmdlib-0.3.2: System/Console/CmdLib/Attribute.hs
{-# LANGUAGE MultiParamTypeClasses, ScopedTypeVariables, GADTs, FlexibleInstances,
PatternGuards, FunctionalDependencies, UndecidableInstances,
TypeSynonymInstances, OverlappingInstances, Rank2Types,
DeriveDataTypeable #-}
module System.Console.CmdLib.Attribute where
import Prelude hiding ( catch )
import Control.Exception
import Data.Maybe( catMaybes, fromMaybe, listToMaybe )
import Data.List( elemIndex )
import Data.Data
import Data.Generics.Text( gshow )
import Control.Monad.State( evalState, State, get, put )
import System.IO.Unsafe
-- Attributes.
data Attribute =
-- | Set a list of short flags (single character per flag, like in @-c@,
-- @-h@) for an option. Without the leading @-@.
Short [Char]
-- | Set a list of long flags for an option.
| Long [String]
-- | Set a list of long flags for an inversion of the option. Only used for
-- boolean invertible options. See also "long".
| InvLong [String]
-- | Whether this option is invertible. Only applies to boolean options and
-- defaults to True. (Invertible means that for --foo, there are --no-foo and
-- --foo=no alternatives. A non-invertible option will only create --foo.)
| Invertible Bool
-- | Set help string (one-line summary) for an option. Displayed in help.
| Help String
-- | When True, this option will contain the list of non-option arguments
-- passed to the command. Only applicable to [String]-typed options. Options
-- marked extra will not show up in help and neither will they be recognized
-- by their name on commandline.
| Extra Bool
-- | When set, this option will not show up on help and won't create a flag
-- (similar to Extra), but instead it will contain the n-th non-option
-- argument. The argument used up by such a positional option will not show
-- up in the list of non-option arguments.
| Positional Int
-- | When True, this option will require that the argument must be provided.
-- If the argument is also Positional, any preceeding Positional arguments
-- should also be Required.
| Required Bool
-- | Set the help string for an argument, the @FOO@ in @--wibblify=FOO@.
| ArgHelp String
-- | Set default value for this option. The default is only applied when its
-- type matches the option's parameter type, otherwise it is ignored.
| forall a. Data a => Default a
-- | When this attribute is given, the flag's value will be passed to the
-- provided IO action (which would presumably record the flag's value in a
-- global IORef for later use). Like with Default, the attribute is only
-- effective if the parameter type of the provided function matches the
-- parameter type of the option to which the attribute is applied.
| forall a. Data a => Global (a -> IO ())
-- | Whether the option is enabled. Disabled options are not recognized and
-- are not shown in help (effectively, they do not exist). Used to enable a
-- subset of all available options for a given command. For Record-based
-- commands (see "RecordCommand"), this is handled automatically based on
-- fields available in the command's constructor. Otherwise, constructs like
--
-- > enable <% option1 +% option2 +% option3 %% disable <% option4
--
-- may be quite useful.
| Enabled Bool
-- | Set the group name for this option. The groups are used to section the
-- help output (the options of a given group are shown together, under the
-- heading of the group). The ordering of the groups is given by the first
-- flag of each group. Flags themselves are in the order in which they are
-- given in the ADT or Record in question.
| Group String
instance Show Attribute where
show (Short x) = "Short " ++ show x
show (Long x) = "Long " ++ show x
show (InvLong x) = "Long " ++ show x
show (Invertible x) = "Invertible " ++ show x
show (Help x) = "Help " ++ show x
show (ArgHelp x) = "ArgHelp " ++ show x
show (Default x) = "Default " ++ gshow x
show (Enabled x) = "Enabled " ++ show x
show (Group x) = "Group " ++ show x
data RequiredArgException = RequiredArgException deriving (Show, Typeable)
instance Exception RequiredArgException
class (Eq k) => AttributeMapLike k a | a -> k where
attrFun :: a -> k -> [Attribute]
attrKeys :: a -> [k]
data AttributeMap k where
(:%%) :: (AttributeMapLike k a, AttributeMapLike k b) => a -> b -> AttributeMap k
SingletonMap :: (AttributeMapLike k a) => a -> AttributeMap k
EmptyMap :: AttributeMap k
-- | Join attribute mappings. E.g. @Key1 %> Attr1 %+ Attr2 %% Key2 %> Attr3 %+
-- Attr4@. Also possible is @[ Key1 %> Attr1, Key2 %> Attr2 ] %% Key3 %>
-- Attr3@, or many other variations.
(%%) :: (AttributeMapLike k a, AttributeMapLike k b) => a -> b -> AttributeMap k
(%%) = (:%%)
instance (Eq k) => AttributeMapLike k [(k, [Attribute])] where
attrKeys = map fst
attrFun pairs = foldl pair (const []) [ \x -> if x == k then a else [] | (k, a) <- pairs ]
where pair f g k = f k ++ g k
instance (Eq k) => AttributeMapLike k (k -> [Attribute]) where
attrKeys _ = [] -- can't extract...
attrFun = id
instance (AttributeMapLike k x) => AttributeMapLike k [x] where
attrKeys = concatMap attrKeys
attrFun l = concat . zipWith ($) (map attrFun l) . repeat
instance (Eq k) => AttributeMapLike k (AttributeMap k) where
attrKeys (a :%% b) = attrKeys a ++ attrKeys b
attrKeys (SingletonMap x) = attrKeys x
attrKeys EmptyMap = []
attrFun (a :%% b) = \k -> attrFun a k ++ attrFun b k
attrFun (SingletonMap x) = attrFun x
attrFun EmptyMap = const []
getattr :: a -> (Attribute -> Maybe a) -> [Attribute] -> a
getattr def proj a = fromMaybe def $ go a
where go (x:xs) | Just k <- proj x = Just k
| otherwise = go xs
go [] = Nothing
enabled = getattr False $ \k -> case k of Enabled j -> Just j ; _ -> Nothing
extra = getattr False $ \k -> case k of Extra j -> Just j ; _ -> Nothing
longs = getattr [] $ \k -> case k of Long j -> Just j ; _ -> Nothing
shorts = getattr [] $ \k -> case k of Short j -> Just j ; _ -> Nothing
invlongs = getattr [] $ \k -> case k of InvLong j -> Just j ; _ -> Nothing
invertible = getattr True $ \k -> case k of Invertible j -> Just j ; _ -> Nothing
helpattr = getattr "(no help)" $ \k -> case k of Help j -> Just j ; _ -> Nothing
arghelp = getattr "X" $ \k -> case k of ArgHelp j -> Just j ; _ -> Nothing
getgroup = getattr "Options" $ \k -> case k of Group j -> Just j ; _ -> Nothing
required = getattr False $ \k -> case k of Required j -> Just j ; _ -> Nothing
positional = getattr Nothing $ \k -> case k of Positional j -> Just (Just j) ; _ -> Nothing
defvalue :: (Typeable a, Data a) => [Attribute] -> a
defvalue attr
| (Default v:rem) <- [ x | x@(Default _) <- attr ] =
fromMaybe (defvalue rem) (cast v)
| otherwise = error "No default value."
setglobal :: [Attribute] -> (forall a. (Typeable a, Data a) => a) -> IO ()
setglobal (Global set:rem) value = set value >> setglobal rem value
setglobal (_:rem) value = setglobal rem value
setglobal [] _ = return ()
-- | Create a group. This extracts all the keys that are (explicitly) mentioned
-- in the body of the group and assigns the corresponding Group attribute to
-- them. Normally used like this:
--
-- > group "Group name" [ option %> Help "some help"
-- > , another %> Help "some other help" ]
--
-- Do not let the type confuse you too much. :)
group :: forall k a. (AttributeMapLike k a) => String -> a -> AttributeMap k
group name amap = foldl (%%) (SingletonMap amap) (map addgrp $ attrKeys amap)
where addgrp :: k -> AttributeMap k
addgrp key = SingletonMap [(key, [Group name])]
-- | For convenience. Same as "Enabled" True.
enable :: Attribute
enable = Enabled True
-- | For convenience. Same as "Enabled" False.
disable :: Attribute
disable = Enabled False
simple :: [Attribute]
simple = Invertible False %+ Default False
-- | For convenience. Same as "Long" ["foo"] %+ "InvLong" ["no-foo"]
long :: String -> [Attribute]
long n = Long [n] %+ InvLong ["no-" ++ n]
-- | For convenience. Same as "Short" ['x']
short :: Char -> Attribute
short n = Short [n]
class AttributeList a where
toAttributes :: a -> [Attribute]
instance AttributeList Attribute where
toAttributes = (:[])
instance AttributeList [Attribute] where
toAttributes = id
-- | Join multiple attributes into a list. Available for convenience (using
-- [Attribute] directly works just as well if preferred, although this is not
-- the case with keys, see @"+%"@).
(%+) :: (AttributeList a, AttributeList b) => a -> b -> [Attribute]
a %+ b = toAttributes a ++ toAttributes b
infixl 9 %+
attrs :: k -> (k -> [Attribute]) -> [Attribute]
attrs = flip ($)
data Key = KeyC Constr | KeyF TypeRep String
deriving (Eq, Show)
class ToKey a where
toKey :: a -> Key
instance (Data a) => ToKey a where
toKey = KeyC . toConstr
-- | Attach a (list of) attributes to a key. The key is usually either an ADT
-- constructor (for use with "ADTFlag"-style flags) or a record selector (for
-- use with "RecordFlag"s).
--
-- > data RFlags = Flags { wibblify :: Int, simplify :: Bool }
-- > data AFlag = Simplify | Wibblify Int
-- > rattr = wibblify %> Help "Add a wibblification pass." (%% ...)
-- > aattr = Wibblify %> Help "Add a wibblification pass." (%% ...)
--
-- @"%+"@ can be used to chain multiple attributes:
--
-- > attrs = wibblify %> Help "some help" %+ Default (3 :: Int) %+ ArgHelp "intensity"
--
-- But lists work just as fine:
--
-- > attrs = wibblify %> [ Help "some help", Default (3 :: Int), ArgHelp "intensity" ]
(%>) :: (ToKey k, AttributeList attr) => k -> attr -> AttributeMap Key
key %> attr = SingletonMap [(toKey key, toAttributes attr)]
class Keys a where
toKeys :: a -> [Key]
instance (ToKey a) => Keys a where
toKeys x = [toKey x]
instance Keys [Key] where
toKeys = id
-- | Attach an attribute to multiple keys: written from right to left,
-- i.e. @Attribute <% Key1 +% Key2@. Useful for setting up option groups
-- (although using "group" may be more convenient in this case) and option
-- enablement.
(<%) :: forall keys. (Keys keys) => Attribute -> keys -> AttributeMap Key
attr <% keys = SingletonMap $ zip (toKeys keys) (repeat [attr])
-- | Join multiple keys into a list, e.g. @Key1 +% Key2@. Useful with @"<%"@ to
-- list multiple (possibly heterogenously-typed) keys.
(+%) :: forall a b. (Keys a, Keys b) => a -> b -> [Key]
a +% b = toKeys a ++ toKeys b
infixl 8 %>
infixl 8 <%
infixl 7 %%
-- | Set an attribute on all keys.
everywhere :: (Eq k) => Attribute -> AttributeMap k
everywhere attr = SingletonMap $ const [attr]
instance (Data a, Data b) => ToKey (a -> b) where
toKey f | Just field <- fieldname = KeyF (typeOf (undefined :: a)) field
| Just constr <- constructor = KeyC constr
| otherwise = error $ "ToKey: " ++
show (typeOf (undefined :: a)) ++ " -> " ++
show (typeOf (undefined :: b))
where constrs = dataTypeConstrs $ dataTypeOf (undefined :: a)
fieldname = listToMaybe $ catMaybes [ nameIn c | c <- constrs ]
isup a = unsafePerformIO $
(undefined `fmap` evaluate a)
`catch` (\(e :: SomeException) -> case show e of
"up" -> return True
_ -> return False)
{-# NOINLINE isup #-}
iospoon a = unsafePerformIO $
(Just `fmap` evaluate a) `catch` (\(e :: SomeException) -> return Nothing)
{-# NOINLINE iospoon #-}
spooned c = [ isup (f $ test c i) | i <- [0..(length (constrFields c) - 1)] ]
nameIn c = elemIndex True (spooned c) >>= \i -> return (constrFields c !! i)
test :: forall b. (Data b) => Constr -> Int -> b
test c i = evalState (gmapM (subst i) (fromConstr c)) 0
subst :: forall x. Data x => Int -> x -> State Int x
subst i _ = do x <- get
res <- if x == i then return $ error "up" else return $ error "down"
put $ x + 1
return res
constructor = iospoon $ toConstr (f undefined)