diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2012 Mauro Taraborelli
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+3. Neither the name of the author nor the names of his contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE 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 AUTHORS 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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+HaskDeep
+========
+
+What is it?
+-----------
+Command line tool that computes file hashes traversing recursively through
+a directory structure.
+Known hashes are saved to file and they can be used to verify the original
+files or a copy of them.
+
+Quick start
+-----------
+Execute haskdeep without arguments and it will show you the help text:
+
+    user@host:~$ haskdeep
+
+    Usage: haskdeep COMMAND [-c|--computation MODE] [-r|--root DIRNAME] [-k|--known FILENAME] [-i|--ignore RULE]
+      Computes hashes and audit a set of files
+
+    Available options:
+      -c,--computation MODE    md5 | sha1 | sha256 | skein512 - default md5
+      -r,--root DIRNAME        Root directory - default current directory
+      -k,--known FILENAME      Known hashes file - default known.haskdeep
+      -i,--ignore RULE         Regex to ignore files or directories
+      -h,--help                Show this help text
+
+    Available commands:
+      compute                  Computes file hashes and saves them to known hashes file
+      audit                    Audits files comparing them to known hashes
+
+Default usage:
+
+1. create known hashes of files contained in a root directory (traversed recursively)
+
+        user@host:~$ haskdeep compute -c md5 -r myimportantfiles/ -k knownhashes.txt -i "tmp|\.log"
+
+2. verify a copy of the same files comparing them with known hashes
+
+        user@host:~$ haskdeep audit -c md5 -r copyofmyimportantfiles/ -k knownhashes.txt -i "tmp|\.log"
+
+Licensing
+---------
+Please see the file called LICENSE.
+
+Reference
+---------
+Heavily inspired by `hashdeep`: <http://md5deep.sourceforge.net/>
+
+Contacts
+--------
+For question and comments:
+
+- [MauroTaraborelli@gmail.com](mailto:MauroTaraborelli@gmail.com)
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/haskdeep.cabal b/haskdeep.cabal
new file mode 100644
--- /dev/null
+++ b/haskdeep.cabal
@@ -0,0 +1,88 @@
+name:                haskdeep
+version:             0.1.0.0
+cabal-version:       >=1.8
+build-type:          Simple
+license:             BSD3
+license-file:        LICENSE
+copyright:           © 2012 Mauro Taraborelli
+author:              Mauro Taraborelli
+maintainer:          maurotaraborelli@gmail.com
+stability:           experimental
+homepage:            https://github.com/maurotrb/haskdeep
+bug-reports:         https://github.com/maurotrb/haskdeep/issues
+synopsis:            Computes and audits file hashes.
+description:
+    A command line application that computes file hashes traversing recursively
+    through a directory structure.
+    .
+    The known hashes can be used to audit the same directory structure or a copy
+    of it.
+    .
+    Execute haskdeep without arguments and it will show you the help text:
+    .
+    > user@host:~$ haskdeep
+    >
+    >  Usage: haskdeep COMMAND [-c|--computation MODE] [-r|--root DIRNAME] [-k|--known FILENAME] [-i|--ignore RULE]
+    >    Computes hashes and audit a set of files
+    >
+    >  Available options:
+    >    -c,--computation MODE    md5 | sha1 | sha256 | skein512 - default md5
+    >    -r,--root DIRNAME        Root directory - default current directory
+    >    -k,--known FILENAME      Known hashes file - default known.haskdeep
+    >    -i,--ignore RULE         Regex to ignore files or directories
+    >    -h,--help                Show this help text
+    >
+    >  Available commands:
+    >    compute                  Computes file hashes and saves them to known hashes file
+    >    audit                    Audits files comparing them to known hashes
+    .
+    Default usage:
+    .
+    1. create known hashes of files contained in a root directory (traversed recursively)
+    .
+    >  user@host:~$ haskdeep compute -c md5 -r myimportantfiles/ -k knownhashes.txt -i "tmp|\.log"
+    .
+    2. verify a copy of the same files comparing them with known hashes
+    .
+    >  user@host:~$ haskdeep audit -c md5 -r copyofmyimportantfiles/ -k knownhashes.txt -i "tmp|\.log"
+    .
+    Heavily inspired by @hashdeep@: <http://md5deep.sourceforge.net/>
+category:            Console, Cryptography, System
+extra-source-files:
+    README.md
+
+executable haskdeep
+  hs-source-dirs:   src
+  main-is:          Main.hs
+  other-modules:
+    Options
+    HaskDeep
+    HaskDeep.Computation
+    HaskDeep.ComputationMode
+    HaskDeep.Configuration
+    HaskDeep.HashSet
+    HaskDeep.KnownHash.Reader
+    HaskDeep.KnownHash.Writer
+  build-depends:
+      attoparsec           ==0.10.*
+    , attoparsec-conduit   ==0.5.*
+    , base                 ==4.5.*
+    , bytestring           ==0.9.*
+    , cereal               ==0.3.*
+    , conduit              ==0.5.*
+    , containers           ==0.4.*
+    , cryptohash           ==0.7.*
+    , crypto-api           ==0.10.*
+    , crypto-conduit       ==0.4.*
+    , filesystem-conduit   ==0.5.*
+    , optparse-applicative ==0.4.*
+    , system-fileio        ==0.3.*
+    , system-filepath      ==0.4.*
+    , text                 ==0.11.*
+    , regex-tdfa           ==1.1.*
+    , regex-tdfa-text      ==1.0.*
+  ghc-options: -Wall
+
+source-repository head
+  type:     git
+  location: git://github.com/maurotrb/haskdeep.git
diff --git a/src/HaskDeep.hs b/src/HaskDeep.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskDeep.hs
@@ -0,0 +1,44 @@
+-- |
+-- Module      :  HaskDeep
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through a directory structure.
+-- Uses a list of known hashes to audit a set of files.
+
+module HaskDeep
+    (
+     -- * The @HashSet@ type
+     HashSet()
+    ,audit
+
+     -- * Configuration
+    ,HaskDeepConfiguration(..)
+    ,defaultHaskDeepConfiguration
+
+     -- * Computation modes
+    ,ComputationMode (..)
+    ,md5hash
+    ,sha1hash
+    ,sha256hash
+    ,skein512hash
+
+     -- * Computation
+    ,compute
+
+     -- * Known hashes file
+    ,readHashes
+    ,writeHashes
+    )
+where
+
+import HaskDeep.Computation
+import HaskDeep.ComputationMode
+import HaskDeep.Configuration
+import HaskDeep.HashSet
+import HaskDeep.KnownHash.Reader
+import HaskDeep.KnownHash.Writer
diff --git a/src/HaskDeep/Computation.hs b/src/HaskDeep/Computation.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskDeep/Computation.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DoAndIfThenElse #-}
+-- |
+-- Module      :  HaskDeep.Computation
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through a directory structure.
+-- Uses a list of known hashes to audit a set of files.
+--
+-- Internal module.
+
+module HaskDeep.Computation
+    (
+     -- * Computation
+     compute  -- Hash ctx a => HaskDeepConfiguration -> ComputationMode a -> IO HashSet
+    )
+where
+
+import           Control.Monad (liftM)
+import           Data.Maybe (fromJust)
+import           Prelude hiding (FilePath)
+
+import           Crypto.Classes (Hash)
+import qualified Crypto.Conduit as CC
+import           Data.Conduit (($$))
+import qualified Data.Conduit as C
+import qualified Data.Conduit.Filesystem as CF
+import qualified Data.Conduit.List as CL
+import qualified Data.Text as T
+import qualified Filesystem as FS
+import           Filesystem.Path.CurrentOS (FilePath)
+import qualified Filesystem.Path.CurrentOS as FSC
+import           Text.Regex.TDFA ((=~))
+import           Text.Regex.TDFA.Text ()
+
+import           HaskDeep.Configuration
+import           HaskDeep.ComputationMode
+import           HaskDeep.HashSet (HashInfo(..), HashSet)
+import qualified HaskDeep.HashSet as HS
+
+-- | Compute @HashSet@ traversing recursively through the directory structure.
+compute :: Hash ctx a => HaskDeepConfiguration
+        -> ComputationMode a
+        -> IO HashSet
+compute conf cm = CF.traverse False root $$ CL.foldM insert_hash empty_with_symbol
+    where
+      root              = rootDirectory conf
+      rule              = ignoreRule conf
+      empty_with_symbol = HS.setSymbol (symbol cm) HS.empty
+
+      insert_hash :: HashSet -> FilePath -> IO HashSet
+      insert_hash hs fp = do let fpt = fpToText $ relativize root fp
+                             if ignore fpt rule
+                             then return hs
+                             else do s <- FS.getSize fp
+                                     h <- liftM (runComputation cm) $ C.runResourceT
+                                          $ CF.sourceFile fp $$ CC.sinkHash
+                                     return $ HS.insert (HashInfo fpt s h) hs
+
+relativize :: FilePath -> FilePath -> FilePath
+relativize root fp = fromJust $ FSC.stripPrefix root fp
+
+fpToText :: FilePath -> T.Text
+fpToText = either id id . FSC.toText
+
+ignore :: T.Text -> Maybe T.Text -> Bool
+ignore fpt (Just rule) = fpt =~ rule
+ignore _   Nothing     = False
diff --git a/src/HaskDeep/ComputationMode.hs b/src/HaskDeep/ComputationMode.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskDeep/ComputationMode.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module      :  HaskDeep.Computation
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through a directory structure.
+-- Uses a list of known hashes to audit a set of files.
+--
+-- Internal module.
+
+module HaskDeep.ComputationMode
+    (
+     -- * Computation modes
+     ComputationMode (..)
+    ,md5hash
+    ,sha1hash
+    ,sha256hash
+    ,skein512hash
+    )
+where
+
+import           Data.Bits ((.&.), shiftR)
+
+import           Crypto.Hash.MD5 (MD5)
+import           Crypto.Hash.SHA1 (SHA1)
+import           Crypto.Hash.SHA256 (SHA256)
+import           Crypto.Hash.Skein512 (Skein512)
+import           Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.Serialize as S
+import qualified Data.Text as T
+
+-- | Algorithm to compute hash.
+data ComputationMode a = ComputationMode
+    { symbol         :: T.Text           -- ^ Computation mode symbol
+    , runComputation :: a -> ByteString  -- ^ Computation function
+    }
+
+-- | MD5 computation.
+md5hash  :: ComputationMode MD5
+md5hash  = ComputationMode "md5" (toHex . S.encode)
+
+-- | SHA1 computation.
+sha1hash :: ComputationMode SHA1
+sha1hash = ComputationMode "sha1" (toHex . S.encode)
+
+-- | SHA256 computation.
+sha256hash :: ComputationMode SHA256
+sha256hash = ComputationMode "sha256" (toHex . S.encode)
+
+-- | Skein512 computation.
+skein512hash :: ComputationMode Skein512
+skein512hash = ComputationMode "skein512" (toHex . S.encode)
+
+-- Only with ByteString 0.10
+--toHex :: ByteString -> ByteString
+--toHex = BS.concat . BL.toChunks . BB.toLazyByteString . BA.byteStringHexFixed
+
+-- Taken from
+-- http://stackoverflow.com/questions/10099921/efficiently-turn-a-bytestring-into-a-hex-representation
+toHex :: ByteString -> ByteString
+toHex bs0 =
+    fst $ BS.unfoldrN (BS.length bs0 * 2) go (Left bs0)
+  where
+    go (Left bs) =
+        case BS.uncons bs of
+            Nothing -> Nothing
+            Just (w, bs') ->
+                let w1 = w `shiftR` 4
+                    w2 = w .&. 15
+                    c1 = toC w1
+                    c2 = toC w2
+                 in Just (c1, Right (c2, bs'))
+    go (Right (c, bs)) = Just (c, Left bs)
+
+    toC w
+        | w < 10 = w + 48
+        | otherwise = w + 87
diff --git a/src/HaskDeep/Configuration.hs b/src/HaskDeep/Configuration.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskDeep/Configuration.hs
@@ -0,0 +1,42 @@
+-- |
+-- Module      :  HaskDeep.Configuration
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through a directory structure.
+-- Uses a list of known hashes to audit a set of files.
+--
+-- Internal module.
+
+module HaskDeep.Configuration
+    (
+     -- * Configuration
+     HaskDeepConfiguration (..)
+    ,defaultHaskDeepConfiguration
+    )
+where
+
+import           Prelude hiding (FilePath)
+
+import           Data.Text (Text)
+import           Filesystem.Path (FilePath)
+import qualified Filesystem.Path.CurrentOS as FSC
+
+-- | HaskDeep configuration.
+data HaskDeepConfiguration = HaskDeepConfiguration
+    { rootDirectory   :: FilePath    -- ^ Root directory
+    , knownHashes     :: FilePath    -- ^ Known hashes file
+    , ignoreRule      :: Maybe Text  -- ^ Ignore rule (Regex)
+    }
+
+-- | HaskDeep default configuration.
+defaultHaskDeepConfiguration :: HaskDeepConfiguration
+defaultHaskDeepConfiguration = HaskDeepConfiguration
+    { rootDirectory   = FSC.decodeString "."
+    , knownHashes     = FSC.decodeString "known.haskdeep"
+    , ignoreRule      = Nothing
+    }
diff --git a/src/HaskDeep/HashSet.hs b/src/HaskDeep/HashSet.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskDeep/HashSet.hs
@@ -0,0 +1,133 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module      :  HaskDeep.HashSet
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through a directory structure.
+-- Uses a list of known hashes to audit a set of files.
+--
+-- Internal module.
+
+module HaskDeep.HashSet
+    (
+     -- * The @HashSet@ type
+     HashSet(..)
+    ,setSymbol     -- Text -> HashSet -> HashSet
+    ,empty         -- HashSet
+    ,insert        -- HashInfo -> HashSet -> HashSet
+    ,fromList      -- [HashInfo] -> HashSet
+    ,toAscList     -- HashSet -> [HashInfo]
+    ,audit         -- HashSet -> HashSet -> (HashSet, HashSet)
+
+     -- * The @HashInfo@ type
+    ,HashInfo(..)
+    ,toByteString  -- HashInfo -> ByteString
+    )
+where
+
+import           Data.List (intercalate)
+
+import           Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Char8 as B8
+import           Data.Set (Set)
+import qualified Data.Set as Set
+import           Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
+
+-- | Information about the hashed file.
+data HashInfo = HashInfo
+    { file :: Text        -- ^ Relative file path
+    , size :: Integer     -- ^ File size in byte
+    , hash :: ByteString  -- ^ File hash
+    } deriving (Eq, Ord)
+
+instance Show HashInfo where
+    show (HashInfo f s h) = intercalate "," [ show s
+                                            , B8.unpack h
+                                            , T.unpack f ]
+
+toByteString :: HashInfo -> ByteString
+toByteString (HashInfo f s h) = BS.intercalate commaBS [ B8.pack (show s)
+                                                       , h
+                                                       , TE.encodeUtf8 f ]
+
+commaBS :: ByteString
+commaBS = B8.singleton ','
+
+-- | Hashed files.
+data HashSet = HashSet
+    { filesCount :: Integer       -- ^ Number of hashed files
+    , sizeSum    :: Integer       -- ^ Total size in bytes of hashed files
+    , compSymbol :: Text          -- ^ Computation mode symbol
+    , hashSet    :: Set HashInfo  -- ^ Hashes
+    } deriving (Eq, Ord)
+
+instance Show HashSet where
+    show (HashSet 0  _  _  _ ) = "No files."
+    show (HashSet fc ss cs hs) = unlines [ "Files count  : " ++ show fc
+                                         , "Files size   : " ++ show ss
+                                         , "Computed with: " ++ T.unpack cs
+                                         , "--"
+                                         , (unlines . map show . Set.toAscList) hs ]
+
+-- | Set computation mode symbol.
+setSymbol :: Text -> HashSet -> HashSet
+setSymbol cs hs = hs { compSymbol = cs }
+
+-- | Create an empty @HashSet@.
+empty :: HashSet
+empty = HashSet 0 0 T.empty Set.empty
+
+-- | Insert an @HashInfo@ into an @HashSet@
+insert :: HashInfo -> HashSet -> HashSet
+insert hi hs = hs
+               { filesCount = filesCount hs + 1
+               , sizeSum    = sizeSum hs + size hi
+               , hashSet    = Set.insert hi $ hashSet hs
+               }
+
+-- | Create an @HashSet@ from a list of @HashInfo@
+fromList :: [HashInfo] -> HashSet
+fromList his = HashSet fc ss T.empty hs
+    where
+      hs = Set.fromList his
+      fc = countFiles hs
+      ss = sumSize hs
+
+-- | Create an ordered list of @HashInfo@ from an @HashSet@.
+toAscList :: HashSet -> [HashInfo]
+toAscList (HashSet _ _ _ hs) = Set.toAscList hs
+
+-- | Get the number of hashed files
+countFiles :: Set HashInfo -> Integer
+countFiles = fromIntegral . Set.size
+
+-- | Get the total size in bytes of hashed files
+sumSize :: Set HashInfo -> Integer
+sumSize = Set.foldr sumSize' 0
+    where
+      sumSize' hi ss = ss + size hi
+
+-- | Compare two @HashSet@ and return the not matching @HashInfo@.
+--
+-- Not matching means all the HashInfo of the first HashSet not present in the second HashSet
+-- and all the HashInfo of the second HashSet not present in the first HashSet.
+audit :: HashSet -> HashSet -> (HashSet, HashSet)
+audit (HashSet _ _ cs1 hs1) (HashSet _ _ cs2 hs2) =
+    ( HashSet hs1_no_match_filescount hs1_no_match_sizesum cs1 hs1_no_match
+    , HashSet hs2_no_match_filescount hs2_no_match_sizesum cs2 hs2_no_match )
+        where
+          matching                = hs1 `Set.intersection` hs2
+          hs1_no_match            = hs1 `Set.difference` matching
+          hs2_no_match            = hs2 `Set.difference` matching
+          hs1_no_match_filescount = countFiles hs1_no_match
+          hs1_no_match_sizesum    = sumSize hs1_no_match
+          hs2_no_match_filescount = countFiles hs2_no_match
+          hs2_no_match_sizesum    = sumSize hs2_no_match
diff --git a/src/HaskDeep/KnownHash/Reader.hs b/src/HaskDeep/KnownHash/Reader.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskDeep/KnownHash/Reader.hs
@@ -0,0 +1,88 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module      :  HaskDeep.Computation.Reader
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through a directory structure.
+-- Uses a list of known hashes to audit a set of files.
+--
+-- Internal module.
+
+module HaskDeep.KnownHash.Reader
+    (
+     -- * Read the known hashes file
+     readHashes  -- HaskDeepConfiguration -> IO HashSet
+    )
+where
+
+import           Control.Applicative ((<*), many)
+import           Control.Monad (liftM)
+import           Data.Maybe(fromJust)
+import           Data.Word (Word8)
+import           Prelude hiding (FilePath)
+
+import           Data.Attoparsec (Parser)
+import qualified Data.Attoparsec as A
+import qualified Data.Attoparsec.Char8 as A8
+import qualified Data.Attoparsec.Combinator as AC
+import qualified Data.ByteString.Char8 as B8
+import           Data.Conduit (($$))
+import qualified Data.Conduit as C
+import qualified Data.Conduit.Attoparsec as CA
+import qualified Data.Conduit.Filesystem as CF
+import           Data.Text ()
+import qualified Data.Text.Encoding as TE
+import           Filesystem.Path ()
+
+import           HaskDeep.Configuration
+import           HaskDeep.HashSet (HashSet, HashInfo(..))
+import qualified HaskDeep.HashSet as HS
+
+-- | Read known hashes file into an @HashSet@.
+readHashes :: HaskDeepConfiguration -- ^ Configuration
+           -> IO HashSet            -- ^ @HashSet@ red from file
+readHashes conf = liftM HS.fromList $ C.runResourceT
+                  $ CF.sourceFile (knownHashes conf) $$ CA.sinkParser knownHashesP
+
+
+-- Parsers
+
+knownHashesP :: Parser [HashInfo]
+knownHashesP = do _ <- AC.count 2 headerP
+                  _ <- many commentP
+                  AC.many1 fileinfoP
+
+headerP :: Parser ()
+headerP = do _ <- A8.string "%%%%"
+             _ <- A.skipWhile (not . isNewline)
+             skipNewline
+
+commentP :: Parser ()
+commentP = do _ <- A8.char8 '#'
+              A.skipWhile (not . isNewline)
+              skipNewline
+
+fileinfoP :: Parser HashInfo
+fileinfoP = do sizep  <- A.takeTill isComma   <* skipComma
+               hashp  <- A.takeTill isComma   <* skipComma
+               filep  <- A.takeTill isNewline <* skipNewline
+               let filep'      = TE.decodeUtf8 filep
+                   (sizep', _) = fromJust $ B8.readInteger sizep
+               return $ HashInfo filep' sizep' hashp
+
+isComma   :: Word8 -> Bool
+isComma w = w == 44
+
+skipComma :: Parser ()
+skipComma = A.skip isComma
+
+isNewline   :: Word8 -> Bool
+isNewline w = w == 10
+
+skipNewline :: Parser ()
+skipNewline = A.skip isNewline
diff --git a/src/HaskDeep/KnownHash/Writer.hs b/src/HaskDeep/KnownHash/Writer.hs
new file mode 100644
--- /dev/null
+++ b/src/HaskDeep/KnownHash/Writer.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module      :  HaskDeep.Computation.Writer
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through a directory structure.
+-- Uses a list of known hashes to audit a set of files.
+--
+-- Internal module.
+
+module HaskDeep.KnownHash.Writer
+    (
+     -- * Write the known hashes file
+     writeHashes  -- HaskDeepConfiguration -> HashSet -> IO ()
+    )
+where
+
+import           Prelude hiding (FilePath)
+
+import           Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Char8 as B8
+import           Data.Conduit (($$))
+import qualified Data.Conduit as C
+import qualified Data.Conduit.Filesystem as CF
+import qualified Data.Conduit.List as CL
+import           Data.Text ()
+import qualified Data.Text.Encoding as TE
+import           Filesystem.Path ()
+import qualified Filesystem.Path.CurrentOS as FSC
+
+import           HaskDeep.Configuration
+import           HaskDeep.HashSet (HashSet)
+import qualified HaskDeep.HashSet as HS
+
+-- | Write an @HashSet@ to a known hashes file.
+writeHashes :: HaskDeepConfiguration -- ^ Configuration
+            -> HashSet               -- ^ @HashSet@ to write
+            -> IO ()
+writeHashes conf hs = C.runResourceT $ CL.sourceList bs_known $$ CF.sinkFile (knownHashes conf)
+    where
+      newline       = "\n" :: ByteString
+      root          =  TE.encodeUtf8 $ either id id $ FSC.toText $ rootDirectory conf
+      ignore        = case ignoreRule conf of
+                        (Just rule) -> TE.encodeUtf8 rule
+                        Nothing     -> BS.empty
+      files_count   = B8.pack $ show $ HS.filesCount hs
+      size_sum      = B8.pack $ show $ HS.sizeSum hs
+      known_header1 = "%%%% HASHDEEP-1.0"
+                      `BS.append` newline
+      known_header2 = "%%%% size,"
+                      `BS.append` TE.encodeUtf8 (HS.compSymbol hs)
+                      `BS.append` ",filename"
+                      `BS.append` newline
+      bs_comments   = map (flip BS.append newline . BS.append "## ")
+                      [ "Root directory: " `BS.append` root
+                      , "Ignore rule   : " `BS.append` ignore
+                      , "Files count   : " `BS.append` files_count
+                      , "Files size    : " `BS.append` size_sum
+                      , "" ]
+      bs_hash_set   = map (flip BS.append newline . HS.toByteString) $ HS.toAscList hs
+      bs_known      = [known_header1, known_header2] ++ bs_comments ++ bs_hash_set
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,50 @@
+-- |
+-- Module      :  HaskDeep.Command.Main
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through a directory structure.
+-- Uses a list of known hashes to audit a set of files.
+
+module Main
+    (
+     main
+    )
+where
+
+import           Prelude hiding (FilePath)
+
+import           Crypto.Classes (Hash)
+import qualified Options.Applicative as OA
+
+import           HaskDeep
+import           Options
+
+main :: IO ()
+main = OA.execParser optionsPI >>= haskdeep
+
+haskdeep :: Options -> IO ()
+haskdeep (Options OptComputation OptMD5      conf) = execComputation conf md5hash
+haskdeep (Options OptComputation OptSHA1     conf) = execComputation conf sha1hash
+haskdeep (Options OptComputation OptSHA256   conf) = execComputation conf sha256hash
+haskdeep (Options OptComputation OptSkein512 conf) = execComputation conf skein512hash
+haskdeep (Options OptAudit       OptMD5      conf) = execAudit       conf md5hash
+haskdeep (Options OptAudit       OptSHA1     conf) = execAudit       conf sha1hash
+haskdeep (Options OptAudit       OptSHA256   conf) = execAudit       conf sha256hash
+haskdeep (Options OptAudit       OptSkein512 conf) = execAudit       conf skein512hash
+
+execComputation :: Hash ctx a => HaskDeepConfiguration -> ComputationMode a -> IO ()
+execComputation conf cm = writeHashes conf =<< compute conf cm
+
+execAudit :: Hash ctx a => HaskDeepConfiguration -> ComputationMode a -> IO ()
+execAudit conf cm = do hashset1 <- readHashes conf
+                       hashset2 <- compute conf cm
+                       let (hashdiff1, hashdiff2) = audit hashset1 hashset2
+                       putStrLn "**** Not present in known hashes"
+                       print hashdiff1
+                       putStrLn "**** Present only in root directory"
+                       print hashdiff2
diff --git a/src/Options.hs b/src/Options.hs
new file mode 100644
--- /dev/null
+++ b/src/Options.hs
@@ -0,0 +1,101 @@
+-- |
+-- Module      :  HaskDeep.Command.Options
+-- Copyright   :  Mauro Taraborelli 2012
+-- License     :  BSD3
+--
+-- Maintainer  :  maurotaraborelli@gmail.com
+-- Stability   :  experimental
+-- Portability :  unknown
+--
+-- Computes hashes traversing recursively through the directory structure.
+-- Use a list of known hashes to audit a set of files.
+
+module Options
+    (
+     Options(..)
+    ,OptExecution(..)
+    ,OptCompMode(..)
+    ,optionsPI
+    )
+where
+
+import           Prelude hiding (FilePath)
+
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Filesystem.Path.CurrentOS (FilePath)
+import qualified Filesystem.Path.CurrentOS as FSC
+import           Options.Applicative
+
+import           HaskDeep
+
+data Options = Options OptExecution OptCompMode HaskDeepConfiguration
+
+data OptExecution = OptComputation
+                  | OptAudit
+
+data OptCompMode = OptMD5
+                 | OptSHA1
+                 | OptSHA256
+                 | OptSkein512
+
+optionsPI :: ParserInfo Options
+optionsPI = info (optionsP <**> helper)
+            ( fullDesc
+              & progDesc "Computes hashes and audit a set of files"
+              & header "haskdeep - file hashing and audit" )
+
+configurationP :: Parser HaskDeepConfiguration
+configurationP = HaskDeepConfiguration
+                 <$> nullOption
+                         ( long "root"
+                           & short 'r'
+                           & metavar "DIRNAME"
+                           & help "Root directory - default current directory"
+                           & reader fpReader
+                           & value (rootDirectory defaultHaskDeepConfiguration))
+                 <*> nullOption
+                         ( long "known"
+                           & short 'k'
+                           & metavar "FILENAME"
+                           & help "Known hashes file - default known.haskdeep"
+                           & reader fpReader
+                           & value (knownHashes defaultHaskDeepConfiguration))
+                 <*> nullOption
+                         ( long "ignore"
+                           & short 'i'
+                           & metavar "RULE"
+                           & help "Regex to ignore files or directories"
+                           & reader ignReader
+                           & value (ignoreRule defaultHaskDeepConfiguration))
+
+optionsP :: Parser Options
+optionsP = Options
+           <$> subparser
+                   ( command "compute"
+                     (info (pure OptComputation)
+                               (progDesc "Computes file hashes and saves them to known hashes file"))
+                     & command "audit"
+                     (info (pure OptAudit)
+                               (progDesc "Audit files comparing them to known hashes")))
+           <*> nullOption
+                   ( long "computation"
+                     & short 'c'
+                     & metavar "MODE"
+                     & help "md5 | sha1 | sha256 | skein512 - default md5"
+                     & reader compReader
+                     & value OptMD5)
+           <*> configurationP
+
+fpReader :: String -> Maybe FilePath
+fpReader fp = Just $ FSC.decodeString fp
+
+ignReader :: String -> Maybe (Maybe Text)
+ignReader = Just . Just . T.pack
+
+compReader :: String -> Maybe OptCompMode
+compReader "md5"      = Just OptMD5
+compReader "sha1"     = Just OptSHA1
+compReader "sha256"   = Just OptSHA256
+compReader "skein512" = Just OptSkein512
+compReader _          = Nothing
