diff --git a/Control/Shell.hs b/Control/Shell.hs
--- a/Control/Shell.hs
+++ b/Control/Shell.hs
@@ -1,60 +1,66 @@
 {-# LANGUAGE TypeFamilies, CPP #-}
 -- | Simple interface for shell scripting-like tasks.
-module Control.Shell (
-    -- * Running Shell programs
-    Shell, ExitReason (..),
-    shell, shell_, exitString,
+module Control.Shell
+  ( -- * Running Shell programs
+    Shell, ExitReason (..)
+  , shell, shell_, exitString
 
     -- * Error handling and control flow
-    (|>),
-    try, orElse, exit,
-    Guard (..), guard, when, unless,
+  , (|>), capture, stream, lift
+  , try, orElse, exit
+  , Guard (..), guard, when, unless
 
     -- * Environment handling
-    setEnv, getEnv, withEnv, lookupEnv, cmdline,
+  , withEnv, withoutEnv, lookupEnv, getEnv, cmdline
 
-    -- * Running commands
-    MonadIO (..),
-    run, run_, genericRun, runInteractive, sudo,
+    -- * Running external commands
+  , MonadIO (..), Env (..)
+  , run, sudo
+  , unsafeLiftIO
+  , absPath, shellEnv, getShellEnv, joinResult, runSh
 
     -- * Working with directories
-    cd, cpdir, pwd, ls, mkdir, rmdir, inDirectory, isDirectory,
-    withHomeDirectory, inHomeDirectory, withAppDirectory, inAppDirectory,
-    forEachFile, forEachFile_, forEachDirectory, forEachDirectory_,
+  , cpdir, pwd, ls, mkdir, rmdir, inDirectory, isDirectory
+  , withHomeDirectory, inHomeDirectory, withAppDirectory, inAppDirectory
+  , forEachFile, forEachFile_, forEachDirectory, forEachDirectory_
 
     -- * Working with files
-    isFile, rm, mv, cp, input, output,
+  , isFile, rm, mv, cp, input, output
+  , withFile, withBinaryFile, openFile, openBinaryFile
 
     -- * Working with temporary files and directories
-    withTempFile, withCustomTempFile,
-    withTempDirectory, withCustomTempDirectory, inTempDirectory,
+  , FileMode
+  , withTempFile, withCustomTempFile
+  , withTempDirectory, withCustomTempDirectory
+  , inTempDirectory, inCustomTempDirectory
 
     -- * Working with handles
-    Handle, IOMode (..),
-    stdin, stdout, stderr,
-    hFlush, hClose, withFile, withBinaryFile, openFile, openBinaryFile,
+  , Handle, IOMode (..)
+  , hFlush, hClose
+  , getStdIn, getStdOut, getStdErr
 
     -- * Text I/O
-    hPutStr, hPutStrLn, echo, ask,
-    hGetLine, hGetContents,
+  , hPutStr, hPutStrLn, echo, echo_, ask, stdin
+  , hGetLine, hGetContents
 
     -- * ByteString I/O
-    hGetBytes, hPutBytes, hGetByteLine, hGetByteContents,
+  , hGetBytes, hPutBytes, hGetByteLine, hGetByteContents
 
     -- * Convenient re-exports
-    module System.FilePath,
-    module Control.Monad
+  , module System.FilePath
+  , module Control.Monad
   ) where
-#if __GLASGOW_HASKELL__ <= 708
-import Control.Applicative
-#endif
-import Control.Monad hiding (guard, when, unless)
-import System.FilePath
-import qualified System.Directory as Dir
 import qualified System.Environment as Env
 import System.IO.Unsafe
+import Control.Shell.Base hiding (getEnv, takeEnvLock, releaseEnvLock, setShellEnv)
+import qualified Control.Shell.Base as CSB
 import Control.Shell.Handle
-import Control.Shell.Internal
+import Control.Shell.File
+import Control.Shell.Directory
+import Control.Shell.Temp
+import Control.Shell.Control
+import Control.Monad hiding (guard, when, unless)
+import System.FilePath
 
 -- | Convert an 'ExitReason' into a 'String'. Successful termination yields
 --   the empty string, while abnormal termination yields the termination
@@ -66,37 +72,42 @@
 exitString (Failure "") = "abnormal termination"
 exitString (Failure s)  = s
 
--- | Lazily read a file.
-input :: FilePath -> Shell String
-input = liftIO . readFile
+-- | Get the complete environment for the current computation.
+getShellEnv :: Shell Env
+getShellEnv = CSB.getEnv
 
--- | Lazily write a file.
-output :: MonadIO m => FilePath -> String -> m ()
-output f = liftIO . writeFile f
+insert :: Eq k => k -> v -> [(k, v)] -> [(k, v)]
+insert k' v' (kv@(k, _) : kvs)
+  | k == k'   = (k', v') : kvs
+  | otherwise = kv : insert k' v' kvs
+insert _ _ _  = []
 
+delete :: Eq k => k -> [(k, v)] -> [(k, v)]
+delete k' (kv@(k, _) : kvs)
+  | k == k'   = kvs
+  | otherwise = kv : delete k' kvs
+delete _ _    = []
+
 -- | The executable's command line arguments.
 cmdline :: [String]
 cmdline = unsafePerformIO Env.getArgs
 
--- | Set an environment variable.
-setEnv :: MonadIO m => String -> String -> m ()
-setEnv k v = liftIO $ Env.setEnv k v
+-- | Run a computation with the given environment variable set.
+withEnv :: String -> String -> Shell a -> Shell a
+withEnv k v m = do
+  e <- CSB.getEnv
+  inEnv (e {envEnvVars = insert k v (envEnvVars e)}) m
 
+-- | Run a computation with the given environment variable unset.
+withoutEnv :: String -> Shell a -> Shell a
+withoutEnv k m = do
+  e <- CSB.getEnv
+  inEnv (e {envEnvVars = delete k (envEnvVars e)}) m
+
 -- | Get the value of an environment variable. Returns Nothing if the variable 
 --   doesn't exist.
 lookupEnv :: String -> Shell (Maybe String)
-lookupEnv = liftIO . Env.lookupEnv
-
--- | 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 act = do
-  v <- lookupEnv key
-  setEnv key $ f (maybe "" id v)
-  x <- act
-  setEnv key $ maybe "" id v
-  return x
+lookupEnv k = lookup k . envEnvVars <$> CSB.getEnv
 
 -- | Get the value of an environment variable. Returns the empty string if
 --   the variable doesn't exist.
@@ -104,232 +115,21 @@
 getEnv key = maybe "" id `fmap` lookupEnv key
 
 -- | Run a command with elevated privileges.
-sudo :: FilePath -> [String] -> String -> Shell String
-sudo cmd as = run "sudo" (cmd:as)
-
--- | Change working directory.
-cd :: MonadIO m => FilePath -> m ()
-cd = liftIO . Dir.setCurrentDirectory
-
--- | Get the current working directory.
-pwd :: MonadIO m => m FilePath
-pwd = liftIO $ Dir.getCurrentDirectory
-
--- | Remove a file.
-rm :: MonadIO m => FilePath -> m ()
-rm = liftIO . Dir.removeFile
-
--- | Rename a file.
-mv :: MonadIO m => FilePath -> FilePath -> m ()
-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 fromdir todir = do
-    assert ("`" ++ fromdir ++ "' is not a directory") (isDirectory fromdir)
-    exists <- isDirectory todir
-    if exists
-      then do
-        mkdir True (todir </> takeFileName fromdir)
-        go fromdir (todir </> takeFileName fromdir)
-      else mkdir True todir >> go fromdir todir
-  where
-    go from to = do
-      forEachDirectory_ from (\dir -> mkdir True (to </> dir))
-      forEachFile_ from $ \file -> do
-        let file' = to </> file
-        assert (errOverwrite file') (not <$> isDirectory file')
-        cp (from </> file) file'
-    errOverwrite f = "cannot overwrite directory `" ++ f
-                     ++ "' with non-directory"
-
--- | Recursively perform an action on each subdirectory of the given directory.
---   The path passed to the callback is relative to the given directory.
---   The action will *not* be performed on the given directory itself.
-forEachDirectory :: FilePath -> (FilePath -> Shell a) -> Shell [a]
-forEachDirectory root f = go ""
-  where
-    dir = if null root then "." else root
-    go subdir = do
-      let dir' = dir </> subdir
-      files <- ls dir'
-      fromdirs <- filterM (\d -> isDirectory (dir' </> d)) files
-      xs <- forM fromdirs $ \d -> do
-        let d' = subdir </> d
-        x <- f d'
-        (x:) <$> go d'
-      return (concat xs)
-
--- | Like 'forEachDirectory', but discards its result.
-forEachDirectory_ :: FilePath -> (FilePath -> Shell ()) -> Shell ()
-forEachDirectory_ root f = go ""
-  where
-    dir = if null root then "." else root
-    go subdir = do
-      let dir' = dir </> subdir
-      files <- ls dir'
-      fromdirs <- filterM (\d -> isDirectory (dir' </> d)) files
-      forM_ fromdirs $ \d -> let d' = subdir </> d in f d' >> go d'
-
--- | 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 root f = go ""
-  where
-    dir = if null root then "." else root
-    go subdir = do
-      let dir' = dir </> subdir
-      files <- ls dir'
-      
-      -- For each file in this directory...
-      onlyfiles <- filterM (\fl -> isFile (dir' </> fl)) files
-      xs <- mapM (\x -> f (subdir </> x)) onlyfiles
-
-      -- For each subdirectory...
-      fromdirs <- filterM (\fl -> isDirectory (dir' </> fl)) files
-      xss <- forM fromdirs $ \d -> do
-        go (subdir </> d)
-      return $ concat (xs:xss)
-
--- | Like @forEachFile@ but only performs a side effect.
-forEachFile_ :: FilePath -> (FilePath -> Shell ()) -> Shell ()
-forEachFile_ root f = go ""
-  where
-    dir = if null root then "." else root
-    go subdir = do
-      let dir' = dir </> subdir
-      files <- ls dir'
-      filterM (\fl -> isFile (dir' </> fl)) files >>= mapM_ (f . (subdir </>))
-      fromdirs <- filterM (\fl -> isDirectory (dir' </> fl)) files
-      forM_ fromdirs $ \d -> go (subdir </> d)
-
--- | 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 :: MonadIO m => Bool -> FilePath -> m ()
-mkdir True = liftIO . Dir.createDirectoryIfMissing True
-mkdir _    = liftIO . Dir.createDirectory
-
--- | Recursively remove a directory. Follows symlinks, so be careful.
-rmdir :: MonadIO m => FilePath -> m ()
-rmdir = liftIO . Dir.removeDirectoryRecursive
-
--- | Do something with the user's home directory.
-withHomeDirectory :: (FilePath -> Shell a) -> Shell a
-withHomeDirectory act = liftIO Dir.getHomeDirectory >>= act
-
--- | Perform an action with the user's home directory as the working directory.
-inHomeDirectory :: Shell a -> Shell a
-inHomeDirectory act = withHomeDirectory $ flip inDirectory 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 with the given application's data directory as the working
---   directory.
-inAppDirectory :: FilePath -> Shell a -> Shell a
-inAppDirectory app act = withAppDirectory app $ flip inDirectory 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
+sudo :: FilePath -> [String] -> Shell ()
+sudo cmd as = run "sudo" (cmd:"--":as)
 
 -- | 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
-
--- | 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 <- try a
-  case ex of
-    Right x -> return x
-    _       -> b
-
--- | Write a string to @stdout@ followed by a newline.
-echo :: MonadIO m => String -> m ()
-echo = liftIO . putStrLn
-
--- | Read one line of input from @stdin@.
-ask :: Shell String
-ask = liftIO getLine
-
-class Guard guard where
-  -- | The type of the guard's return value, if it succeeds.
-  type Result guard
-
-  -- | Perform a Shell computation; if the computation succeeds but returns
-  --   a false-ish value, the outer Shell computation fails with the given
-  --   error message.
-  assert :: String -> guard -> Shell (Result guard)
-
-instance Guard (Maybe a) where
-  type Result (Maybe a) = a
-  assert _ (Just x) = return x
-  assert ""  _      = fail $ "Guard failed!"
-  assert desc _     = fail desc
-
-instance Guard Bool where
-  type Result Bool = ()
-  assert _ True = return ()
-  assert ""  _  = fail $ "Guard failed!"
-  assert desc _ = fail desc
-
-instance Guard a => Guard (Shell a) where
-  type Result (Shell a) = Result a
-  assert desc m = m >>= \x -> assert desc x
-
--- | Perform a Shell computation; if the computation succeeds but returns
---   a false-ish value, the outer Shell computation fails.
-guard :: Guard g => g -> Shell (Result g)
-guard = assert ""
+inTempDirectory = withTempDirectory . flip inDirectory
 
--- | Perform the given computation if the given guard passes, otherwise do
---   nothing.
-when :: Guard g => g -> Shell a -> Shell ()
-when g m = do
-  res <- try (guard g)
-  case res of
-    Right _ -> void m
-    _       -> pure ()
+-- | Performs a command inside a temporary directory. The directory will be
+--   cleaned up after the command finishes.
+inCustomTempDirectory :: FilePath -> Shell a -> Shell a
+inCustomTempDirectory dir m = withCustomTempDirectory dir $ flip inDirectory m
 
--- | Perform the given computation if the given guard fails, otherwise do
---   nothing.
-unless :: Guard g => g -> Shell a -> Shell ()
-unless g m = void (guard g) `orElse` void m
+-- | Get the standard input, output and error handle respectively.
+getStdIn, getStdOut, getStdErr :: Shell Handle
+getStdIn  = envStdIn  <$> CSB.getEnv
+getStdOut = envStdOut <$> CSB.getEnv
+getStdErr = envStdErr <$> CSB.getEnv
diff --git a/Control/Shell/Base.hs b/Control/Shell/Base.hs
new file mode 100644
--- /dev/null
+++ b/Control/Shell/Base.hs
@@ -0,0 +1,129 @@
+-- | Basic operations such as reading input/output, etc.
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Control.Shell.Base
+  ( module Control.Shell.Internal
+  , module System.FilePath
+  , IO.Handle, FileMode (..)
+  , MonadIO (..), shellEnv
+  , shell_
+  , stdin, echo, echo_, ask
+  , capture, stream, lift
+  , takeEnvLock, releaseEnvLock, setShellEnv, joinResult, absPath
+  ) where
+import qualified System.Process as Proc
+import qualified System.IO as IO
+import qualified Control.Concurrent as Conc
+import qualified System.Directory as Dir
+import qualified System.Environment as Env
+import System.FilePath
+import Data.IORef
+import Control.Monad.IO.Class
+import System.IO.Unsafe
+import Control.Shell.Internal
+
+-- | Perform a file operation in binary or text mode?
+data FileMode = BinaryMode | TextMode
+  deriving (Show, Eq)
+
+-- | Create an absolute path from the environment and a potentially relative
+--   path. Has no effect if the path is already absolute.
+absPath :: Env -> FilePath -> FilePath
+absPath env fp
+  | isAbsolute fp = fp
+  | otherwise     = envWorkDir env </> fp
+
+{-# NOINLINE globalEnvLock #-}
+globalEnvLock :: Conc.MVar ()
+globalEnvLock = unsafePerformIO $ Conc.newMVar ()
+
+{-# NOINLINE globalEnv #-}
+globalEnv :: IORef Env
+globalEnv = unsafePerformIO $ newIORef undefined
+
+-- | Take the global environment lock.
+takeEnvLock :: Shell ()
+takeEnvLock = unsafeLiftIO $ Conc.takeMVar globalEnvLock
+
+-- | Release the global environment lock.
+releaseEnvLock :: Shell ()
+releaseEnvLock = unsafeLiftIO $ Conc.putMVar globalEnvLock ()
+
+-- | Set the global shell environment to the one of the current computation.
+--   Returns the current global environment.
+--   Should never, ever, be called without holding the global environment lock.
+setShellEnv :: Env -> IO Env
+setShellEnv env = do
+  evs <- Env.getEnvironment
+  wd <- Dir.getCurrentDirectory
+  writeIORef globalEnv env
+  Dir.setCurrentDirectory (envWorkDir env)
+  mapM_ Env.unsetEnv (map fst evs)
+  mapM_ (uncurry Env.setEnv) (envEnvVars env)
+  return $ Env IO.stdin IO.stdout IO.stderr wd evs
+
+-- | Get the current global shell environment, including standard input,
+--   output and error handles. Only safe to call within a computation lifted
+--   into 'Shell' by 'liftIO'.
+shellEnv :: IO Env
+shellEnv = readIORef globalEnv
+
+instance MonadIO Shell where
+  liftIO m = do
+    env <- getEnv
+    unsafeLiftIO $ Conc.withMVar globalEnvLock $ \_ -> do
+      oldenv <- setShellEnv env
+      m <* setShellEnv oldenv
+
+-- | Run a shell computation and return its result. If the computation calls
+--   'exit', the return value will be undefined. If the computation fails,
+--   an error will be thrown.
+shell_ :: Shell a -> IO a
+shell_ m = do
+  res <- shell m
+  case res of
+    Right x          -> pure x
+    Left Success     -> pure $ error "Shell computation terminated successfully"
+    Left (Failure e) -> error e
+
+-- | Perform the given computation and return its standard output.
+capture :: Shell () -> Shell String
+capture m = do
+  env <- getEnv
+  (r, w) <- unsafeLiftIO Proc.createPipe
+  inEnv (env {envStdOut = w}) m
+  unsafeLiftIO $ IO.hClose w >> IO.hGetContents r
+
+-- | Lift a pure function to a computation over standard input/output.
+--   Similar to 'interact'.
+stream :: (String -> String) -> Shell ()
+stream f = stdin >>= echo_ . f
+
+-- | Lift a shell computation to a function over stdin and stdout.
+--   Similar to 'interact'.
+lift :: (String -> Shell String) -> Shell ()
+lift f = stdin >>= f >>= echo_
+
+-- | Get the contents of the computation's standard input.
+stdin :: Shell String
+stdin = getEnv >>= unsafeLiftIO . IO.hGetContents . envStdIn
+
+-- | Write a string to standard output, followed by a newline.
+echo :: String -> Shell ()
+echo s = getEnv >>= unsafeLiftIO . flip IO.hPutStrLn s . envStdOut
+
+-- | Write a string to standard output, without appending a newline.
+echo_ :: String -> Shell ()
+echo_ s = getEnv >>= unsafeLiftIO . flip IO.hPutStr s . envStdOut
+
+-- | Read a line of text from standard input.
+ask :: Shell String
+ask = getEnv >>= unsafeLiftIO . IO.hGetLine . envStdIn
+
+-- | Propagate an explicit 'ExitResult' through the computation.
+joinResult :: Shell (Either ExitReason a) -> Shell a
+joinResult m = do
+  res <- m
+  case res of
+    Right x          -> pure x
+    Left Success     -> exit
+    Left (Failure e) -> fail e
diff --git a/Control/Shell/Concurrent.hs b/Control/Shell/Concurrent.hs
--- a/Control/Shell/Concurrent.hs
+++ b/Control/Shell/Concurrent.hs
@@ -6,14 +6,12 @@
     parallel, parallel_,
     chunks
   ) where
-#if __GLASGOW_HASKELL__ <= 708
-import Control.Applicative
-#endif
 import Control.Concurrent
 import Control.Monad
 import Control.Shell
 import Data.IORef
 
+-- | Only used to have something reliable to attach the futures' weakrefs to.
 type FinalizerHandle = IORef ThreadId
 
 -- | A future is a computation which is run in parallel with a program's main
@@ -23,40 +21,43 @@
 --   is garbage collected. This means that a future should *always* be
 --   'await'ed at some point or otherwise kept alive, to ensure that the
 --   computation finishes.
+--
+--   Note that all any code called in a future using 'unsafeLiftIO' must
+--   refrain from using environment variables, standard input/output, relative
+--   paths and the current working directory, in order to avoid race
+--   conditions. Code within the 'Shell' monad, code imported using 'liftIO'
+--   and external processes spawned from within 'Shell' is perfectly safe.
 data Future a = Future !FinalizerHandle !(MVar (Either ExitReason a))
 
 -- | Create a future value.
 future :: Shell a -> Shell (Future a)
-future m = liftIO $ do
-  v <- newEmptyMVar
-  tid <- forkIO $ shell m >>= putMVar v
-  -- We need a WeakRef to something that's not referenced by the computation
-  -- to be able to kill it when the result is not reachable. IORef to TID is
-  -- as good as anything.
-  r <- newIORef tid
-  _ <- mkWeakIORef r (killThread tid)
-  return $ Future r v
-
--- | Inspect a result from a previous shell computation and make it the result
---   of this computation as well.
-fromResult :: Either ExitReason a -> Shell a
-fromResult x =
-  case x of
-    Left Success       -> exit
-    Left (Failure err) -> fail err
-    Right x'           -> return x'
+future m = do
+  env <- getShellEnv
+  unsafeLiftIO $ do
+    v <- newEmptyMVar
+    tid <- forkIO $ runSh env m >>= putMVar v
+    -- We need a WeakRef to something that's not referenced by the computation
+    -- to be able to kill it when the result is not reachable. IORef to TID is
+    -- as good as anything.
+    r <- newIORef tid
+    _ <- mkWeakIORef r (killThread tid)
+    return $ Future r v
 
 -- | Wait for a future value.
 await :: Future a -> Shell a
-await (Future h v) = liftIO (readMVar v <* readIORef h) >>= fromResult
+await (Future h v) = joinResult $ unsafeLiftIO (readMVar v <* readIORef h)
 
 -- | Check whether a future value has arrived or not.
 check :: Future a -> Shell (Maybe a)
 check (Future h v) = do
-  mx <- liftIO $ tryReadMVar v <* readIORef h
-  maybe (pure Nothing) (fmap Just . fromResult) (h `seq` mx)
+  mx <- unsafeLiftIO $ tryReadMVar v <* readIORef h
+  case h `seq` mx of
+    Just x -> Just <$> joinResult (pure x)
+    _      -> pure Nothing
 
 -- | Perform the given computations in parallel.
+--   The race condition warning for 'Future' when modifying environment
+--   variables or using relative paths still applies.
 parallel :: [Shell a] -> Shell [a]
 parallel = mapM future >=> mapM await
 
diff --git a/Control/Shell/Control.hs b/Control/Shell/Control.hs
new file mode 100644
--- /dev/null
+++ b/Control/Shell/Control.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE TypeFamilies #-}
+-- | Control flow constructs.
+module Control.Shell.Control
+  ( module Control.Monad
+  , Guard (..)
+  , guard, when, unless, orElse
+  ) where
+import Control.Shell.Internal
+import Control.Monad hiding (when, unless, guard)
+
+-- | 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 <- try a
+  case ex of
+    Right x -> return x
+    _       -> b
+
+class Guard guard where
+  -- | The type of the guard's return value, if it succeeds.
+  type Result guard
+
+  -- | Perform a Shell computation; if the computation succeeds but returns
+  --   a false-ish value, the outer Shell computation fails with the given
+  --   error message.
+  assert :: String -> guard -> Shell (Result guard)
+
+instance Guard (Maybe a) where
+  type Result (Maybe a) = a
+  assert _ (Just x) = return x
+  assert desc _     = fail desc
+
+instance Guard Bool where
+  type Result Bool = ()
+  assert _ True = return ()
+  assert desc _ = fail desc
+
+instance Guard a => Guard (Shell a) where
+  type Result (Shell a) = Result a
+  assert desc m = m >>= \x -> assert desc x
+
+-- | Perform a Shell computation; if the computation succeeds but returns
+--   a false-ish value, the outer Shell computation fails.
+--   Corresponds to 'CM.guard'.
+guard :: Guard g => g -> Shell (Result g)
+guard = assert "Guard failed!"
+
+-- | Perform the given computation if the given guard passes, otherwise do
+--   nothing.The guard raising an error counts as failure as far as this
+--   function is concerned.
+--   Corresponds to 'CM.when'.
+when :: Guard g => g -> Shell () -> Shell ()
+when g m = do
+  res <- try (guard g)
+  case res of
+    Right _ -> m
+    _       -> return ()
+
+-- | Perform the given computation if the given guard fails, otherwise do
+--   nothing. The guard raising an error counts as failure as far as this
+--   function is concerned.
+--   Corresponds to 'CM.unless'.
+unless :: Guard g => g -> Shell () -> Shell ()
+unless g m = void (guard g) `orElse` m
diff --git a/Control/Shell/Directory.hs b/Control/Shell/Directory.hs
new file mode 100644
--- /dev/null
+++ b/Control/Shell/Directory.hs
@@ -0,0 +1,161 @@
+-- | Working with directories.
+module Control.Shell.Directory where
+import qualified System.Directory as Dir
+import Control.Monad
+import Control.Shell.Base
+import Control.Shell.Control
+import Control.Shell.File
+
+-- | Get the current working directory.
+pwd :: Shell FilePath
+pwd = envWorkDir <$> getEnv
+
+-- | 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 f t = do
+    e <- getEnv
+    let fromdir = absPath e f
+        todir = absPath e t
+    assert ("`" ++ fromdir ++ "' is not a directory") (isDirectory fromdir)
+    exists <- isDirectory todir
+    if exists
+      then do
+        mkdir True (todir </> takeFileName fromdir)
+        go fromdir (todir </> takeFileName fromdir)
+      else mkdir True todir >> go fromdir todir
+  where
+    go from to = do
+      forEachDirectory_ from (\dir -> mkdir True (to </> dir))
+      forEachFile_ from $ \file -> do
+        let file' = to </> file
+        assert (errOverwrite file') (not <$> isDirectory file')
+        cp (from </> file) file'
+    errOverwrite d = "cannot overwrite directory `" ++ d
+                     ++ "' with non-directory"
+
+-- | Recursively perform an action on each subdirectory of the given directory.
+--   The path passed to the callback is relative to the given directory.
+--   The action will *not* be performed on the given directory itself.
+forEachDirectory :: FilePath -> (FilePath -> Shell a) -> Shell [a]
+forEachDirectory r f = do
+    e <- getEnv
+    go (absPath e $ if null r then "." else r) ""
+  where
+    go dir subdir = do
+      let dir' = dir </> subdir
+      files <- ls dir'
+      fromdirs <- filterM (\d -> isDirectory (dir' </> d)) files
+      xs <- forM fromdirs $ \d -> do
+        let d' = subdir </> d
+        x <- f d'
+        (x:) <$> go dir d'
+      return (concat xs)
+
+-- | Like 'forEachDirectory', but discards its result.
+forEachDirectory_ :: FilePath -> (FilePath -> Shell ()) -> Shell ()
+forEachDirectory_ r f = do
+    e <- getEnv
+    go (absPath e $ if null r then "." else r) ""
+  where
+    go dir subdir = do
+      let dir' = dir </> subdir
+      files <- ls dir'
+      fromdirs <- filterM (\d -> isDirectory (dir' </> d)) files
+      forM_ fromdirs $ \d -> let d' = subdir </> d in f d' >> go dir d'
+
+-- | 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 r f = do
+    e <- getEnv
+    go (absPath e $ if null r then "." else r) ""
+  where
+    go dir subdir = do
+      let dir' = dir </> subdir
+      files <- ls dir'
+      
+      -- For each file in this directory...
+      onlyfiles <- filterM (\fl -> isFile (dir' </> fl)) files
+      xs <- mapM (\x -> f (subdir </> x)) onlyfiles
+
+      -- For each subdirectory...
+      fromdirs <- filterM (\fl -> isDirectory (dir' </> fl)) files
+      xss <- forM fromdirs $ \d -> do
+        go dir (subdir </> d)
+      return $ concat (xs:xss)
+
+-- | Like @forEachFile@ but only performs a side effect.
+forEachFile_ :: FilePath -> (FilePath -> Shell ()) -> Shell ()
+forEachFile_ r f = do
+    e <- getEnv
+    go (absPath e $ if null r then "." else r) ""
+  where
+    go dir subdir = do
+      let dir' = dir </> subdir
+      files <- ls dir'
+      filterM (\fl -> isFile (dir' </> fl)) files >>= mapM_ (f . (subdir </>))
+      fromdirs <- filterM (\fl -> isDirectory (dir' </> fl)) files
+      forM_ fromdirs $ \d -> go dir (subdir </> d)
+
+-- | List the contents of a directory, sans '.' and '..'.
+ls :: FilePath -> Shell [FilePath]
+ls dir = do
+  e <- getEnv
+  contents <- unsafeLiftIO $ Dir.getDirectoryContents (absPath e dir)
+  return [f | f <- contents, f /= ".", f /= ".."]
+
+-- | Create a directory. Optionally create any required missing directories as
+--   well.
+mkdir :: Bool -> FilePath -> Shell ()
+mkdir True dir = do
+  e <- getEnv
+  unsafeLiftIO $ Dir.createDirectoryIfMissing True (absPath e dir)
+mkdir _    dir = do
+  e <- getEnv
+  unsafeLiftIO $ Dir.createDirectory (absPath e dir)
+
+-- | Recursively remove a directory. Follows symlinks, so be careful.
+rmdir :: FilePath -> Shell ()
+rmdir dir = do
+  e <- getEnv
+  unsafeLiftIO $ Dir.removeDirectoryRecursive (absPath e dir)
+
+-- | Do something with the user's home directory.
+withHomeDirectory :: (FilePath -> Shell a) -> Shell a
+withHomeDirectory act = liftIO Dir.getHomeDirectory >>= act
+
+-- | Perform an action with the user's home directory as the working directory.
+inHomeDirectory :: Shell a -> Shell a
+inHomeDirectory act = withHomeDirectory $ flip inDirectory 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 with the given application's data directory as the working
+--   directory.
+inAppDirectory :: FilePath -> Shell a -> Shell a
+inAppDirectory app act = withAppDirectory app $ flip inDirectory 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
+  env <- getEnv
+  inEnv (env {envWorkDir = absPath env dir}) act
+
+-- | Does the given path lead to a directory?
+isDirectory :: FilePath -> Shell Bool
+isDirectory dir = do
+  e <- getEnv
+  unsafeLiftIO $ Dir.doesDirectoryExist (absPath e dir)
+
+-- | Does the given path lead to a file?
+isFile :: FilePath -> Shell Bool
+isFile f = do
+  e <- getEnv
+  unsafeLiftIO $ Dir.doesFileExist (absPath e f)
diff --git a/Control/Shell/Download.hs b/Control/Shell/Download.hs
deleted file mode 100644
--- a/Control/Shell/Download.hs
+++ /dev/null
@@ -1,84 +0,0 @@
-{-# LANGUAGE OverloadedStrings, CPP #-}
--- | High level functions for downloading files.
-module Control.Shell.Download
-  ( URI
-  , fetch, fetchBytes
-  , fetchFile
-#if DOWNLOAD_EXTRAS
-  , fetchTags, fetchXML, fetchFeed
-#endif
-  ) where
-import Data.ByteString as BS (ByteString, writeFile)
-import Data.String
-import Network.HTTP
-import qualified Network.URI as U
-#if DOWNLOAD_EXTRAS
-import Text.Feed.Import (parseFeedString)
-import Text.Feed.Types (Feed)
-import Text.HTML.TagSoup (Tag, parseTags)
-import Text.XML.Light (Content, parseXML)
-#endif
-import Control.Shell
-
--- | A Uniform Resource Locator.
-type URI = String
-
-liftE :: Show e => IO (Either e a) -> Shell a
-liftE m = do
-  res <- liftIO m
-  case res of
-    Left e  -> fail (show e)
-    Right x -> return x
-
-httpFail :: (Int, Int, Int) -> String -> Shell a
-httpFail (a,b,c) reason =
-  fail $ "HTTP error " ++ concat [show a, show b, show c] ++ ": " ++ reason
-
-fetchSomething :: (IsString a, HStream a) => URI -> Shell a
-fetchSomething uri = do
-    u <- assert ("could not parse URI `" ++ uri ++ "'") (U.parseURI uri)
-    rsp <- liftE $ simpleHTTP (req u)
-    case rspCode rsp of
-      (2,_,_) -> return (rspBody rsp)
-      code    -> httpFail code (rspReason rsp)
-  where
-    req u = Request {
-        rqURI = u,
-        rqMethod = GET,
-        rqHeaders = [],
-        rqBody = ""
-      }
-
--- | Download content specified by a url using curl, returning the content
---   as a strict 'ByteString'.
-fetchBytes :: URI -> Shell ByteString
-fetchBytes = fetchSomething
-
--- | Download content specified by a url using curl, returning the content
---   as a 'String'.
-fetch :: URI -> Shell String
-fetch = fetchSomething
-
--- | Download content specified by a url using curl, writing the content to
---   the file specified by the given 'FilePath'.
-fetchFile :: FilePath -> URI -> Shell ()
-fetchFile file = fetchBytes >=> liftIO . BS.writeFile file
-
-#if DOWNLOAD_EXTRAS
--- | Download the content as for 'fetch', but return it as a list of parsed
---   tags using the tagsoup html parser.
-fetchTags :: URI -> Shell [Tag String]
-fetchTags = fmap parseTags . fetch
-
--- | Download the content as for 'fetch', but return it as parsed XML, using
---   the xml-light parser.
-fetchXML :: URI -> Shell [Content]
-fetchXML = fmap parseXML . fetch
-
--- | Download the content as for 'fetch', but return it as as parsed RSS or
---   Atom content, using the feed library parser.
-fetchFeed :: URI -> Shell Feed
-fetchFeed uri = do
-  str <- fetch uri
-  assert ("could not parse feed from `" ++ uri ++ "'") (parseFeedString str)
-#endif
diff --git a/Control/Shell/File.hs b/Control/Shell/File.hs
new file mode 100644
--- /dev/null
+++ b/Control/Shell/File.hs
@@ -0,0 +1,71 @@
+-- | Working with files.
+module Control.Shell.File
+  ( rm, mv, cp
+  , input, output
+  , IO.IOMode (..)
+  , withFile, withBinaryFile
+  , openFile, openBinaryFile
+  ) where
+import qualified System.Directory as Dir
+import qualified System.IO as IO
+import Control.Shell.Base
+
+-- | Lazily read a file.
+input :: FilePath -> Shell String
+input f = do
+  e <- getEnv
+  unsafeLiftIO $ readFile (absPath e f)
+
+-- | Lazily write a file.
+output :: FilePath -> String -> Shell ()
+output f s = do
+  e <- getEnv
+  unsafeLiftIO $ writeFile (absPath e f) s
+
+-- | Remove a file.
+rm :: FilePath -> Shell ()
+rm dir = do
+  e <- getEnv
+  unsafeLiftIO $ Dir.removeFile (absPath e dir)
+
+-- | Rename a file.
+mv :: FilePath -> FilePath -> Shell ()
+mv from to = do
+  e <- getEnv
+  unsafeLiftIO $ Dir.renameFile (absPath e from) (absPath e to)
+
+-- | 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 f t = do
+  e <- getEnv
+  let (from, to) = (absPath e f, absPath e t)
+  todir <- unsafeLiftIO $ Dir.doesDirectoryExist to
+  if todir
+    then cp from (to </> takeFileName from)
+    else liftIO $ Dir.copyFile from to
+
+-- | Perform a computation over a file.
+withFile :: FilePath -> IO.IOMode -> (IO.Handle -> Shell a) -> Shell a
+withFile file mode f = joinResult $ do
+  e <- getEnv
+  unsafeLiftIO (IO.withFile (absPath e file) mode (shell . f))
+
+-- | Perform a computation over a binary file.
+withBinaryFile :: FilePath -> IO.IOMode -> (IO.Handle -> Shell a) -> Shell a
+withBinaryFile file mode f = joinResult $ do
+  e <- getEnv
+  unsafeLiftIO (IO.withBinaryFile (absPath e file) mode (shell . f))
+
+-- | Open a file, returning a handle to it.
+openFile :: FilePath -> IO.IOMode -> Shell IO.Handle
+openFile file mode = do
+  e <- getEnv
+  unsafeLiftIO $ IO.openFile (absPath e file) mode
+
+-- | Open a file in binary mode, returning a handle to it.
+openBinaryFile :: FilePath -> IO.IOMode -> Shell IO.Handle
+openBinaryFile file mode = do
+  e <- getEnv
+  unsafeLiftIO $ IO.openBinaryFile (absPath e file) mode
diff --git a/Control/Shell/Handle.hs b/Control/Shell/Handle.hs
--- a/Control/Shell/Handle.hs
+++ b/Control/Shell/Handle.hs
@@ -1,78 +1,50 @@
 -- | Writing Shell programs using 'Handle's.
 module Control.Shell.Handle (
-    IO.Handle, IO.IOMode (..),
-    IO.stdin, IO.stdout, IO.stderr,
-
     hPutStr, hPutStrLn,
     hGetLine, hGetContents,
-
     hGetBytes, hPutBytes, hGetByteLine, hGetByteContents,
-
-    hFlush, hClose, withFile, withBinaryFile, openFile, openBinaryFile
+    hFlush, hClose
   ) where
 import qualified System.IO as IO
 import qualified Data.ByteString as BS
-import Control.Shell.Internal
+import Control.Shell.Base
 
 -- | Write a string to a handle.
 hPutStr :: IO.Handle -> String -> Shell ()
-hPutStr h s = liftIO $ IO.hPutStr h s
+hPutStr h s = unsafeLiftIO $ IO.hPutStr h s
 
 -- | Write a string to a handle, followed by a newline.
 hPutStrLn :: IO.Handle -> String -> Shell ()
-hPutStrLn h s = liftIO $ IO.hPutStrLn h s
+hPutStrLn h s = unsafeLiftIO $ IO.hPutStrLn h s
 
 -- | Close a handle.
 hClose :: IO.Handle -> Shell ()
-hClose = liftIO . IO.hClose
+hClose = unsafeLiftIO . IO.hClose
 
 -- | Flush a handle.
 hFlush :: IO.Handle -> Shell ()
-hFlush = liftIO . IO.hFlush
+hFlush = unsafeLiftIO . IO.hFlush
 
 -- | Read a line of input from a handle.
 hGetLine :: IO.Handle -> Shell String
-hGetLine = liftIO . IO.hGetLine
+hGetLine = unsafeLiftIO . IO.hGetLine
 
 -- | Lazily read all remaining input from a handle.
 hGetContents :: IO.Handle -> Shell String
-hGetContents = liftIO . IO.hGetContents
+hGetContents = unsafeLiftIO . IO.hGetContents
 
 -- | Read @n@ bytes from a handle.
 hGetBytes :: IO.Handle -> Int -> Shell BS.ByteString
-hGetBytes h = liftIO . BS.hGet h
+hGetBytes h = unsafeLiftIO . BS.hGet h
 
 -- | Read a line of input from a handle and return it as a 'BS.ByteString'.
 hGetByteLine :: IO.Handle -> Shell BS.ByteString
-hGetByteLine = liftIO . BS.hGetLine
+hGetByteLine = unsafeLiftIO . BS.hGetLine
 
 -- | Read all remaining input from a handle and return it as a 'BS.ByteString'.
 hGetByteContents :: IO.Handle -> Shell BS.ByteString
-hGetByteContents = liftIO . BS.hGetContents
+hGetByteContents = unsafeLiftIO . BS.hGetContents
 
 -- | Write a 'BS.ByteString' to a handle. Newline is not appended.
 hPutBytes :: IO.Handle -> BS.ByteString -> Shell ()
-hPutBytes h = liftIO . BS.hPutStr h
-
--- | Perform a computation over a file.
-withFile :: FilePath -> IO.IOMode -> (IO.Handle -> Shell a) -> Shell a
-withFile file mode f =
-  liftIO (IO.withFile file mode (shell . f)) >>= liftResult
-
--- | Perform a computation over a binary file.
-withBinaryFile :: FilePath -> IO.IOMode -> (IO.Handle -> Shell a) -> Shell a
-withBinaryFile file mode f =
-  liftIO (IO.withBinaryFile file mode (shell . f)) >>= liftResult
-
--- | Open a file, returning a handle to it.
-openFile :: FilePath -> IO.IOMode -> Shell IO.Handle
-openFile file = liftIO . IO.openFile file
-
--- | Open a file in binary mode, returning a handle to it.
-openBinaryFile :: FilePath -> IO.IOMode -> Shell IO.Handle
-openBinaryFile file = liftIO . IO.openBinaryFile file
-
-liftResult :: Either ExitReason a -> Shell a
-liftResult (Right x)            = return x
-liftResult (Left Success)       = exit
-liftResult (Left (Failure err)) = fail err
+hPutBytes h = unsafeLiftIO . BS.hPutStr h
diff --git a/Control/Shell/Internal.hs b/Control/Shell/Internal.hs
--- a/Control/Shell/Internal.hs
+++ b/Control/Shell/Internal.hs
@@ -1,303 +1,219 @@
-{-# LANGUAGE CPP #-}
--- | Internal, hairy bits of Shellmate.
-module Control.Shell.Internal (
-    MonadIO (..), Shell, ExitReason (..),
-    shell, shell_,
-    (|>), exit,
-    run, run_, genericRun, runInteractive,
-    withTempDirectory, withCustomTempDirectory,
-    withTempFile, withCustomTempFile,
-    try
+{-# LANGUAGE CPP, GADTs, RecordWildCards #-}
+-- | Primitives and run function.
+module Control.Shell.Internal
+  ( Shell
+  , ExitReason (..), Env (..)
+  , shell, runSh
+  , exit, run, try, getEnv, inEnv, unsafeLiftIO, (|>)
   ) where
-#if __GLASGOW_HASKELL__ <= 708
-import Control.Applicative
-#endif
-import Control.Monad (ap)
-import Control.Monad.IO.Class
+import Control.Monad (when, ap)
 import qualified Control.Concurrent as Conc
 import qualified Control.Exception as Ex
-import Data.List (sort)
-import qualified System.Directory as Dir
-import qualified System.Environment as Env
 import qualified System.Exit as Exit
 import qualified System.Process as Proc
 import qualified System.IO as IO
-import qualified System.IO.Temp as Temp
+import qualified System.Directory as Dir (getCurrentDirectory)
+import qualified System.Environment as Env (getEnvironment)
 
 -- | A command name plus a ProcessHandle.
-data Pid = Pid {pidName :: String, pidHandle :: Proc.ProcessHandle}
+data Pid = PID !String                         !Proc.ProcessHandle
+         | TID !(Conc.MVar (Maybe ExitReason)) !Conc.ThreadId
 
--- | Monad for running shell commands. If a command fails, the entire
---   computation is aborted unless @mayFail@ is used.
-newtype Shell a = Shell {
-    unSh :: IO ([Pid], Result a)
+-- | A shell environment: consists of the current standard input, output and
+--   error handles used by the computation, as well as the current working
+--   directory and set of environment variables.
+data Env = Env
+  { envStdIn   :: !IO.Handle
+  , envStdOut  :: !IO.Handle
+  , envStdErr  :: !IO.Handle
+  , envWorkDir :: !FilePath
+  , envEnvVars :: ![(String, String)]
   }
 
-data Result a = Fail !String | Next !a | Done
-
-data ExitReason = Success | Failure !String
-  deriving (Show, Eq)
-
-instance Functor Result where
-  fmap f (Next x) = Next (f x)
-  fmap _ (Fail x) = Fail x
-  fmap _ Done     = Done
+-- | A shell command: either an IO computation or a pipeline of at least one
+--   step.
+data Shell a where
+  Lift   :: !(IO a) -> Shell a
+  Pipe   :: ![PipeStep] -> Shell ()
+  Bind   :: Shell a -> (a -> Shell b) -> Shell b
+  GetEnv :: Shell Env
+  InEnv  :: Env -> Shell a -> Shell a
+  Try    :: Shell a -> Shell (Either String a)
+  Done   :: Shell a
+  Fail   :: String -> Shell a
 
-instance Monad Shell where
-  fail err = Shell $ return ([], Fail err)
-  return x = Shell $ return ([], Next 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 $ do
-    (pids, x) <- m
-    merr <- waitPids pids
-    case (x, merr) of
-      (Fail err, _) -> return ([], Fail err)
-      (_, Just err) -> return ([], Fail err)
-      (Next x', _)  -> unSh (f x')
-      (Done, _)     -> return ([], Done)
+-- | A step in a pipeline: either a shell computation or an external process.
+data PipeStep
+  = Proc     !String ![String]
+  | Internal !(Shell ())
 
-instance MonadIO Shell where
-  liftIO act = Shell $ flip Ex.catch exHandler $ do
-    x <- act
-    return ([], Next x)
+instance Functor Shell where
+  fmap f m = m >>= return . f
 
 instance Applicative Shell where
-  pure  = return
   (<*>) = ap
+  pure  = return
 
-instance Functor Shell where
-  fmap f (Shell x) = Shell (fmap (fmap (fmap f)) x)
+instance Monad Shell where
+  return = Lift . return
+  (>>=)  = Bind
+  fail   = Fail
 
--- | Given a list of old key-value pairs and a list of new key-value pairs,
---   returns the list of key-value pairs where the key exists in both the old
---   and the new list, and the values are different between the new and the old
---   list. The values in the returned list are taken from the old list.
---   Keys must be unique.
---   @oldValues [(a, 1), (c, 2)] [(b, 3), (c, 4)] == [(c, 2)]@
-oldValues :: (Ord a, Eq a, Eq b) => [(a, b)] -> [(a, b)] -> [(a, b)]
-oldValues xxs@((k1, v1):xs) yys@((k2, v2):ys)
-  | k1 <  k2             = oldValues xs yys
-  | k1 >  k2             = oldValues xxs ys
-  | k1 == k2 && v1 /= v2 = (k1, v1) : oldValues xs ys
-  | otherwise            = oldValues xs ys
-oldValues _ _            = []
+-- | Lift an IO computation into a shell. The lifted computation is not
+--   thread-safe, and should thus absolutely not use environment variables,
+--   relative paths or standard input/output.
+unsafeLiftIO :: IO a -> Shell a
+unsafeLiftIO = Lift
 
--- | Remove all given keys from the given key-value list. Both lists must be
---   sorted, and the keys in the key-value list must be unique.
-(\\) :: (Ord a, Eq a) => [(a, b)] -> [(a, b)] -> [(a, b)]
-xxs@(x@(k1,_):xs) \\ kks@((k,_):ks)
-  | k <  k1 = xxs \\ ks
-  | k >  k1 = x:(xs \\ kks)
-  | k == k1 = xs \\ ks
-xs \\ _     = xs
+-- | Why did the computation terminate?
+data ExitReason = Success | Failure !String
+  deriving (Show, Eq)
 
--- | Run a Shell computation. The program's working directory and environment
---   will be restored after after the computation finishes.
+-- | Run a shell computation. If part of the computation fails, the whole
+--   computation fails. The computation's environment is initially that of the
+--   whole process.
 shell :: Shell a -> IO (Either ExitReason a)
-shell act = do
-    dir <- Dir.getCurrentDirectory
-    env <- sort <$> Env.getEnvironment
-    (pids, res) <- unSh act
-    merr <- waitPids pids
-    Dir.setCurrentDirectory dir
-    resetEnv env
-    case merr of
-      Just err -> return $ Left $ Failure err
-      _        -> return $ resultToEither res
+shell m = do
+    evs <- Env.getEnvironment
+    wd <- Dir.getCurrentDirectory
+    runSh (env wd evs) m
   where
-    resultToEither (Next x) = Right x
-    resultToEither (Fail e) = Left (Failure e)
-    resultToEither (Done)   = Left Success
-
-    resetEnv :: [(String, String)] -> IO ()
-    resetEnv old = do
-      new <- sort <$> Env.getEnvironment
-      mapM_ (Env.unsetEnv . fst) (new \\ old)
-      mapM_ (uncurry Env.setEnv) (oldValues old new)
+    env wd evs = Env
+      { envStdIn   = IO.stdin
+      , envStdOut  = IO.stdout
+      , envStdErr  = IO.stderr
+      , envWorkDir = wd
+      , envEnvVars = evs
+      }
 
--- | Run a shell computation and discard its return value. If the computation
---   fails, print its error message to @stderr@ and exit.
-shell_ :: Shell a -> IO ()
-shell_ act = do
-  res <- shell act
+runSh :: Env -> Shell a -> IO (Either ExitReason a)
+runSh _ (Lift m) = do
+  Ex.catch (Right <$> m)
+           (\(Ex.SomeException e) -> pure $ Left (Failure (show e)))
+runSh env (Pipe p) = do
+  ((stepenv, step) : steps) <- mkEnvs env p
+  ma <- waitPids =<< mapM (uncurry (runStep True)) steps
+  mb <- waitPids . (:[]) =<< runStep False stepenv step
+  case ma >> mb of
+    Just err -> pure $ Left err
+    _        -> pure $ Right ()
+runSh _ Done = do
+  return $ Left Success
+runSh env (Bind m f) = do
+  res <- runSh env m
   case res of
-    Left (Failure err) -> IO.hPutStrLn IO.stderr err >> Exit.exitFailure
-    _                  -> return ()
-
--- | 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 $ do
-  (pids, x) <- m
-  (pids', x') <- case x of
-    Fail err -> return ([], Fail err)
-    Next x'  -> unSh (f x')
-    Done     -> return ([], Done)
-  return (pids ++ pids', x')
-infixl 1 |>
-
--- | Terminate a computation, successfully.
-exit :: Shell a
-exit = Shell $ return ([], Done)
-
--- | 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 $ do
-    Temp.withSystemTempDirectory template act'
-  where
-    act' fp = Ex.catch (unSh (act fp)) exHandler
+    Right x -> runSh env (f x)
+    Left e  -> pure $ Left e
+runSh env GetEnv = do
+  pure $ Right env
+runSh _ (InEnv env m) = do
+  runSh env m
+runSh env (Try m) = do
+  res <- runSh env m
+  case res of
+    Right x          -> pure $ Right (Right x)
+    Left (Failure e) -> pure $ Right (Left e)
+    Left Success     -> pure $ Left Success
+runSh _ (Fail e) = do
+  pure $ Left (Failure e)
 
--- | 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 $ do
-    Temp.withTempDirectory dir "shellmate" act'
+-- | Start a pipeline step.
+runStep :: Bool -> Env -> PipeStep -> IO Pid
+runStep closefds Env{..} (Proc cmd args) = do
+    (_, _, _, ph) <- Proc.createProcess cproc
+    pure $ PID cmd ph
   where
-    act' fp = Ex.catch (unSh (act fp)) exHandler
-
--- | 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 $ do
-    Temp.withSystemTempFile template act'
+    cproc = Proc.CreateProcess
+      { Proc.cmdspec      = Proc.RawCommand cmd args
+      , Proc.cwd          = Just envWorkDir
+      , Proc.env          = Just envEnvVars
+      , Proc.std_in       = Proc.UseHandle envStdIn
+      , Proc.std_out      = Proc.UseHandle envStdOut
+      , Proc.std_err      = Proc.UseHandle envStdErr
+      , Proc.close_fds    = closefds
+#if MIN_VERSION_process(1,2,0)
+      , Proc.delegate_ctlc = False
+#endif
+      , Proc.create_group = False
+      }
+runStep closefds env (Internal cmd) = do
+  v <- Conc.newEmptyMVar
+  tid <- Conc.forkFinally (runSh env cmd >>= done) $ \res -> do
+    case res of
+      Right (Left e) -> Conc.putMVar v (Just e)
+      Left e         -> Conc.putMVar v (Just $ Failure $ show e)
+      _              -> Conc.putMVar v Nothing
+  pure $ TID v tid
   where
-    act' fp h = Ex.catch (unSh (act fp h)) exHandler
+    done x = do
+      when closefds $ IO.hClose (envStdOut env)
+      return x
 
--- | 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 $ do
-    Temp.withTempFile dir "shellmate" act'
+-- | Pair up pipe steps with corresponding environments, ensuring that each
+--   step is connected to the next via a pipe.
+mkEnvs :: Env -> [PipeStep] -> IO [(Env, PipeStep)]
+mkEnvs env = go [] (envStdIn env)
   where
-    act' fp h = Ex.catch (unSh (act fp h)) exHandler
+    go acc stdi (step : steps) = do
+      (next, stdo) <- Proc.createPipe
+      go ((env {envStdIn = stdi, envStdOut = stdo}, step):acc) next steps
+    go ((e, s) : steps) _ _ = do
+      pure ((e {envStdOut = envStdOut env}, s) : steps)
+    go acc _ _ = pure acc
 
--- | Perform an action that may fail without aborting the entire computation.
---   Forces serialization. If the inner computation terminates successfully,
---   the outer computation terminates as well.
-try :: Shell a -> Shell (Either String a)
-try (Shell act) = Shell $ do
-  (pids, x) <- Ex.catch act exHandler
-  merr <- waitPids pids
-  case (merr, x) of
-    (Just err, _) -> return ([], Next (Left err))
-    (_, Next x')  -> return ([], Next (Right x'))
-    (_, Fail err) -> return ([], Next (Left err))
-    (_, Done)     -> return ([], Done)
+-- | Terminate a pid, be it process or thread.
+killPid :: Pid -> IO ()
+killPid (PID _ p) = Proc.terminateProcess p
+killPid (TID _ t) = Conc.killThread t
 
 -- | 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)
+waitPids :: [Pid] -> IO (Maybe ExitReason)
+waitPids (PID cmd p : ps) = do
+  exCode <- Proc.waitForProcess p
   case exCode of
     Exit.ExitFailure ec -> do
-      killPids ps
-      return . Just $ "Command '" ++ (pidName p) ++ "' failed with error "
-                    ++" code " ++ show ec
+      mapM_ killPid ps
+      return . Just $ Failure $ "Command '" ++ cmd ++ "' failed with error "
+                              ++" code " ++ show ec
     _ -> do
       waitPids ps
+waitPids (TID v _ : ps) = do
+  merr <- Conc.takeMVar v
+  case merr of
+    Just e -> mapM_ killPid ps >> return (Just e)
+    _      -> waitPids ps
 waitPids _ = do
   return Nothing
 
--- | Kill all processes in the list.
-killPids :: [Pid] -> IO ()
-killPids = mapM_ (Proc.terminateProcess . pidHandle)
-
--- | General exception handler; any exception causes failure.
-exHandler :: Ex.SomeException -> IO ([Pid], Result a)
-exHandler x = return ([], Fail $ show x)
-
--- | 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
-  exCode <- liftIO $ do
-    (Just inp, _, _, pid) <- runP p args Proc.CreatePipe
-                                         Proc.Inherit
-                                         Proc.Inherit
-    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 an interactive process.
-runInteractive :: FilePath -> [String] -> Shell ()
-runInteractive p args = do
-  exCode <- liftIO $ do
-    (_, _, _, pid) <- runP p args Proc.Inherit Proc.Inherit Proc.Inherit
-    Proc.waitForProcess pid
-  case exCode of
-    Exit.ExitFailure ec -> fail $ "Command '" ++ p ++ "' failed with error " 
-                                ++" code " ++ show ec
-    _                   -> return ()
-
 -- | Execute an external command. No globbing, escaping or other external shell
 --   magic is performed on either the command or arguments. The program's
---   stdout will be returned, and not echoed to the screen.
-run :: FilePath -> [String] -> String -> Shell String
-run p args stdin = Shell $ do
-  (output, _, pid) <- runHelper p args stdin Proc.Inherit
-  return ([Pid p pid], Next output)
+--   stdout will be written to stdout.
+run :: FilePath -> [String] -> Shell ()
+run p args = Pipe [Proc p args]
 
--- | Like 'run', but always succeeds and returns the program's standard
---   error stream and exit code.
-genericRun :: FilePath -> [String] -> String -> Shell (Int, String, String)
-genericRun p args stdin = Shell $ do
-  (output, Just errh, pid) <- runHelper p args stdin Proc.CreatePipe
-  exCode <- Proc.waitForProcess pid
-  errstr <- liftIO $ IO.hGetContents errh
-  case errstr `seq` exCode of
-    Exit.ExitSuccess    -> return ([], Next (0, output, errstr))
-    Exit.ExitFailure ec -> return ([], Next (ec, output, errstr))
+-- | Terminate the program successfully.
+exit :: Shell a
+exit = Done
 
--- | Helper for 'run' and 'runWithStderr'.
-runHelper :: FilePath
-           -> [String]
-           -> String
-           -> Proc.StdStream
-           -> IO (String, Maybe IO.Handle, Proc.ProcessHandle)
-runHelper p args inpstr errstream = do
-  (Just inp, Just out, merr, pid) <- runP p args Proc.CreatePipe
-                                                 Proc.CreatePipe
-                                                 errstream
-  let feed str = do
-        case splitAt 4096 str of
-          ([], [])      -> IO.hClose inp
-          (first, str') -> IO.hPutStr inp first >> feed str'
-  _ <- Conc.forkIO $ feed inpstr
-  output <- IO.hGetContents out
-  output `seq` return (output, merr, pid)
+-- | Connect the standard output of the first argument to the standard input
+--   of the second argument, and run the two computations in parallel.
+(|>) :: Shell () -> Shell () -> Shell ()
+Pipe m |> Pipe n = Pipe (m ++ n)
+Pipe m |> n      = Pipe (m ++ [Internal n])
+m      |> Pipe n = Pipe (Internal m : n)
+m      |> n      = Pipe [Internal m, Internal n]
+infixl 5 |>
 
--- | Create a process. Helper for 'run' and friends.
-runP :: String
-     -> [String]
-     -> Proc.StdStream
-     -> Proc.StdStream
-     -> Proc.StdStream
-     -> IO (Maybe IO.Handle,
-            Maybe IO.Handle,
-            Maybe IO.Handle,
-            Proc.ProcessHandle)
-runP p args stdin stdout stderr =
-    Proc.createProcess cproc
-  where
-    cproc = Proc.CreateProcess {
-        Proc.cmdspec      = Proc.RawCommand p args,
-        Proc.cwd          = Nothing,
-        Proc.env          = Nothing,
-        Proc.std_in       = stdin,
-        Proc.std_out      = stdout,
-        Proc.std_err      = stderr,
-        Proc.close_fds    = False,
-#if MIN_VERSION_process(1,2,0)
-        Proc.delegate_ctlc = False,
-#endif
-        Proc.create_group = False
-      }
+-- | Run a computation in the given environment.
+inEnv :: Env -> Shell a -> Shell a
+inEnv = InEnv
+
+-- | Get the current environment.
+getEnv :: Shell Env
+getEnv = GetEnv
+
+-- | Attempt to run a computation. If the inner computation fails, the outer
+--   computations returns its error message, otherwise its result is returned.
+try :: Shell a -> Shell (Either String a)
+try = Try
diff --git a/Control/Shell/Temp.hs b/Control/Shell/Temp.hs
new file mode 100644
--- /dev/null
+++ b/Control/Shell/Temp.hs
@@ -0,0 +1,66 @@
+-- | Temporary files and directories.
+module Control.Shell.Temp
+  ( withTempDirectory, withCustomTempDirectory
+  , withTempFile, withCustomTempFile
+  ) where
+import Control.Shell.Base
+import qualified System.IO.Temp as Temp
+import qualified System.IO as IO
+
+-- | Create a temp directory in the standard system temp directory, do
+--   something with it, then remove it.
+withTempDirectory :: (FilePath -> Shell a) -> Shell a
+withTempDirectory act = joinResult $ do
+  env <- getEnv
+  takeEnvLock
+  unsafeLiftIO $ do
+    oldenv <- setShellEnv env
+    Temp.withSystemTempDirectory "shellmate" $ \fp -> do
+      setShellEnv oldenv
+      runSh env $ do
+        releaseEnvLock
+        act fp
+
+-- | Create a temp directory in given directory, do something with it, then
+--   remove it.
+withCustomTempDirectory :: FilePath -> (FilePath -> Shell a) -> Shell a
+withCustomTempDirectory dir act = joinResult $ do
+  env <- getEnv
+  takeEnvLock
+  unsafeLiftIO $ do
+    oldenv <- setShellEnv env
+    Temp.withTempDirectory dir "shellmate" $ \fp -> do
+      setShellEnv oldenv
+      runSh env $ do
+        releaseEnvLock
+        act fp
+
+-- | Create a temp file in the standard system temp directory, do something
+--   with it, then remove it.
+withTempFile :: FileMode -> (FilePath -> IO.Handle -> Shell a) -> Shell a
+withTempFile fm act = joinResult $ do
+  env <- getEnv
+  takeEnvLock
+  unsafeLiftIO $ do
+    oldenv <- setShellEnv env
+    Temp.withSystemTempFile "shellmate" $ \fp h -> do
+      setShellEnv oldenv
+      runSh env $ do
+        releaseEnvLock
+        unsafeLiftIO $ IO.hSetBinaryMode h (fm == BinaryMode)
+        act fp h
+
+-- | Create a temp file in the standard system temp directory, do something
+--   with it, then remove it.
+withCustomTempFile :: FileMode -> FilePath -> (FilePath -> IO.Handle -> Shell a) -> Shell a
+withCustomTempFile fm dir act = joinResult $ do
+  env <- getEnv
+  takeEnvLock
+  unsafeLiftIO $ do
+    oldenv <- setShellEnv env
+    Temp.withTempFile dir "shellmate" $ \fp h -> do
+      setShellEnv oldenv
+      runSh env $ do
+        releaseEnvLock
+        unsafeLiftIO $ IO.hSetBinaryMode h (fm == BinaryMode)
+        act fp h
diff --git a/shellmate.cabal b/shellmate.cabal
--- a/shellmate.cabal
+++ b/shellmate.cabal
@@ -1,52 +1,40 @@
 name:                shellmate
-version:             0.2.3
+version:             0.3
 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
+homepage:            https://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
+cabal-version:       >=1.10
+bug-reports:         https://github.com/valderman/shellmate/issues
 
 source-repository head
     type:       git
     location:   https://github.com/valderman/shellmate.git
-
-flag download-extras
-    default: True
-    description: Build Control.Shell.Download with extras for parsing downloaded HTML, XML, RSS and Atom feeds.
     
 library
   exposed-modules:
     Control.Shell
     Control.Shell.Concurrent
-    Control.Shell.Download
   other-modules:
     Control.Shell.Handle
     Control.Shell.Internal
+    Control.Shell.Base
+    Control.Shell.Temp
+    Control.Shell.Control
+    Control.Shell.File
+    Control.Shell.Directory
   build-depends:
-    base         >=4.7    && <5,
-    transformers >=0.3    && <0.5,
-    bytestring   >=0.10   && <0.11,
-    filepath     >=1.3    && <1.5,
-    process      >=1.1    && <1.5,
-    directory    >=1.1    && <1.3,
-    temporary    >=1.1    && <1.3,
-    -- for Control.Shell.Download
-    HTTP         >=4000.2 && <4000.3,
-    network-uri  >=2.6    && <2.7
-
-  if flag(download-extras)
-    build-depends:            
-      feed    >=0.3    && <0.4,
-      tagsoup >=0.13   && <0.14,
-      xml     >=1.3    && <1.4
-    cpp-options:
-      -DDOWNLOAD_EXTRAS=1
-  ghc-options:
-    -Wall
+    base         >=4.8  && <5,
+    transformers >=0.3  && <0.5,
+    bytestring   >=0.10 && <0.11,
+    filepath     >=1.3  && <1.5,
+    process      >=1.1  && <1.5,
+    directory    >=1.1  && <1.3,
+    temporary    >=1.1  && <1.3
+  default-language:
+    Haskell2010
