diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for streaming-process
+
+## 0.1.0.0  -- 2018-05-23
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 Ivan Lazar Miljenovic
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+streaming-process
+=================
+
+[![Hackage](https://img.shields.io/hackage/v/streaming-process.svg)](https://hackage.haskell.org/package/streaming-process) [![Build Status](https://travis-ci.org/haskell-streaming/streaming-process.svg)](https://travis-ci.org/haskell-streaming/streaming-process)
+
+Run a process, streaming data in or out.
+
+A lot of configuration options are available to fine-tune which inputs
+and outputs are streamed.
+
+This uses [streaming-with] to handle resource management, and
+[streaming-concurrency] for handling both `stdout` and `stderr`
+together.
+
+As such, code is typically run in a continuation-passing-style.  You
+may wish to use the `Streaming.Process.Lifted` module if you have many
+of these nested.
+
+[streaming-with]: http://hackage.haskell.org/package/streaming-with
+[streaming-concurrency]: http://hackage.haskell.org/package/streaming-concurrency
+
+Exceptions
+----------
+
+The functions in this library will all throw
+`ProcessExitedUnsuccessfully` if the process/command itself fails.
+
+WARNING
+-------
+
+If using this module, you will need to have:
+
+```cabal
+ghc-options -threaded
+```
+
+in the executable section of your `.cabal` file, otherwise your code
+will likely hang!
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/Streaming/Process.hs b/src/Streaming/Process.hs
new file mode 100644
--- /dev/null
+++ b/src/Streaming/Process.hs
@@ -0,0 +1,262 @@
+{-# LANGUAGE FlexibleContexts, MultiParamTypeClasses, NamedFieldPuns,
+             RankNTypes, RecordWildCards #-}
+
+{- |
+   Module      : Streaming.Process
+   Description : Run system process with support for Streams
+   Copyright   : (c) Ivan Lazar Miljenovic
+   License     : MIT
+   Maintainer  : Ivan.Miljenovic@gmail.com
+
+   Run system commands in a streaming fashion.
+
+   __WARNING:__ If using this module, you will need to have
+   @ghc-options -threaded@ in your @.cabal@ file otherwise it will
+   likely hang!
+
+   These functions are typically written to be used in a
+   continuation-passing style to allow for proper finalisation.  If
+   you have many of these nested, it may be easier to use the
+   "Streaming.Process.Lifted" module.
+
+   These functions will all throw 'ProcessExitedUnsuccessfully' if the
+   process\/command itself fails.
+
+ -}
+module Streaming.Process
+  ( -- * High level functions
+    withStreamingProcess
+  , withStreamingCommand
+  , streamInput
+  , streamInputCommand
+  , withStreamingOutput
+  , withStreamingOutputCommand
+    -- * Lower level
+  , StreamProcess(..)
+  , switchOutputs
+  , WithStream(..)
+  , WithStream'
+  , SupplyStream(..)
+  , withStreamProcess
+  , withStreamCommand
+  , withProcessHandles
+  , processInput
+  , withProcessOutput
+    -- * Interleaved stdout and stderr
+  , StdOutErr
+  , withStreamOutputs
+    -- * Re-exports
+    -- $reexports
+  , module Data.Streaming.Process
+  , concurrently
+  ) where
+
+import           Data.ByteString.Streaming (ByteString)
+import qualified Data.ByteString.Streaming as SB
+import           Streaming                 (hoist)
+import           Streaming.Concurrent      (unbounded, withMergedStreams)
+import qualified Streaming.Prelude         as S
+
+import Control.Concurrent.Async.Lifted (concurrently)
+import Control.Monad.Base              (MonadBase)
+import Control.Monad.Catch             (MonadMask, finally, onException, throwM)
+import Control.Monad.IO.Class          (MonadIO, liftIO)
+import Control.Monad.Trans.Control     (MonadBaseControl)
+import Data.Streaming.Process
+import Data.Streaming.Process.Internal (InputSource(..), OutputSink(..))
+import System.Exit                     (ExitCode(..))
+import System.IO                       (hClose)
+import System.Process                  (CreateProcess(..),
+                                        StdStream(CreatePipe), shell)
+
+--------------------------------------------------------------------------------
+
+-- | Feeds the provided data into the specified process, then
+--   concurrently streams stdout and stderr into the provided
+--   continuation.
+--
+--   Note that the monad used in the 'StdOutErr' argument to the
+--   continuation can be different from the final result, as it's up
+--   to the caller to make sure the result is reached.
+withStreamingProcess :: (MonadBaseControl IO m, MonadIO m, MonadMask m
+                        , MonadBase IO n)
+                        => CreateProcess -> ByteString m v
+                        -> (StdOutErr n () -> m r) -> m r
+withStreamingProcess cp inp = withStreamProcess cp
+                              . flip (withProcessHandles inp)
+
+-- | As with 'withStreamingProcess', but run the specified command in
+--   a shell.
+withStreamingCommand :: (MonadBaseControl IO m, MonadIO m, MonadMask m
+                        , MonadBase IO n)
+                        => String -> ByteString m v
+                        -> (StdOutErr n () -> m r) -> m r
+withStreamingCommand = withStreamingProcess . shell
+
+-- | Feed input into a process with no expected output.
+streamInput :: (MonadIO m, MonadMask m) => CreateProcess
+               -> ByteString m r -> m r
+streamInput cp = withStreamProcess cp . flip processInput
+
+-- | As with 'streamInput' but run the specified command in a shell.
+streamInputCommand :: (MonadIO m, MonadMask m) => String
+                      -> ByteString m r -> m r
+streamInputCommand = streamInput . shell
+
+-- | Obtain the output of a process with no input (ignoring error
+--   output).
+withStreamingOutput :: (MonadIO n, MonadIO m, MonadMask m)
+                       => CreateProcess
+                       -> (ByteString n () -> m r) -> m r
+withStreamingOutput cp = withStreamProcess cp . flip withProcessOutput
+
+-- | As with 'withStreamingOutput' but run the specified command in a
+--  shell.
+withStreamingOutputCommand :: (MonadIO n, MonadIO m, MonadMask m)
+                              => String
+                              -> (ByteString n () -> m r) -> m r
+withStreamingOutputCommand = withStreamingOutput . shell
+
+--------------------------------------------------------------------------------
+
+-- | Feeds the provided data into the input handle, then concurrently
+--   streams stdout and stderr into the provided continuation.
+--
+--   Note that the monad used in the 'StdOutErr' argument to the
+--   continuation can be different from the final result, as it's up
+--   to the caller to make sure the result is reached.
+withProcessHandles :: (MonadBaseControl IO m, MonadIO m, MonadMask m, MonadBase IO n)
+                      => ByteString m v
+                      -> StreamProcess (SupplyStream m)
+                                       (WithStream' m)
+                                       (WithStream' m)
+                      -> (StdOutErr n () -> m r) -> m r
+withProcessHandles inp sp@StreamProcess{..} f =
+  snd <$> concurrently withIn withOutErr
+  where
+    withIn = supplyStream toStdin inp
+
+    withOutErr = withStreamOutputs sp f
+
+-- | Stream input into a process, ignoring any output.
+processInput :: (MonadIO m, MonadMask m)
+                => StreamProcess (SupplyStream m) ClosedStream ClosedStream
+                -> ByteString m r -> m r
+processInput StreamProcess{toStdin} = supplyStream toStdin
+
+-- | Read the output from a process, ignoring stdin and stderr.
+withProcessOutput :: (MonadIO n, MonadIO m, MonadMask m)
+                     => StreamProcess ClosedStream (WithStream n m) ClosedStream
+                     -> (ByteString n () -> m r) -> m r
+withProcessOutput StreamProcess{fromStdout} = withStream fromStdout
+
+--------------------------------------------------------------------------------
+
+-- | Represents the input and outputs for a streaming process.
+data StreamProcess stdin stdout stderr = StreamProcess
+  { toStdin    :: !stdin
+  , fromStdout :: !stdout
+  , fromStderr :: !stderr
+  } deriving (Eq, Show)
+
+-- | Switch the two outputs.  Useful for example if using
+--   'withStreamProcess' and 'withProcessHandles' but wanting to deal
+--   with any potential output from stderr before stdout.
+switchOutputs :: StreamProcess stdin stdout stderr
+                 -> StreamProcess stdin stderr stdout
+switchOutputs sp@StreamProcess{fromStdout, fromStderr}
+  = sp { fromStdout = fromStderr
+       , fromStderr = fromStdout
+       }
+
+-- | A variant of 'withCheckedProcess' that will on an exception kill
+--   the child process and attempt to perform cleanup (though you
+--   should also attempt to do so in your own code).
+--
+--   Will throw 'ProcessExitedUnsuccessfully' on a non-successful exit code.
+--
+--   Compared to @withCheckedProcessCleanup@ from @conduit-extra@,
+--   this has the three parameters grouped into 'StreamProcess' to
+--   make it more of a continuation.
+withStreamProcess :: (InputSource stdin, OutputSink stdout, OutputSink stderr
+                     , MonadIO m, MonadMask m)
+                     => CreateProcess
+                     -> (StreamProcess stdin stdout stderr -> m r) -> m r
+withStreamProcess cp f = do
+  (stdin, stdout, stderr, sph) <- streamingProcess cp
+  r <- f (StreamProcess stdin stdout stderr)
+         `onException` terminateStreamingProcess sph
+  ec <- waitForStreamingProcess sph `finally` closeStreamingProcessHandle sph
+  case ec of
+    ExitSuccess   -> return r
+    ExitFailure _ -> throwM (ProcessExitedUnsuccessfully cp ec)
+
+-- | A variant of 'withStreamProcess' that runs the provided
+--   command in a shell.
+withStreamCommand :: (InputSource stdin, OutputSink stdout, OutputSink stderr
+                     , MonadIO m, MonadMask m)
+                     => String
+                     -> (StreamProcess stdin stdout stderr -> m r) -> m r
+withStreamCommand = withStreamProcess . shell
+
+terminateStreamingProcess :: (MonadIO m) => StreamingProcessHandle -> m ()
+terminateStreamingProcess = liftIO . terminateProcess . streamingProcessHandleRaw
+
+--------------------------------------------------------------------------------
+
+-- | A representation of the concurrent streaming of both @stdout@ and
+--   @stderr@ (contrast to 'SB.hGet').
+--
+--   Note that if for example you wish to completely discard stderr,
+--   you can do so with @'hoist' 'SB.effects'@ (or just process the
+--   stdout, then run 'SB.effects' at the end to discard the stderr).
+type StdOutErr m r = ByteString (ByteString m) r
+
+-- | Get both stdout and stderr concurrently.
+withStreamOutputs :: ( MonadMask m, MonadIO m, MonadBaseControl IO m
+                     , MonadBase IO n)
+                     => StreamProcess stdin (WithStream' m) (WithStream' m)
+                     -> (StdOutErr n () -> m r) -> m r
+withStreamOutputs StreamProcess{fromStdout, fromStderr} f =
+  withStream fromStdout $ \stdout ->
+    withStream fromStderr $ \stderr ->
+      let getOut = S.map Left  . SB.toChunks $ stdout
+          getErr = S.map Right . SB.toChunks $ stderr
+      in withMergedStreams unbounded [getOut, getErr] (f . mrg)
+  where
+    mrg = SB.fromChunks . hoist SB.fromChunks . S.partitionEithers
+
+--------------------------------------------------------------------------------
+
+-- | A wrapper for being able to provide a stream of bytes.
+newtype SupplyStream m = SupplyStream { supplyStream :: forall r. ByteString m r -> m r }
+
+instance (MonadMask m, MonadIO m) => InputSource (SupplyStream m) where
+  isStdStream = (\(Just h) -> return (SupplyStream $ \inp ->
+                                       SB.hPut h inp `finally` liftIO (hClose h))
+                , Just CreatePipe
+                )
+
+-- | A wrapper for something taking a continuation with a stream of
+--   bytes as input.
+newtype WithStream n m = WithStream { withStream :: forall r. (ByteString n () -> m r) -> m r }
+
+-- | An alias for the common case of @n ~ m@.
+type WithStream' m = WithStream m m
+
+instance (MonadIO m, MonadMask m, MonadIO n) => OutputSink (WithStream n m) where
+  osStdStream = (\(Just h) -> return (WithStream $ \f ->
+                                       f (SB.hGetContents h) `finally` liftIO (hClose h))
+                , Just CreatePipe
+                )
+
+--------------------------------------------------------------------------------
+
+{- $reexports
+
+All of "Data.Streaming.Process" is available for you to use.
+
+The 'concurrently' function will probably be useful if manually
+handling process inputs and outputs.
+
+-}
diff --git a/src/Streaming/Process/Lifted.hs b/src/Streaming/Process/Lifted.hs
new file mode 100644
--- /dev/null
+++ b/src/Streaming/Process/Lifted.hs
@@ -0,0 +1,197 @@
+{-# LANGUAGE FlexibleContexts, MultiParamTypeClasses, TypeFamilies #-}
+
+{- |
+   Module      : Streaming.Process.Lifted
+   Description : Lifted variants of "Streaming.Process"
+   Copyright   : (c) Ivan Lazar Miljenovic
+   License     : MIT
+   Maintainer  : Ivan.Miljenovic@gmail.com
+
+   This module defines variants of those in "Streaming.Process" for
+   use with the 'Withable' class, found in the @streaming-with@
+   package.
+
+   __WARNING:__ If using this module, you will need to have
+   @ghc-options -threaded@ in your @.cabal@ file otherwise it will
+   likely hang!
+
+   These functions will all throw 'ProcessExitedUnsuccessfully' if the
+   process\/command itself fails.
+
+ -}
+module Streaming.Process.Lifted
+  ( -- * High level functions
+    withStreamingProcess
+  , withStreamingCommand
+  , streamInput
+  , streamInputCommand
+  , withStreamingOutput
+  , withStreamingOutputCommand
+    -- * Lower level
+  , StreamProcess(..)
+  , switchOutputs
+  , WithStream(WithStream)
+  , WithStream'
+  , withStream
+  , SupplyStream(SupplyStream)
+  , supplyStream
+  , withStreamProcess
+  , withStreamCommand
+  , withProcessHandles
+  , processInput
+  , withProcessOutput
+    -- * Interleaved stdout and stderr
+  , StdOutErr
+  , withStreamOutputs
+    -- * Re-exports
+    -- $reexports
+  , module Data.Streaming.Process
+  , concurrently
+  ) where
+
+import           Streaming.Process (StdOutErr, StreamProcess(..),
+                                    SupplyStream(SupplyStream),
+                                    WithStream(WithStream), WithStream',
+                                    switchOutputs)
+import qualified Streaming.Process as SP
+
+import Data.ByteString.Streaming (ByteString)
+import Streaming.With.Lifted     (Withable(..))
+
+import Control.Concurrent.Async.Lifted (concurrently)
+import Control.Monad.Base              (MonadBase)
+import Control.Monad.IO.Class          (MonadIO)
+import Control.Monad.Trans.Control     (MonadBaseControl)
+import Data.Streaming.Process
+import System.Process                  (shell)
+
+--------------------------------------------------------------------------------
+
+-- | Feeds the provided data into the specified process, then
+--   concurrently streams stdout and stderr into the provided
+--   continuation.
+--
+--   Note that the monad used in the 'StdOutErr' argument to the
+--   continuation can be different from the final result, as it's up
+--   to the caller to make sure the result is reached.
+withStreamingProcess :: ( Withable w, MonadBaseControl IO (WithMonad w)
+                        , MonadBase IO n)
+                        => CreateProcess -> ByteString (WithMonad w) v
+                        -> w (StdOutErr n ())
+withStreamingProcess cp inp = liftWith (SP.withStreamingProcess cp inp)
+
+-- | As with 'withStreamingProcess', but run the specified command in
+--   a shell.
+withStreamingCommand :: ( Withable w, MonadBaseControl IO (WithMonad w)
+                        , MonadBase IO n)
+                        => String -> ByteString (WithMonad w) v
+                        -> w (StdOutErr n ())
+withStreamingCommand = withStreamingProcess . shell
+
+-- | Feed input into a process with no expected output.
+streamInput :: (Withable w) => CreateProcess
+               -> ByteString (WithMonad w) r -> w r
+streamInput cp inp = liftAction (SP.streamInput cp inp)
+
+-- | As with 'streamInput' but run the specified command in a shell.
+streamInputCommand :: (Withable w) => String
+                      -> ByteString (WithMonad w) r -> w r
+streamInputCommand = streamInput . shell
+
+-- | Obtain the output of a process with no input (ignoring error
+--   output).
+withStreamingOutput :: (Withable w, MonadIO n)
+                       => CreateProcess
+                       -> w (ByteString n ())
+withStreamingOutput cp = liftWith (SP.withStreamingOutput cp)
+
+-- | As with 'withStreamingOutput' but run the specified command in a
+--  shell.
+withStreamingOutputCommand :: (Withable w, MonadIO n)
+                              => String
+                              -> w (ByteString n ())
+withStreamingOutputCommand = withStreamingOutput . shell
+
+--------------------------------------------------------------------------------
+
+-- | Feeds the provided data into the input handle, then concurrently
+--   streams stdout and stderr into the provided continuation.
+--
+--   Note that the monad used in the 'StdOutErr' argument to the
+--   continuation can be different from the final result, as it's up
+--   to the caller to make sure the result is reached.
+withProcessHandles :: (Withable w, m ~ WithMonad w, MonadBaseControl IO m, MonadBase IO n)
+                      => ByteString m v
+                      -> StreamProcess (SupplyStream m)
+                                       (WithStream' m)
+                                       (WithStream' m)
+                      -> w (StdOutErr n ())
+withProcessHandles inp sp = liftWith (SP.withProcessHandles inp sp)
+
+-- | Stream input into a process, ignoring any output.
+processInput :: (Withable w)
+                => StreamProcess (SupplyStream (WithMonad w)) ClosedStream ClosedStream
+                -> ByteString (WithMonad w) r -> w r
+processInput sp inp = liftAction (SP.processInput sp inp)
+
+-- | Read the output from a process, ignoring stdin and stderr.
+withProcessOutput :: (Withable w, MonadIO n)
+                     => StreamProcess ClosedStream (WithStream n (WithMonad w)) ClosedStream
+                     -> w (ByteString n ())
+withProcessOutput sp = liftWith (SP.withProcessOutput sp)
+
+--------------------------------------------------------------------------------
+
+-- | A variant of 'withCheckedProcess' that will on an exception kill
+--   the child process and attempt to perform cleanup (though you
+--   should also attempt to do so in your own code).
+--
+--   Will throw 'ProcessExitedUnsuccessfully' on a non-successful exit code.
+--
+--   Compared to @withCheckedProcessCleanup@ from @conduit-extra@,
+--   this has the three parameters grouped into 'StreamProcess' to
+--   make it more of a continuation.
+withStreamProcess :: (InputSource stdin, OutputSink stdout, OutputSink stderr
+                     , Withable w)
+                     => CreateProcess -> w (StreamProcess stdin stdout stderr)
+withStreamProcess cp = liftWith (SP.withStreamProcess cp)
+
+-- | A variant of 'withStreamProcess' that runs the provided
+--   command in a shell.
+withStreamCommand :: (InputSource stdin, OutputSink stdout, OutputSink stderr
+                     , Withable w)
+                     => String -> w (StreamProcess stdin stdout stderr)
+withStreamCommand = withStreamProcess . shell
+
+--------------------------------------------------------------------------------
+
+-- | Get both stdout and stderr concurrently.
+withStreamOutputs :: ( Withable w, m ~ WithMonad w, MonadBaseControl IO m
+                     , MonadBase IO n)
+                     => StreamProcess stdin (WithStream' m) (WithStream' m)
+                     -> w (StdOutErr n ())
+withStreamOutputs sp = liftWith (SP.withStreamOutputs sp)
+
+--------------------------------------------------------------------------------
+
+-- | Please note that - unlike the version in "Streaming.Process" -
+--   this is /not/ a record selector.
+supplyStream :: (Withable w) => SupplyStream (WithMonad w)
+                -> ByteString (WithMonad w) r -> w r
+supplyStream ss inp = liftAction (SP.supplyStream ss inp)
+
+-- | Please note that - unlike the version in "Streaming.Process" -
+--   this is /not/ a record selector.
+withStream :: (Withable w) => WithStream n (WithMonad w) -> w (ByteString n ())
+withStream ws = liftWith (SP.withStream ws)
+
+--------------------------------------------------------------------------------
+
+{- $reexports
+
+All of "Data.Streaming.Process" is available for you to use.
+
+The 'concurrently' function will probably be useful if manually
+handling process inputs and outputs.
+
+-}
diff --git a/streaming-process.cabal b/streaming-process.cabal
new file mode 100644
--- /dev/null
+++ b/streaming-process.cabal
@@ -0,0 +1,58 @@
+name:                streaming-process
+version:             0.1.0.0
+synopsis:            Streaming support for running system process
+description:
+  Stream data in and out of external commands.  Configuration options
+  are available to choose which inputs and outputs to use.
+license:             MIT
+license-file:        LICENSE
+author:              Ivan Lazar Miljenovic
+maintainer:          Ivan.Miljenovic@gmail.com
+copyright:           Ivan Lazar Miljenovic
+category:            Data, Streaming
+build-type:          Simple
+extra-source-files:  ChangeLog.md, README.md
+cabal-version:       >=1.10
+tested-with:         GHC == 7.10.2, GHC == 8.0.2, GHC == 8.2.2,
+                     GHC == 8.4.1, GHC == 8.5.*
+
+source-repository head
+  type:     git
+  location: https://github.com/ivan-m/streaming-process.git
+
+library
+  exposed-modules:     Streaming.Process
+                     , Streaming.Process.Lifted
+  build-depends:       base == 4.*
+                     , bytestring
+                     , directory >= 1.2 && < 1.4
+                     , exceptions >= 0.6 && < 0.11
+                     , lifted-async >= 0.9.1 && < 0.11
+                     , monad-control == 1.*
+                     , process >= 1.2.0.0 && < 1.7
+                     , streaming >= 0.1.4.0 && < 0.3
+                     , streaming-bytestring >= 0.1.4.5 && < 0.2
+                     , streaming-commons >= 0.1.16 && < 0.3
+                     , streaming-concurrency == 0.3.*
+                     , streaming-with == 0.2.*
+                     , transformers
+                     , transformers-base
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+
+test-suite simple-processes
+  type:                exitcode-stdio-1.0
+  main-is:             simple.hs
+  other-modules:       Paths_streaming_process
+  build-depends:       streaming-process
+                     , base
+                     , bytestring
+                     , hspec == 2.4.*
+                     , QuickCheck == 2.*
+                     , quickcheck-instances
+                     , streaming
+                     , streaming-bytestring
+  hs-source-dirs:      test
+  default-language:    Haskell2010
+  ghc-options:         -Wall -threaded
diff --git a/test/simple.hs b/test/simple.hs
new file mode 100644
--- /dev/null
+++ b/test/simple.hs
@@ -0,0 +1,57 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+{- |
+   Module      : Main
+   Description : Simple process testing
+   Copyright   : (c) Ivan Lazar Miljenovic
+   License     : MIT
+   Maintainer  : Ivan.Miljenovic@gmail.com
+
+   This will probably only run on *nix systems.
+
+ -}
+module Main (main) where
+
+import Streaming.Process
+
+import qualified Data.ByteString.Lazy      as LB
+import           Data.ByteString.Streaming (ByteString)
+import qualified Data.ByteString.Streaming as B
+import           Streaming                 (Of(..))
+
+import Test.Hspec                (describe, hspec)
+import Test.Hspec.QuickCheck     (prop)
+import Test.QuickCheck           (Property, ioProperty)
+import Test.QuickCheck.Instances ()
+
+--------------------------------------------------------------------------------
+
+main :: IO ()
+main = hspec $
+  describe "cat" $ do
+    prop "stdout only" prop_cat
+    prop "stdout and stderr" prop_catBoth
+
+-- Use lazy bytestrings to avoid issues with it being chunked
+-- differently.
+--
+-- But don't take them as arguments
+prop_cat :: LB.ByteString -> Property
+prop_cat bs = ioProperty (noStdErr "cat" (B.fromLazy bs) (fmap (bs==) . B.toLazy_))
+
+prop_catBoth :: LB.ByteString -> Property
+prop_catBoth bs = ioProperty $
+  withStreamingCommand "cat | tee /dev/stderr" (B.fromLazy bs) $
+    fmap (uncurry' ((&&) . isSame))
+    . B.toLazy
+    . fmap isSame
+    . B.toLazy_
+  where
+    isSame = (bs==)
+
+noStdErr :: String -> ByteString IO r -> (ByteString IO () -> IO v) -> IO v
+noStdErr cmd inp f = withStreamCommand cmd $ \(StreamProcess stdin stdout ClosedStream) ->
+  snd <$> concurrently (supplyStream stdin inp) (withStream stdout f)
+
+uncurry' :: (a -> b -> c) -> Of a b -> c
+uncurry' f (a :> b) = f a b
