packages feed

shellmate 0.1.5 → 0.1.6

raw patch · 4 files changed

+547/−541 lines, 4 filessetup-changedPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Control.Shell: class File a
+ Control.Shell: forEachFile_ :: FilePath -> (FilePath -> Shell ()) -> Shell ()
+ Control.Shell: hClose :: Handle -> Shell ()

Files

Control/Shell.hs view
@@ -1,478 +1,484 @@-{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, DeriveDataTypeable,
-             MultiParamTypeClasses, FunctionalDependencies,
-             CPP,
-             UndecidableInstances #-}
--- | Simple interface for shell scripting-like tasks.
-module Control.Shell ( 
-    Shell,
-    Guard (..),
-    shell,
-    mayFail, orElse,
-    withEnv, getEnv, lookupEnv,
-    run, run_, runInteractive, genericRun, sudo,
-    cd, cpDir, pwd, ls, mkdir, rmdir, inDirectory, isDirectory,
-    withHomeDirectory, inHomeDirectory, withAppDirectory, inAppDirectory,
-    forEachFile, cpFiltered,
-    isFile, rm, mv, cp, file,
-    withTempFile, withCustomTempFile,
-    withTempDirectory, withCustomTempDirectory, inTempDirectory,
-    hPutStr, hPutStrLn, echo,
-    (|>),
-    module System.FilePath, liftIO
-  ) where
-import Control.Applicative
-import Control.Monad (ap, forM, filterM, forM_, when)
-import Control.Monad.IO.Class
-import Data.Time.Clock
-import Data.Typeable
-import System.FilePath
-import System.IO.Unsafe
-import qualified System.Process as Proc
-import qualified System.Directory as Dir
-import qualified System.Exit as Exit
-import qualified System.IO as IO
-import qualified Control.Exception as Ex
-import qualified Control.Concurrent as Conc
-import qualified System.IO.Temp as Temp
-import qualified System.Environment as Env
-
-newtype ShellException = ShellException String deriving (Typeable, Show)
-instance Ex.Exception ShellException
-
--- | Environment variables.
-type Env = [(String, String)]
-
--- | A command name plus a ProcessHandle.
-data Pid = Pid {pidName :: String, pidHandle :: Proc.ProcessHandle}
-
--- | Monad for running shell commands. If a command fails, the entire
---   computation is aborted unless @mayFail@ is used.
-newtype Shell a = Shell {
-    unSh :: Env -> IO (Either String a, [Pid])
-  }
-
-instance Monad Shell where
-  fail err = Shell $ \_ -> return (Left err, [])
-  return x = Shell $ \_ -> return (Right x, [])
-  -- | The bind operation of the Shell monad is effectively a barrier; all
-  --   commands on the left hand side of a bind will complete before any
-  --   command on the right hand side is attempted.
-  --   To lazily stream data between two commands, use the @|>@ combinator.
-  (Shell m) >>= f = Shell $ \env -> do
-    (x, pids) <- m env
-    merr <- waitPids pids
-    case (x, merr) of
-      (Left err, _) -> return (Left err, [])
-      (_, Just err) -> return (Left err, [])
-      (Right x', _) -> unSh (f x') env
-
-instance MonadIO Shell where
-  liftIO act = Shell $ \_ -> flip Ex.catch exHandler $ do
-    x <- act
-    return (Right x, [])
-
-instance Applicative Shell where
-  pure  = return
-  (<*>) = ap
-
-instance Functor Shell where
-  fmap f x = do
-    x' <- x
-    return $! f x'
-
--- | Enable the user to use 'file "foo" |> fmap reverse |> file "bar"' as
---   syntax for reading the file 'foo', reversing the contents and writing the
---   result to 'bar'.
---   Note that this uses lazy IO in the form of read/writeFile.
-class File a where
-  file :: FilePath -> a
-
-instance File (String -> Shell ()) where
-  file f = liftIO . writeFile f
-
-instance File (Shell String) where
-  file f = liftIO $ readFile f
-
--- | Lazy counterpart to monadic bind. To stream data from a command 'a' to a
---   command 'b', do 'a |> b'.
-(|>) :: Shell String -> (String -> Shell a) -> Shell a
-(Shell m) |> f = Shell $ \env -> do
-  (x, pids) <- m env
-  (x', pids') <- case x of
-    Left err -> return (Left err, [])
-    Right x' -> unSh (f x') env
-  return (x', pids ++ pids')
-infixl 1 |>
-
--- | Wait for all processes in the given list. If a process has failed, its
---   error message is returned and the rest are killed.
-waitPids :: [Pid] -> IO (Maybe String)
-waitPids (p:ps) = do
-  exCode <- Proc.waitForProcess (pidHandle p)
-  case exCode of
-    Exit.ExitFailure ec -> do
-      killPids ps
-      return . Just $ "Command '" ++ (pidName p) ++ "' failed with error "
-                    ++" code " ++ show ec
-    _ -> do
-      waitPids ps
-waitPids _ = do
-  return Nothing
-
--- | Kill all processes in the list.
-killPids :: [Pid] -> IO ()
-killPids = mapM_ (Proc.terminateProcess . pidHandle)
-
--- | Run a Shell computation. The program's working directory  will be restored 
---   after executing the computation.
-shell :: Shell a -> IO (Either String a)
-shell act = do
-  dir <- Dir.getCurrentDirectory
-  env <- Env.getEnvironment
-  (res, pids) <- unSh act env
-  merr <- waitPids pids
-  Dir.setCurrentDirectory dir
-  case merr of
-    Just err -> return $ Left err
-    _        -> return res
-
--- | Run a computation with a new value for an environment variable.
---   Note that this will *not* affect external commands spawned using @liftIO@
---   or which directory is considered the system temp directory.
-withEnv :: String -> (String -> String) -> Shell a -> Shell a
-withEnv key f (Shell act) = Shell $ \env -> do
-    act $ insert env
-  where
-    insert (x@(k,v):xs) | k == key  = (key, f v) : xs
-                        | otherwise = x : insert xs
-    insert _                        = [(key, f "")]
-
--- | Get the value of an environment variable. Returns Nothing if the variable 
---   doesn't exist.
-lookupEnv :: String -> Shell (Maybe String)
-lookupEnv key = Shell $ \env -> return (Right $ lookup key env, [])
-
--- | Get the value of an environment variable. Returns the empty string if
---   the variable doesn't exist.
-getEnv :: String -> Shell String
-getEnv key = maybe "" id `fmap` lookupEnv key
-
--- | General exception handler; any exception causes failure.
-exHandler :: Ex.SomeException -> IO (Either String a, [Pid])
-exHandler x = return (Left $ show x, [])
-
--- | Execute an external command. No globbing, escaping or other external shell
---   magic is performed on either the command or arguments. The program's text
---   output will be returned, and not echoed to the screen.
-run :: FilePath -> [String] -> String -> Shell String
-run p args stdin = do
-  (Just inp, Just out, pid) <- runP p args Proc.CreatePipe Proc.CreatePipe
-  Shell $ \_ -> do
-    let feed str = do
-          case splitAt 4096 str of
-            ([], [])      -> IO.hClose inp
-            (first, str') -> IO.hPutStr inp first >> feed str'
-    Conc.forkIO $ feed stdin
-    s <- IO.hGetContents out
-    s `seq` return (Right s, [Pid p pid])
-
--- | Run a program and return a boolean indicating whether the command
---   succeeded, the output from stdout, and the output from stderr.
---   This command will never fail.
-genericRun :: FilePath -> [String] -> String -> Shell (Bool, String, String)
-genericRun p args stdin = do
-    (Just inp, Just out, Just err, pid) <- createproc
-    Shell $ \_ -> do
-      let feed str = do
-            case splitAt 4096 str of
-              ([], [])      -> IO.hClose inp
-              (first, str') -> IO.hPutStr inp first >> feed str'
-      Conc.forkIO $ feed stdin
-      o <- IO.hGetContents out
-      e <- IO.hGetContents err
-      merr <- waitPids [Pid p pid]
-      return (Right (maybe True (const False) merr, o, e), [])
-  where
-    createproc = Shell $ \env -> do
-      (inp, out, err, pid) <- Proc.createProcess (cproc env)
-      return (Right (inp, out, err, pid), [])
-    cproc env = Proc.CreateProcess {
-        Proc.cmdspec      = Proc.RawCommand p args,
-        Proc.cwd          = Nothing,
-        Proc.env          = Just env,
-        Proc.std_in       = Proc.CreatePipe,
-        Proc.std_out      = Proc.CreatePipe,
-        Proc.std_err      = Proc.CreatePipe,
-        Proc.close_fds    = False,
-#if MIN_VERSION_process(1,2,0)
-        Proc.delegate_ctlc = False,
-#endif
-        Proc.create_group = False
-      }
-
-
--- | Like @run@, but echoes the command's text output to the screen instead of
---   returning it.
-run_ :: FilePath -> [String] -> String -> Shell ()
-run_ p args stdin = do
-  (Just inp, _, pid) <- runP p args Proc.CreatePipe Proc.Inherit
-  exCode <- liftIO $ do
-    IO.hPutStr inp stdin
-    IO.hClose inp
-    Proc.waitForProcess pid
-  case exCode of
-    Exit.ExitFailure ec -> fail $ "Command '" ++ p ++ "' failed with error " 
-                                ++" code " ++ show ec
-    _                   -> return ()
-
--- | Run a command with elevated privileges.
-sudo :: FilePath -> [String] -> String -> Shell String
-sudo cmd args stdin = run "sudo" (cmd:args) stdin
-
--- | Create a process. Helper for @run@ and friends.
-runP :: String
-     -> [String]
-     -> Proc.StdStream
-     -> Proc.StdStream
-     -> Shell (Maybe IO.Handle, Maybe IO.Handle, Proc.ProcessHandle)
-runP p args stdin stdout = Shell $ \env -> do
-    (inp, out, _, pid) <- Proc.createProcess (cproc env)
-    return (Right (inp, out, pid), [])
-  where
-    cproc env = Proc.CreateProcess {
-        Proc.cmdspec      = Proc.RawCommand p args,
-        Proc.cwd          = Nothing,
-        Proc.env          = Just env,
-        Proc.std_in       = stdin,
-        Proc.std_out      = stdout,
-        Proc.std_err      = Proc.Inherit,
-        Proc.close_fds    = False,
-#if MIN_VERSION_process(1,2,0)
-        Proc.delegate_ctlc = False,
-#endif
-        Proc.create_group = False
-      }
-
--- | Run an interactive process.
-runInteractive :: FilePath -> [String] -> Shell ()
-runInteractive p args = do
-  (_, _, pid) <- runP p args Proc.Inherit Proc.Inherit
-  exitCode <- liftIO $ Proc.waitForProcess pid
-  case exitCode of
-    Exit.ExitFailure ec -> fail (show ec)
-    _                   -> return ()
-
--- | Change working directory.
-cd :: FilePath -> Shell ()
-cd = liftIO . Dir.setCurrentDirectory
-
--- | Get the current working directory.
-pwd :: Shell FilePath
-pwd = liftIO $ Dir.getCurrentDirectory
-
--- | Remove a file.
-rm :: FilePath -> Shell ()
-rm = liftIO . Dir.removeFile
-
--- | Rename a file.
-mv :: FilePath -> FilePath -> Shell ()
-mv from to = liftIO $ Dir.renameFile from to
-
--- | Recursively copy a directory. If the target is a directory that already
---   exists, the source directory is copied into that directory using its
---   current name.
-cpDir :: FilePath -> FilePath -> Shell ()
-cpDir from to = do
-  todir <- isDirectory to
-  if todir
-    then do
-      let to' = to </> takeBaseName from
-      todir' <- isDirectory to'
-      if todir'
-        then do
-          files <- ls from
-          forM_ files $ \f -> cpDir (from </> f) (to' </> f)
-        else do
-          cpDir from to'
-    else do
-      cpfile <- isFile from
-      if cpfile
-        then do
-          cp from to
-        else do
-          liftIO $ Dir.createDirectoryIfMissing False to
-          ls from >>= mapM_ (\f -> cpDir (from </> f) (to </> f))
-
--- | Recursively copy a directory, but omit all files that do not match the
---   give predicate.
-cpFiltered :: (FilePath -> Bool) -> FilePath -> FilePath -> Shell ()
-cpFiltered pred from to = do
-  isdir <- isDirectory to
-  -- Lazily create directories only when needed!
-  let to' = unsafePerformIO $ do
-        when (not isdir) $ Dir.createDirectory to
-        return to
-  files <- ls from
-  mapM_ ((`cp` to') . (from </>)) (filter pred files)
-  fromdirs <- filterM (\d -> isDirectory (from </> d)) files
-  forM_ fromdirs $ \dir -> do
-    cpFiltered pred (from </> dir) (to </> dir)
-
--- | Perform an action on each file in the given directory.
---   This function will traverse any subdirectories of the given as well.
---   File paths are given relative to the given directory; the current working
---   directory is not affected.
-forEachFile :: FilePath -> (FilePath -> Shell a) -> Shell [a]
-forEachFile dir f = do
-  echo $ "forEachFile in dir " ++ dir
-  files <- map (dir </>) <$> ls dir
-  xs <- filterM isFile files >>= mapM f
-  fromdirs <- filterM isDirectory files
-  xss <- forM fromdirs $ \d -> do
-    forEachFile d f
-  return $ concat (xs:xss)
-
--- | Copy a file. Fails if the source is a directory. If the target is a
---   directory, the source file is copied into that directory using its current
---   name.
-cp :: FilePath -> FilePath -> Shell ()
-cp from to = do
-  todir <- isDirectory to
-  if todir
-    then cp from (to </> takeFileName from)
-    else liftIO $ Dir.copyFile from to
-
--- | List the contents of a directory, sans '.' and '..'.
-ls :: FilePath -> Shell [FilePath]
-ls dir = do
-  contents <- liftIO $ Dir.getDirectoryContents dir
-  return [f | f <- contents, f /= ".", f /= ".."]
-
--- | Create a directory. Optionally create any required missing directories as
---   well.
-mkdir :: Bool -> FilePath -> Shell ()
-mkdir True = liftIO . Dir.createDirectoryIfMissing True
-mkdir _    = liftIO . Dir.createDirectory
-
--- | Recursively remove a directory. Follows symlinks, so be careful.
-rmdir :: FilePath -> Shell ()
-rmdir = liftIO . Dir.removeDirectoryRecursive
-
--- | Do something with the user's home directory.
-withHomeDirectory :: (FilePath -> Shell a) -> Shell a
-withHomeDirectory act = liftIO Dir.getHomeDirectory >>= act
-
--- | Do something *in* the user's home directory.
-inHomeDirectory :: Shell a -> Shell a
-inHomeDirectory act = withHomeDirectory $ \dir -> inDirectory dir act
-
--- | Do something with the given application's data directory.
-withAppDirectory :: String -> (FilePath -> Shell a) -> Shell a
-withAppDirectory app act = liftIO (Dir.getAppUserDataDirectory app) >>= act
-
--- | Do something *in* the given application's data directory.
-inAppDirectory :: FilePath -> Shell a -> Shell a
-inAppDirectory app act = withAppDirectory app $ \dir -> inDirectory dir act
-
--- | Execute a command in the given working directory, then restore the
---   previous working directory.
-inDirectory :: FilePath -> Shell a -> Shell a
-inDirectory dir act = do
-  curDir <- pwd
-  cd dir
-  x <- act
-  cd curDir
-  return x
-
--- | Does the given path lead to a directory?
-isDirectory :: FilePath -> Shell Bool
-isDirectory = liftIO . Dir.doesDirectoryExist
-
--- | Does the given path lead to a file?
-isFile :: FilePath -> Shell Bool
-isFile = liftIO . Dir.doesFileExist
-
--- | Create a temp directory in the standard system temp directory, do
---   something with it, then remove it.
-withTempDirectory :: String -> (FilePath -> Shell a) -> Shell a
-withTempDirectory template act = Shell $ \env -> do
-    Temp.withSystemTempDirectory template (act' env)
-  where
-    act' env fp = Ex.catch (unSh (act fp) env) exHandler
-
--- | Create a temp directory in given directory, do something with it, then
---   remove it.
-withCustomTempDirectory :: FilePath -> (FilePath -> Shell a) -> Shell a
-withCustomTempDirectory dir act = Shell $ \env -> do
-    Temp.withTempDirectory dir "shellmate" (act' env)
-  where
-    act' env fp = Ex.catch (unSh (act fp) env) exHandler
-
--- | Performs a command inside a temporary directory. The directory will be
---   cleaned up after the command finishes.
-inTempDirectory :: Shell a -> Shell a
-inTempDirectory = withTempDirectory "shellmate" . flip inDirectory
-
--- | Create a temp file in the standard system temp directory, do something
---   with it, then remove it.
-withTempFile :: String -> (FilePath -> IO.Handle -> Shell a) -> Shell a
-withTempFile template act = Shell $ \env -> do
-    Temp.withSystemTempFile template (act' env)
-  where
-    act' env fp h = Ex.catch (unSh (act fp h) env) exHandler
-
--- | Create a temp file in the standard system temp directory, do something
---   with it, then remove it.
-withCustomTempFile :: FilePath -> (FilePath -> IO.Handle -> Shell a) -> Shell a
-withCustomTempFile dir act = Shell $ \env -> do
-    Temp.withTempFile dir "shellmate" (act' env)
-  where
-    act' env fp h = Ex.catch (unSh (act fp h) env) exHandler
-
--- | Perform an action that may fail without aborting the entire computation.
---   Forces serialization.
-mayFail :: Shell a -> Shell (Either String a)
-mayFail (Shell act) = Shell $ \env -> do
-  (x, pids) <- Ex.catch (act env) exHandler
-  merr <- waitPids pids
-  case merr of
-    Just err -> return (Right (Left err), [])
-    _        -> return (Right x, [])
-
--- | Attempt to run the first command. If the first command fails, run the
---   second. Forces serialization of the first command.
-orElse :: Shell a -> Shell a -> Shell a
-orElse a b = do
-  ex <- mayFail a
-  case ex of
-    Right x -> return x
-    _       -> b
-
--- | @IO.hPutStr@ lifted into Shell for convenience.
-hPutStr :: IO.Handle -> String -> Shell ()
-hPutStr h s = liftIO $ IO.hPutStr h s
-
--- | @IO.hPutStrLn@ lifted into Shell for convenience.
-hPutStrLn :: IO.Handle -> String -> Shell ()
-hPutStrLn h s = liftIO $ IO.hPutStrLn h s
-
--- | @putStrLn@ lifted into Shell for convenience.
-echo :: String -> Shell ()
-echo = liftIO . putStrLn
-
-class Guard guard a | guard -> a where
-  -- | Perform a Shell computation; if the computation succeeds but returns
-  --   a false-ish value, the outer Shell computation fails with the given
-  --   error message.
-  guard :: String -> guard -> Shell a
-
-instance Guard (Maybe a) a where
-  guard _ (Just x) = return x
-  guard desc _     = fail $ "Guard failed: " ++ desc
-
-instance Guard Bool () where
-  guard _ True = return ()
-  guard desc _ = fail $ "Guard failed: " ++ desc
-
-instance Guard a b => Guard (Shell a) b where
-  guard desc m = m >>= \x -> guard desc x
+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, DeriveDataTypeable,+             MultiParamTypeClasses, FunctionalDependencies,+             CPP,+             UndecidableInstances #-}+-- | Simple interface for shell scripting-like tasks.+module Control.Shell ( +    Shell,+    Guard (..),+    File (..),+    shell,+    mayFail, orElse,+    withEnv, getEnv, lookupEnv,+    run, run_, runInteractive, genericRun, sudo,+    cd, cpDir, pwd, ls, mkdir, rmdir, inDirectory, isDirectory,+    withHomeDirectory, inHomeDirectory, withAppDirectory, inAppDirectory,+    forEachFile, forEachFile_, cpFiltered,+    isFile, rm, mv, cp,+    withTempFile, withCustomTempFile,+    withTempDirectory, withCustomTempDirectory, inTempDirectory,+    hPutStr, hPutStrLn, hClose, echo,+    (|>),+    module System.FilePath, liftIO+  ) where+import Control.Applicative+import Control.Monad (ap, forM, filterM, forM_, when)+import Control.Monad.IO.Class+import Data.Time.Clock+import Data.Typeable+import System.FilePath+import System.IO.Unsafe+import qualified System.Process as Proc+import qualified System.Directory as Dir+import qualified System.Exit as Exit+import qualified System.IO as IO+import qualified Control.Exception as Ex+import qualified Control.Concurrent as Conc+import qualified System.IO.Temp as Temp+import qualified System.Environment as Env++newtype ShellException = ShellException String deriving (Typeable, Show)+instance Ex.Exception ShellException++-- | Environment variables.+type Env = [(String, String)]++-- | A command name plus a ProcessHandle.+data Pid = Pid {pidName :: String, pidHandle :: Proc.ProcessHandle}++-- | Monad for running shell commands. If a command fails, the entire+--   computation is aborted unless @mayFail@ is used.+newtype Shell a = Shell {+    unSh :: Env -> IO (Either String a, [Pid])+  }++instance Monad Shell where+  fail err = Shell $ \_ -> return (Left err, [])+  return x = Shell $ \_ -> return (Right x, [])+  -- | The bind operation of the Shell monad is effectively a barrier; all+  --   commands on the left hand side of a bind will complete before any+  --   command on the right hand side is attempted.+  --   To lazily stream data between two commands, use the @|>@ combinator.+  (Shell m) >>= f = Shell $ \env -> do+    (x, pids) <- m env+    merr <- waitPids pids+    case (x, merr) of+      (Left err, _) -> return (Left err, [])+      (_, Just err) -> return (Left err, [])+      (Right x', _) -> unSh (f x') env++instance MonadIO Shell where+  liftIO act = Shell $ \_ -> flip Ex.catch exHandler $ do+    x <- act+    return (Right x, [])++instance Applicative Shell where+  pure  = return+  (<*>) = ap++instance Functor Shell where+  fmap f x = do+    x' <- x+    return $! f x'++-- | Enable the user to use 'file "foo" |> fmap reverse |> file "bar"' as+--   syntax for reading the file 'foo', reversing the contents and writing the+--   result to 'bar'.+--   Note that this uses lazy IO in the form of read/writeFile.+class File a where+  file :: FilePath -> a++instance File (String -> Shell ()) where+  file f = liftIO . writeFile f++instance File (Shell String) where+  file f = liftIO $ readFile f++-- | Lazy counterpart to monadic bind. To stream data from a command 'a' to a+--   command 'b', do 'a |> b'.+(|>) :: Shell String -> (String -> Shell a) -> Shell a+(Shell m) |> f = Shell $ \env -> do+  (x, pids) <- m env+  (x', pids') <- case x of+    Left err -> return (Left err, [])+    Right x' -> unSh (f x') env+  return (x', pids ++ pids')+infixl 1 |>++-- | Wait for all processes in the given list. If a process has failed, its+--   error message is returned and the rest are killed.+waitPids :: [Pid] -> IO (Maybe String)+waitPids (p:ps) = do+  exCode <- Proc.waitForProcess (pidHandle p)+  case exCode of+    Exit.ExitFailure ec -> do+      killPids ps+      return . Just $ "Command '" ++ (pidName p) ++ "' failed with error "+                    ++" code " ++ show ec+    _ -> do+      waitPids ps+waitPids _ = do+  return Nothing++-- | Kill all processes in the list.+killPids :: [Pid] -> IO ()+killPids = mapM_ (Proc.terminateProcess . pidHandle)++-- | Run a Shell computation. The program's working directory  will be restored +--   after executing the computation.+shell :: Shell a -> IO (Either String a)+shell act = do+  dir <- Dir.getCurrentDirectory+  env <- Env.getEnvironment+  (res, pids) <- unSh act env+  merr <- waitPids pids+  Dir.setCurrentDirectory dir+  case merr of+    Just err -> return $ Left err+    _        -> return res++-- | Run a computation with a new value for an environment variable.+--   Note that this will *not* affect external commands spawned using @liftIO@+--   or which directory is considered the system temp directory.+withEnv :: String -> (String -> String) -> Shell a -> Shell a+withEnv key f (Shell act) = Shell $ \env -> do+    act $ insert env+  where+    insert (x@(k,v):xs) | k == key  = (key, f v) : xs+                        | otherwise = x : insert xs+    insert _                        = [(key, f "")]++-- | Get the value of an environment variable. Returns Nothing if the variable +--   doesn't exist.+lookupEnv :: String -> Shell (Maybe String)+lookupEnv key = Shell $ \env -> return (Right $ lookup key env, [])++-- | Get the value of an environment variable. Returns the empty string if+--   the variable doesn't exist.+getEnv :: String -> Shell String+getEnv key = maybe "" id `fmap` lookupEnv key++-- | General exception handler; any exception causes failure.+exHandler :: Ex.SomeException -> IO (Either String a, [Pid])+exHandler x = return (Left $ show x, [])++-- | Execute an external command. No globbing, escaping or other external shell+--   magic is performed on either the command or arguments. The program's text+--   output will be returned, and not echoed to the screen.+run :: FilePath -> [String] -> String -> Shell String+run p args stdin = do+  (Just inp, Just out, pid) <- runP p args Proc.CreatePipe Proc.CreatePipe+  Shell $ \_ -> do+    let feed str = do+          case splitAt 4096 str of+            ([], [])      -> IO.hClose inp+            (first, str') -> IO.hPutStr inp first >> feed str'+    Conc.forkIO $ feed stdin+    s <- IO.hGetContents out+    s `seq` return (Right s, [Pid p pid])++-- | Run a program and return a boolean indicating whether the command+--   succeeded, the output from stdout, and the output from stderr.+--   This command will never fail.+genericRun :: FilePath -> [String] -> String -> Shell (Bool, String, String)+genericRun p args stdin = do+    (Just inp, Just out, Just err, pid) <- createproc+    Shell $ \_ -> do+      let feed str = do+            case splitAt 4096 str of+              ([], [])      -> IO.hClose inp+              (first, str') -> IO.hPutStr inp first >> feed str'+      Conc.forkIO $ feed stdin+      o <- IO.hGetContents out+      e <- IO.hGetContents err+      merr <- waitPids [Pid p pid]+      return (Right (maybe True (const False) merr, o, e), [])+  where+    createproc = Shell $ \env -> do+      (inp, out, err, pid) <- Proc.createProcess (cproc env)+      return (Right (inp, out, err, pid), [])+    cproc env = Proc.CreateProcess {+        Proc.cmdspec      = Proc.RawCommand p args,+        Proc.cwd          = Nothing,+        Proc.env          = Just env,+        Proc.std_in       = Proc.CreatePipe,+        Proc.std_out      = Proc.CreatePipe,+        Proc.std_err      = Proc.CreatePipe,+        Proc.close_fds    = False,+#if MIN_VERSION_process(1,2,0)+        Proc.delegate_ctlc = False,+#endif+        Proc.create_group = False+      }+++-- | Like @run@, but echoes the command's text output to the screen instead of+--   returning it.+run_ :: FilePath -> [String] -> String -> Shell ()+run_ p args stdin = do+  (Just inp, _, pid) <- runP p args Proc.CreatePipe Proc.Inherit+  exCode <- liftIO $ do+    IO.hPutStr inp stdin+    IO.hClose inp+    Proc.waitForProcess pid+  case exCode of+    Exit.ExitFailure ec -> fail $ "Command '" ++ p ++ "' failed with error " +                                ++" code " ++ show ec+    _                   -> return ()++-- | Run a command with elevated privileges.+sudo :: FilePath -> [String] -> String -> Shell String+sudo cmd args stdin = run "sudo" (cmd:args) stdin++-- | Create a process. Helper for @run@ and friends.+runP :: String+     -> [String]+     -> Proc.StdStream+     -> Proc.StdStream+     -> Shell (Maybe IO.Handle, Maybe IO.Handle, Proc.ProcessHandle)+runP p args stdin stdout = Shell $ \env -> do+    (inp, out, _, pid) <- Proc.createProcess (cproc env)+    return (Right (inp, out, pid), [])+  where+    cproc env = Proc.CreateProcess {+        Proc.cmdspec      = Proc.RawCommand p args,+        Proc.cwd          = Nothing,+        Proc.env          = Just env,+        Proc.std_in       = stdin,+        Proc.std_out      = stdout,+        Proc.std_err      = Proc.Inherit,+        Proc.close_fds    = False,+#if MIN_VERSION_process(1,2,0)+        Proc.delegate_ctlc = False,+#endif+        Proc.create_group = False+      }++-- | Run an interactive process.+runInteractive :: FilePath -> [String] -> Shell ()+runInteractive p args = do+  (_, _, pid) <- runP p args Proc.Inherit Proc.Inherit+  exitCode <- liftIO $ Proc.waitForProcess pid+  case exitCode of+    Exit.ExitFailure ec -> fail (show ec)+    _                   -> return ()++-- | Change working directory.+cd :: FilePath -> Shell ()+cd = liftIO . Dir.setCurrentDirectory++-- | Get the current working directory.+pwd :: Shell FilePath+pwd = liftIO $ Dir.getCurrentDirectory++-- | Remove a file.+rm :: FilePath -> Shell ()+rm = liftIO . Dir.removeFile++-- | Rename a file.+mv :: FilePath -> FilePath -> Shell ()+mv from to = liftIO $ Dir.renameFile from to++-- | Recursively copy a directory. If the target is a directory that already+--   exists, the source directory is copied into that directory using its+--   current name.+cpDir :: FilePath -> FilePath -> Shell ()+cpDir from to = do+  todir <- isDirectory to+  if todir+    then do+      cpDir from (to </> takeBaseName from)+    else do+      cpfile <- isFile from+      if cpfile+        then do+          cp from to+        else do+          liftIO $ Dir.createDirectoryIfMissing False to+          ls from >>= mapM_ (\f -> cpDir (from </> f) (to </> f))++-- | Recursively copy a directory, but omit all files that do not match the+--   give predicate.+cpFiltered :: (FilePath -> Bool) -> FilePath -> FilePath -> Shell ()+cpFiltered pred from to = do+  isdir <- isDirectory to+  -- Lazily create directories only when needed!+  let to' = unsafePerformIO $ do+        when (not isdir) $ Dir.createDirectory to+        return to+  files <- ls from+  mapM_ ((`cp` to') . (from </>)) (filter pred files)+  fromdirs <- filterM (\d -> isDirectory (from </> d)) files+  forM_ fromdirs $ \dir -> do+    cpFiltered pred (from </> dir) (to </> dir)++-- | Perform an action on each file in the given directory.+--   This function will traverse any subdirectories of the given as well.+--   File paths are given relative to the given directory; the current working+--   directory is not affected.+forEachFile :: FilePath -> (FilePath -> Shell a) -> Shell [a]+forEachFile dir f = do+  files <- map (dir </>) <$> ls dir+  xs <- filterM isFile files >>= mapM f+  fromdirs <- filterM isDirectory files+  xss <- forM fromdirs $ \d -> do+    forEachFile d f+  return $ concat (xs:xss)++-- | Like @forEachFile@ but only performs a side effect.+forEachFile_ :: FilePath -> (FilePath -> Shell ()) -> Shell ()+forEachFile_ dir f = do+  files <- map (dir </>) <$> ls dir+  filterM isFile files >>= mapM_ f+  fromdirs <- filterM isDirectory files+  forM_ fromdirs $ \d -> do+    forEachFile d f++-- | Copy a file. Fails if the source is a directory. If the target is a+--   directory, the source file is copied into that directory using its current+--   name.+cp :: FilePath -> FilePath -> Shell ()+cp from to = do+  todir <- isDirectory to+  if todir+    then cp from (to </> takeFileName from)+    else liftIO $ Dir.copyFile from to++-- | List the contents of a directory, sans '.' and '..'.+ls :: FilePath -> Shell [FilePath]+ls dir = do+  contents <- liftIO $ Dir.getDirectoryContents dir+  return [f | f <- contents, f /= ".", f /= ".."]++-- | Create a directory. Optionally create any required missing directories as+--   well.+mkdir :: Bool -> FilePath -> Shell ()+mkdir True = liftIO . Dir.createDirectoryIfMissing True+mkdir _    = liftIO . Dir.createDirectory++-- | Recursively remove a directory. Follows symlinks, so be careful.+rmdir :: FilePath -> Shell ()+rmdir = liftIO . Dir.removeDirectoryRecursive++-- | Do something with the user's home directory.+withHomeDirectory :: (FilePath -> Shell a) -> Shell a+withHomeDirectory act = liftIO Dir.getHomeDirectory >>= act++-- | Do something *in* the user's home directory.+inHomeDirectory :: Shell a -> Shell a+inHomeDirectory act = withHomeDirectory $ \dir -> inDirectory dir act++-- | Do something with the given application's data directory.+withAppDirectory :: String -> (FilePath -> Shell a) -> Shell a+withAppDirectory app act = liftIO (Dir.getAppUserDataDirectory app) >>= act++-- | Do something *in* the given application's data directory.+inAppDirectory :: FilePath -> Shell a -> Shell a+inAppDirectory app act = withAppDirectory app $ \dir -> inDirectory dir act++-- | Execute a command in the given working directory, then restore the+--   previous working directory.+inDirectory :: FilePath -> Shell a -> Shell a+inDirectory dir act = do+  curDir <- pwd+  cd dir+  x <- act+  cd curDir+  return x++-- | Does the given path lead to a directory?+isDirectory :: FilePath -> Shell Bool+isDirectory = liftIO . Dir.doesDirectoryExist++-- | Does the given path lead to a file?+isFile :: FilePath -> Shell Bool+isFile = liftIO . Dir.doesFileExist++-- | Create a temp directory in the standard system temp directory, do+--   something with it, then remove it.+withTempDirectory :: String -> (FilePath -> Shell a) -> Shell a+withTempDirectory template act = Shell $ \env -> do+    Temp.withSystemTempDirectory template (act' env)+  where+    act' env fp = Ex.catch (unSh (act fp) env) exHandler++-- | Create a temp directory in given directory, do something with it, then+--   remove it.+withCustomTempDirectory :: FilePath -> (FilePath -> Shell a) -> Shell a+withCustomTempDirectory dir act = Shell $ \env -> do+    Temp.withTempDirectory dir "shellmate" (act' env)+  where+    act' env fp = Ex.catch (unSh (act fp) env) exHandler++-- | Performs a command inside a temporary directory. The directory will be+--   cleaned up after the command finishes.+inTempDirectory :: Shell a -> Shell a+inTempDirectory = withTempDirectory "shellmate" . flip inDirectory++-- | Create a temp file in the standard system temp directory, do something+--   with it, then remove it.+withTempFile :: String -> (FilePath -> IO.Handle -> Shell a) -> Shell a+withTempFile template act = Shell $ \env -> do+    Temp.withSystemTempFile template (act' env)+  where+    act' env fp h = Ex.catch (unSh (act fp h) env) exHandler++-- | Create a temp file in the standard system temp directory, do something+--   with it, then remove it.+withCustomTempFile :: FilePath -> (FilePath -> IO.Handle -> Shell a) -> Shell a+withCustomTempFile dir act = Shell $ \env -> do+    Temp.withTempFile dir "shellmate" (act' env)+  where+    act' env fp h = Ex.catch (unSh (act fp h) env) exHandler++-- | Perform an action that may fail without aborting the entire computation.+--   Forces serialization.+mayFail :: Shell a -> Shell (Either String a)+mayFail (Shell act) = Shell $ \env -> do+  (x, pids) <- Ex.catch (act env) exHandler+  merr <- waitPids pids+  case merr of+    Just err -> return (Right (Left err), [])+    _        -> return (Right x, [])++-- | Attempt to run the first command. If the first command fails, run the+--   second. Forces serialization of the first command.+orElse :: Shell a -> Shell a -> Shell a+orElse a b = do+  ex <- mayFail a+  case ex of+    Right x -> return x+    _       -> b++-- | @IO.hPutStr@ lifted into Shell for convenience.+hPutStr :: IO.Handle -> String -> Shell ()+hPutStr h s = liftIO $ IO.hPutStr h s++-- | @IO.hPutStrLn@ lifted into Shell for convenience.+hPutStrLn :: IO.Handle -> String -> Shell ()+hPutStrLn h s = liftIO $ IO.hPutStrLn h s++-- | @IO.hClose@ lifted into Shell for convenience.+hClose :: IO.Handle -> Shell ()+hClose h = liftIO $ IO.hClose h++-- | @putStrLn@ lifted into Shell for convenience.+echo :: String -> Shell ()+echo = liftIO . putStrLn++class Guard guard a | guard -> a where+  -- | Perform a Shell computation; if the computation succeeds but returns+  --   a false-ish value, the outer Shell computation fails with the given+  --   error message.+  guard :: String -> guard -> Shell a++instance Guard (Maybe a) a where+  guard _ (Just x) = return x+  guard desc _     = fail $ "Guard failed: " ++ desc++instance Guard Bool () where+  guard _ True = return ()+  guard desc _ = fail $ "Guard failed: " ++ desc++instance Guard a b => Guard (Shell a) b where+  guard desc m = m >>= \x -> guard desc x
LICENSE view
@@ -1,30 +1,30 @@-Copyright (c) 2014, Anton Ekblad
-
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-    * Redistributions of source code must retain the above copyright
-      notice, this list of conditions and the following disclaimer.
-
-    * Redistributions in binary form must reproduce the above
-      copyright notice, this list of conditions and the following
-      disclaimer in the documentation and/or other materials provided
-      with the distribution.
-
-    * Neither the name of Anton Ekblad nor the names of other
-      contributors may be used to endorse or promote products derived
-      from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+Copyright (c) 2014, Anton Ekblad++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Anton Ekblad nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Setup.hs view
@@ -1,2 +1,2 @@-import Distribution.Simple
-main = defaultMain
+import Distribution.Simple+main = defaultMain
shellmate.cabal view
@@ -1,31 +1,31 @@-name:                shellmate
-version:             0.1.5
-synopsis:            Simple interface for shell scripting in Haskell.
-description:         Aims to simplify development of cross-platform shell scripts and similar things.
-homepage:            http://github.com/valderman/shellmate
-license:             BSD3
-license-file:        LICENSE
-author:              Anton Ekblad
-maintainer:          anton@ekblad.cc
--- copyright:           
-category:            System
-build-type:          Simple
-cabal-version:       >=1.8
-bug-reports:         http://github.com/valderman/shellmate/issues
-
-source-repository head
-    type:       git
-    location:   https://github.com/valderman/shellmate.git
-
-library
-  exposed-modules:
-    Control.Shell
-  -- other-modules:       
-  build-depends:
-    base >=4.5 && <5,
-    transformers >=0.3,
-    time,
-    filepath,
-    process,
-    directory,
-    temporary
+name:                shellmate+version:             0.1.6+synopsis:            Simple interface for shell scripting in Haskell.+description:         Aims to simplify development of cross-platform shell scripts and similar things.+homepage:            http://github.com/valderman/shellmate+license:             BSD3+license-file:        LICENSE+author:              Anton Ekblad+maintainer:          anton@ekblad.cc+-- copyright:           +category:            System+build-type:          Simple+cabal-version:       >=1.8+bug-reports:         http://github.com/valderman/shellmate/issues++source-repository head+    type:       git+    location:   https://github.com/valderman/shellmate.git++library+  exposed-modules:+    Control.Shell+  -- other-modules:       +  build-depends:+    base >=4.5 && <5,+    transformers >=0.3,+    time,+    filepath,+    process,+    directory,+    temporary