packages feed

shell-monad 0.2.1 → 0.3.0

raw patch · 6 files changed

+329/−57 lines, 6 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Control.Monad.Shell: (->-) :: Script () -> Fd -> (Script (), Fd)
- Control.Monad.Shell: (|<&) :: (Script (), Fd) -> Fd -> Script ()
- Control.Monad.Shell: (|>&) :: (Script (), Fd) -> Fd -> Script ()
- Control.Monad.Shell: instance Eq Var
- Control.Monad.Shell: instance Ord Var
- Control.Monad.Shell: instance Show Var
- Control.Monad.Shell: val :: Var -> Quoted Text
+ Control.Monad.Shell: (&) :: Script () -> Fd -> (Script (), Fd)
+ Control.Monad.Shell: (<&) :: (Script (), Fd) -> Fd -> Script ()
+ Control.Monad.Shell: (>&) :: (Script (), Fd) -> Fd -> Script ()
+ Control.Monad.Shell: FromBeginning :: Direction
+ Control.Monad.Shell: FromEnd :: Direction
+ Control.Monad.Shell: LongestMatch :: Greediness
+ Control.Monad.Shell: ShortestMatch :: Greediness
+ Control.Monad.Shell: WithVar :: Var -> (Quoted Text -> Quoted Text) -> WithVar
+ Control.Monad.Shell: caseOf :: Var -> [(Quoted Text, Script ())] -> Script ()
+ Control.Monad.Shell: data Direction
+ Control.Monad.Shell: data Greediness
+ Control.Monad.Shell: data WithVar
+ Control.Monad.Shell: defaultVar :: Param param => Var -> param -> Script Var
+ Control.Monad.Shell: errUnlessVar :: Param param => Var -> param -> Script Var
+ Control.Monad.Shell: instance Eq VarName
+ Control.Monad.Shell: instance Ord VarName
+ Control.Monad.Shell: instance Param WithVar
+ Control.Monad.Shell: instance Show VarName
+ Control.Monad.Shell: lengthVar :: Var -> Script Var
+ Control.Monad.Shell: setVar :: Param param => Var -> param -> Script ()
+ Control.Monad.Shell: trimVar :: Greediness -> Direction -> Var -> Quoted Text -> Script Var
+ Control.Monad.Shell: whenVar :: Param param => Var -> param -> Script Var

Files

CHANGELOG view
@@ -1,3 +1,15 @@+shell-monad (0.3.0) unstable; urgency=medium++  * Renamed a few operators.+  * Added defaultVar, whenVar, lengthVar, and trimVar.+  * Due to changes in how variable expansion is handled, the val function+    has been removed, and WithVar should be used instead.+  * newVar now ensures that the variable starts out empty.+  * Added caseOf+  * Fix bug in stopOnFailure.++ -- Joey Hess <id@joeyh.name>  Fri, 26 Dec 2014 22:58:56 -0400+ shell-monad (0.2.1) unstable; urgency=medium    * Simple support for globs.
Control/Monad/Shell.hs view
@@ -7,49 +7,65 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-}  module Control.Monad.Shell (+	-- * Core 	Script, 	script, 	linearScript, 	Var,-	val, 	Quoted, 	quote, 	glob,+	-- * Running commands 	run, 	cmd, 	Param, 	CmdParams, 	Output(..), 	Val(..),-	comment,+	-- * Shell variables 	NamedLike(..), 	NameHinted, 	newVar, 	newVarContaining,+	setVar, 	globalVar, 	positionalParameters, 	takeParameter,+	defaultVar,+	whenVar,+	lengthVar,+	trimVar,+	Greediness(..),+	Direction(..),+	WithVar(..),+	-- * Monadic combinators 	func, 	forCmd, 	whileCmd, 	ifCmd, 	whenCmd, 	unlessCmd,-	readVar,-	stopOnFailure,-	ignoreFailure,+	caseOf, 	(-|-), 	(-&&-), 	(-||-),+	-- * Redirection 	RedirFile, 	(|>), 	(|>>), 	(|<), 	toStderr,-	(|>&),-	(|<&),-	(->-),+	(>&),+	(<&),+	(&), 	hereDocument,+	-- * Error handling+	stopOnFailure,+	ignoreFailure,+	errUnlessVar,+	-- * Misc+	comment,+	readVar, ) where  import qualified Data.Text.Lazy as L@@ -61,12 +77,21 @@ import System.Posix.IO (stdInput, stdOutput, stdError)  -- | A shell variable.-newtype Var = Var L.Text+data Var = Var+	{ varName :: VarName+	, expandVar :: Env -> VarName -> Quoted L.Text+	}++newtype VarName = VarName L.Text 	deriving (Eq, Ord, Show) --- | Expand a shell variable to its value.-val :: Var -> Quoted L.Text-val (Var v) = Q ("\"$" <> v <> "\"")+simpleVar :: VarName -> Var+simpleVar name = Var+	{ varName = name+	-- Used to expand the variable; can be overridden for other+	-- types of variable expansion.+	, expandVar = \_ (VarName n) -> Q ("\"$" <> n <> "\"")+	}  -- | A value that is safely quoted. newtype Quoted a = Q { getQ :: a }@@ -79,7 +104,7 @@ -- like csh. quote :: L.Text -> Quoted L.Text quote t-	| L.all (isAlphaNum) t = Q t+	| L.all (\c -> isAlphaNum c || c == '_') t = Q t 	| otherwise = Q $ q <> L.intercalate "'\"'\"'" (L.splitOn q t) <> q   where 	q = "'"@@ -148,7 +173,7 @@ -- | Environment built up by the shell script monad, -- so it knows which environment variables and functions are in use. data Env = Env-	{ envVars :: S.Set Var+	{ envVars :: S.Set VarName 	, envFuncs :: S.Set Func 	} @@ -156,7 +181,7 @@ 	mempty = Env mempty mempty 	mappend a b = Env (envVars a <> envVars b) (envFuncs a <> envFuncs b) -modifyEnvVars :: Env -> (S.Set Var -> S.Set Var) -> Env+modifyEnvVars :: Env -> (S.Set VarName -> S.Set VarName) -> Env modifyEnvVars env f = env { envVars = f (envVars env) }  modifyEnvFuncs :: Env -> (S.Set Func -> S.Set Func) -> Env@@ -216,8 +241,11 @@ 			use $ redirFd fd1 (Just stdInput) <> "<&" <> showFd fd2 		(RedirHereDoc t) 			| multiline -> -				let marker = eofMarker t-				in use $ "<<" <> marker <> "\n" <> t <> "\n" <> marker+				let myEOF = eofMarker t+				in use $ "<<" <> myEOF <> "\n"+					<> t +					<> "\n" +					<> myEOF 			-- Here documents cannot be represented in a single 			-- line script. Instead, generate: 			-- (echo l1; echo l2; ...) | cmd@@ -309,8 +337,13 @@ -- | Var arguments cause the (quoted) value of a shell variable to be -- passed to the command. instance Param Var where-	toTextParam = toTextParam . val+	toTextParam v = \env -> getQ $ expandVar v env (varName v) +-- | Allows modifying the value of a shell variable before it is passed to+-- the command.+instance Param WithVar where+	toTextParam (WithVar v f) = getQ . f . Q . toTextParam v+ -- | Quoted Text arguments are passed as-is. instance Param (Quoted L.Text) where 	toTextParam (Q v) = const v@@ -345,6 +378,13 @@ -- | An arbitrary value. newtype Val v = Val v +-- | Allows modifying the value of a variable before it is passed to a+-- command. The function is passed a Quoted Text which will expand to the+-- value of the variable, and can modify it, by using eg 'mappend'.+--+-- > cmd "rmdir" (WithVar name ("/home/" <>))+data WithVar = WithVar Var (Quoted L.Text -> Quoted L.Text)+ -- | Adds an Expr to the script. add :: Expr -> Script () add expr = Script $ \env -> ([expr], env, ())@@ -374,36 +414,31 @@ instance NameHinted (Maybe L.Text) where 	hinted = id --- | Defines a new shell variable.+-- | Defines a new shell variable, which starts out not being set. -- -- Each call to newVar will generate a new, unique variable name. -- -- The namehint can influence this name, but is modified to ensure -- uniqueness. newVar :: (NameHinted namehint) => namehint -> Script Var-newVar = hinted $ \namehint -> Script $ \env ->-	let v = go namehint env (0 :: Integer)-	in ([], modifyEnvVars env (S.insert v), v)-  where-	go namehint env x-		| S.member v (envVars env) = go namehint env (succ x)-		| otherwise = v-	  where-		v = Var $ "_"-			<> genvarname namehint-			<> if x == 0 then "" else L.pack (show (x + 1))-	-	genvarname = maybe "v" (L.filter isAlpha)+newVar = newVarContaining ""  -- | Creates a new shell variable, with an initial value. newVarContaining :: (NameHinted namehint) => L.Text -> namehint -> Script Var newVarContaining value = hinted $ \namehint -> do-	v@(Var name) <- newVar namehint+	v@(Var { varName = VarName name }) <- newVarUnsafe namehint 	Script $ \env -> ([Cmd (name <> "=" <> getQ (quote value))], env, v) +-- | Sets the Var to the value of the param. +setVar :: Param param => Var -> param -> Script ()+setVar (Var { varName = VarName name }) p = Script $ \env -> +	([Cmd (name <> "=" <> toTextParam p env)], env, ())+ -- | Gets a Var that refers to a global variable, such as PATH globalVar :: L.Text -> Script Var-globalVar name = Script $ \env -> let v = Var name in ([], modifyEnvVars env (S.insert v), v)+globalVar name = Script $ \env ->+	let v = simpleVar (VarName name)+	in ([], modifyEnvVars env (S.insert (varName v)), v)  -- | This special Var expands to whatever parameters were passed to the -- shell script.@@ -413,7 +448,7 @@ -- -- (This is `$@` in shell) positionalParameters :: Var-positionalParameters = Var "@"+positionalParameters = simpleVar (VarName "@")  -- | Takes the first positional parameter, removing it from -- positionalParameters and returning a new Var that holds the value of the@@ -429,9 +464,104 @@ -- >   cmd "echo" "remaining parameters:" positionalParameters takeParameter :: (NameHinted namehint) => namehint -> Script Var takeParameter = hinted $ \namehint -> do-	p@(Var name) <- newVar namehint+	p@(Var { varName = VarName name}) <- newVarUnsafe namehint 	Script $ \env -> ([Cmd (name <> "=\"$1\""), Cmd "shift"], env, p) ++-- | Creates a new shell variable, but does not ensure that it's not+-- already set to something. For use when the caller is going to generate+-- some shell script that is guaranteed to clobber any existing value of+-- the variable.+newVarUnsafe :: (NameHinted namehint) => namehint -> Script Var+newVarUnsafe = hinted $ \namehint -> Script $ \env ->+	let v = go namehint env (0 :: Integer)+	in ([], modifyEnvVars env (S.insert (varName v)), v)+  where+	go namehint env x+		| S.member (varName v) (envVars env) =+			go namehint env (succ x)+		| otherwise = v+	  where+		v = simpleVar $ VarName $ "_"+			<> genvarname namehint+			<> if x == 0 then "" else L.pack (show (x + 1))+	+	genvarname = maybe "v" (L.filter isAlpha)++modVar :: Var -> (L.Text -> Env -> L.Text) -> Script Var+modVar (Var { varName = VarName varname }) p = do+	v <- newVarUnsafe (NamedLike varname)+	return $ v+		{ expandVar = \env _ -> Q $ "\"${" <> p varname env <> "}\""+		}++modVar' :: (Param param) => L.Text -> Var -> param -> Script Var+modVar' t v p = modVar v $ \varname env ->+	varname <> t <> toTextParam p env++-- | Generates a new Var. Expanding this Var will yield the same+-- result as expanding the input Var, unless it is empty, in which case+-- it instead defaults to the expansion of the param.+defaultVar :: (Param param) => Var -> param -> Script Var+defaultVar = modVar' ":-"++-- | Generates a new Var. If the input Var is empty, then this new Var+-- will likewise expand to the empty string. But if not, the new Var+-- expands to the param.+whenVar :: (Param param) => Var -> param -> Script Var+whenVar = modVar' ":+"++-- | Generates a new Var. If the input Var is empty then expanding this new+-- Var will cause an error to be thrown, using the param as the error+-- message. If the input Var is not empty, then the new Var expands to the+-- same thing the input Var expands to.+errUnlessVar :: (Param param) => Var -> param -> Script Var+errUnlessVar = modVar' ":?"++-- | Generates a new Var, which expands to the length of the+-- expansion of the input Var.+--+-- Note that 'lengthVar positionalParameters' expands to the number+-- of positional parameters.+lengthVar :: Var -> Script Var+-- Implementation note: ${#${foo:-bar}} is not legal shell code.+-- So, to allow taking the length of Vars that expand to such things,+-- a temporary Var is created, assigned to the expansion of the input Var.+-- This yields shell code like:+-- ${_tmp1:-$(_tmp1="${foo:-bar}"; echo ${#_tmp1})}+-- But, this approach won't work for $@, so handle it as a special+-- case.+lengthVar v@(Var { varName = VarName varname })+	| varname /= "@" = do+		tmpvar <- newVar (NamedLike "tmp")+		modVar tmpvar $ \tmpname env ->+			let hack = do+				setVar tmpvar v+				cmd ("echo" :: L.Text) $ tmpvar+					{ expandVar = \_ _ -> Q $+						"${#" <> tmpname <> "}"+					}+			in varname <> ":-" <> toTextParam (Output hack) env+	| otherwise = return $ simpleVar (VarName "#")++-- | Produces a Var that is a trimmed version of the input Var.+--+-- The Quoted Text is removed from the value of the Var, either+-- from the beginning or from the end.+--+-- If the Quoted Text was produced by 'glob', it could match in+-- multiple ways. You can choose whether to remove the shortest or+-- the longest match.+trimVar :: Greediness -> Direction -> Var -> Quoted L.Text -> Script Var+trimVar ShortestMatch FromBeginning = modVar' "#"+trimVar LongestMatch FromBeginning = modVar' "##"+trimVar ShortestMatch FromEnd = modVar' "%"+trimVar LongestMatch FromEnd = modVar' "%%"++data Greediness = ShortestMatch | LongestMatch++data Direction = FromBeginning | FromEnd+ -- | Defines a shell function, and returns an action that can be run to -- call the function. --@@ -488,9 +618,9 @@ -- The action is run for each part, passed a Var containing the part. forCmd :: Script () -> (Var -> Script ()) -> Script () forCmd c a = do-	v@(Var vname) <- newVar (NamedLike "x")+	v@(Var { varName = VarName varname}) <- newVarUnsafe (NamedLike "x") 	s <- toLinearScript <$> runM c-	add $ Cmd $ "for " <> vname <> " in $(" <> s <> ")"+	add $ Cmd $ "for " <> varname <> " in $(" <> s <> ")" 	block "do" (a v) 	add $ Cmd "done" @@ -537,6 +667,32 @@ 	ifCmd' ("! " <>) cond $ 		block "then" a +-- | Matches the value of the Var against the Quoted Text (which can+-- be generated by 'glob'), and runs the Script action associated+-- with the first match.+caseOf :: Var -> [(Quoted L.Text, Script ())] -> Script ()+caseOf _ [] = return ()+caseOf v l = go True l+  where+	-- The case expression is formatted somewhat unusually,+	-- in order to make it work in both single line and multi-line+	-- rendering.+	--+	-- > case "$foo" in ook) : +	-- >     echo got ook+	-- >     echo yay+	-- > ;; *) :+	-- >     echo default+	-- > ;; esac+	go _ [] = add $ Cmd $ ";; esac"+	go atstart ((t, s):rest) = do+		let leader = if atstart+			then "case " <> toTextParam v undefined <> " in "+			else ";; "+		add $ Cmd $ leader <> getQ t <> ") :"+		mapM_ (add . indent) =<< runM s+		go False rest+ -- | Creates a block such as "do : ; cmd ; cmd" or "else : ; cmd ; cmd" -- -- The use of : ensures that the block is not empty, and allows@@ -549,13 +705,14 @@  -- | Generates shell code to fill a variable with a line read from stdin. readVar :: Var -> Script ()-readVar (Var vname) = add $ Cmd $ "read " <> getQ (quote vname)+readVar (Var { varName = VarName varname }) = add $+	Cmd $ "read " <> getQ (quote varname)  -- | By default, shell scripts continue running past commands that exit -- nonzero. Use "stopOnFailure True" to make the script stop on the first -- such command. stopOnFailure :: Bool -> Script ()-stopOnFailure b = add $ Cmd $ "set " <> if b then "-" else "+" <> "x"+stopOnFailure b = add $ Cmd $ "set " <> (if b then "-" else "+") <> "x"  -- | Makes a nonzero exit status be ignored. ignoreFailure :: Script () -> Script ()@@ -637,23 +794,27 @@  -- | Redirects a script's output to stderr. toStderr :: Script () -> Script ()-toStderr s = s ->- stdOutput |>& stdError+toStderr s = s &stdOutput>&stdError  -- | Redirects the first file descriptor to output to the second. -- -- For example, to redirect a command's stderr to stdout: ----- > cmd "foo" ->- stdError) |>& stdOutput-(|>&) :: (Script (), Fd) -> Fd -> Script ()-(s, fd1) |>& fd2 = redir s (RedirOutput fd1 fd2)+-- > cmd "foo" &stdError>&stdOutput+(>&) :: (Script (), Fd) -> Fd -> Script ()+(s, fd1) >& fd2 = redir s (RedirOutput fd1 fd2)  -- | Redirects the first file descriptor to input from the second.-(|<&) :: (Script (), Fd) -> Fd -> Script ()-(s, fd1) |<& fd2 = redir s (RedirInput fd1 fd2)+--+-- For example, to read from Fd 42:+--+-- > cmd "foo" &stdInput<&Fd 42+(<&) :: (Script (), Fd) -> Fd -> Script ()+(s, fd1) <& fd2 = redir s (RedirInput fd1 fd2) --- | Helper for '|>&' and '|<&'-(->-) :: Script () -> Fd -> (Script (), Fd)-(->-) = (,)+-- | Helper for '>&' and '<&'+(&) :: Script () -> Fd -> (Script (), Fd)+(&) = (,)  -- | Provides the Text as input to the Script, using a here-document. hereDocument :: Script () -> L.Text -> Script ()
TODO view
@@ -1,3 +1,2 @@ * shell arithmetic * nicer interface for the `test` command (aka `[`)-* ${var%foo} etc
+ examples/protocol.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE OverloadedStrings, ExtendedDefaultRules, MultiParamTypeClasses #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-}+import Control.Monad.Shell+import qualified Data.Text.Lazy as T+import qualified Data.Text.Lazy.IO as T+import Control.Applicative+default (T.Text)++data Proto+	= Foo String+	| Bar+	| Baz Integer+	deriving (Show)++class Monad t => OutputsProto t where+	output :: Proto -> t ()++instance OutputsProto IO where+	output = putStrLn . fromProto++instance OutputsProto Script where+	output = cmd "echo" . fromProto++class Monad t => InputsProto t p where+	input :: t p++instance InputsProto IO Proto where+	input = toProto <$> readLn++instance InputsProto Script Var where+	input = do+		v <- newVar ()+		readVar v+		return v++protoExchangeIO :: Proto -> IO Proto+protoExchangeIO p = do+	output p+	input++foo :: Script ()+foo = do+	stopOnFailure True+	handler <- func (NamedLike "handler") $+		handleProto =<< input+	output (Foo "starting up")+	handler+	output Bar+	handler++pFOO, pBAR, pBAZ :: String+(pFOO, pBAR, pBAZ) = ("FOO", "BAR", "BAZ")++fromProto :: Proto -> String+fromProto (Foo s) = pFOO ++ " " ++ s+fromProto Bar = pBAR ++ " "+fromProto (Baz i) = pBAZ ++ " " ++ show i++-- throws exception if the string cannot be parsed+toProto :: String -> Proto+toProto s = case break (== ' ') s of+	(w, ' ':rest)+		| w == pFOO -> Foo rest+		| w == pBAR && null rest -> Bar+		| w == pBAZ -> Baz (read rest)+		| otherwise -> error $ "unknown protocol command: " ++ w+	(_, _) -> error "protocol splitting error"++handleProto :: Var -> Script ()+handleProto v = do+	w <- getProtoCommand v+	caseOf w+		[ (quote (T.pack pFOO), handleFoo =<< getProtoRest v)+		, (quote (T.pack pBAR), handleBar)+		, (quote (T.pack pBAZ), handleBaz =<< getProtoRest v)+		, (glob "*", do+			toStderr $ cmd "echo" "unknown protocol command" w+			cmd "false"+		  )+		]++handleFoo :: Var -> Script ()+handleFoo v = toStderr $ cmd "echo" "yay, I got a Foo" v++handleBar :: Script ()+handleBar = toStderr $ cmd "echo" "yay, I got a Bar"++handleBaz :: Var -> Script ()+handleBaz num = forCmd (cmd "seq" "1" num) $+	toStderr . cmd "echo" "yay, I got a Baz"++getProtoCommand :: Var -> Script Var+getProtoCommand v = trimVar LongestMatch FromEnd v (glob " *")++getProtoRest :: Var -> Script Var+getProtoRest v = trimVar ShortestMatch FromBeginning v (glob "[! ]*[ ]")++main :: IO ()+main = T.writeFile "protocol.sh" $ script foo
examples/santa.hs view
@@ -11,9 +11,9 @@ 	hohoho <- mkHohoho 	hohoho (Val 1) -	promptFor "What's your name?" $ \name -> pipeLess $ do-		cmd "echo" "Let's see what's in" (val name <> quote "'s") "stocking!"-		forCmd (cmd "ls" "-1" (quote "/home/" <> val name)) $ \f -> do+	promptFor "What's your name?" "virginia" $ \name -> pipeLess $ do+		cmd "echo" "Let's see what's in" (WithVar name (<> quote "'s")) "stocking!"+		forCmd (cmd "ls" "-1" (WithVar name (quote "/home/" <>))) $ \f -> do 			cmd "echo" "a shiny new" f 			hohoho (Val 1) @@ -29,9 +29,9 @@ pipeLess :: Script () -> Script () pipeLess c = c -|- cmd "less" -promptFor :: T.Text -> (Var -> Script ()) -> Script ()-promptFor prompt cont = do+promptFor :: T.Text -> T.Text -> (Var -> Script ()) -> Script ()+promptFor prompt defaultname cont = do 	cmd "printf" (prompt <> " ") 	var <- newVar (NamedLike prompt) 	readVar var-	cont var+	cont =<< defaultVar var defaultname
shell-monad.cabal view
@@ -1,5 +1,5 @@ Name: shell-monad-Version: 0.2.1+Version: 0.3.0 Cabal-Version: >= 1.8 License: BSD3 Maintainer: Joey Hess <id@joeyh.name>@@ -16,6 +16,7 @@   CHANGELOG   TODO   examples/santa.hs+  examples/protocol.hs  Library   GHC-Options: -Wall