diff --git a/.editorconfig b/.editorconfig
new file mode 100644
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,23 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 8
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.hs]
+indent_size = 2
+
+[*.md]
+indent_size = 2
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[Makefile]
+indent_size = 4
+indent_style = tab
diff --git a/.gitignore b/.gitignore
new file mode 100644
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*~
+.stack-work/
+.test/
+dist-newstyle/
+macrm.cabal
+macrm.tix
+stack.yaml.lock
diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,64 @@
+# Changelog for macrm
+
+All notable changes to this project will be documented in this file.
+
+## Unreleased changes
+
+#### Added
+
+- add man manual
+
+#### Changed
+
+## 1.0.0.5
+
+#### Changed
+
+- fix for `stack sdist`
+
+## 1.0.0.4
+
+#### Added
+
+- add tests
+
+#### Changed
+
+- fix some bugs
+- add Makefile
+- add .editorconfig
+- add hie.yaml
+- update .travis.yml
+- refactor a bit
+
+## 1.0.0.3
+
+#### Changed
+
+- update LTS to 16.17
+- update README.md
+
+## 1.0.0.2
+
+#### Changed
+
+- update LTS to 15.15
+- improve version string
+- refactor a bit
+
+## 1.0.0.1
+
+#### Changed
+
+- add .travis.yml
+- format source code
+- fix some warning of hlint
+- update LTS to 14.27
+- update .gitignore
+- add hie.yaml for Haskell IDE Engine
+
+## 1.0.0.0
+
+#### Added
+
+- implement first version
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+(c) 2018 Satoshi Ogata
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Satoshi Ogata nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,74 @@
+MAKEFILE := $(lastword $(MAKEFILE_LIST))
+MAKEFILE_DIR := $(dir $(MAKEFILE))
+SRCS := app/Main.hs $(wildcard src/*.hs) $(wildcard test/*.hs)
+
+.PHONY: all
+all: hie.yaml
+	cd $(MAKEFILE_DIR) && stack build --fast --pedantic
+
+.PHONY: build
+build:
+	cd $(MAKEFILE_DIR) && stack build --pedantic
+
+.PHONY: clean
+clean:
+	cd $(MAKEFILE_DIR) && stack clean
+
+.PHONY: rebuild
+rebuild: clean build
+
+.PHONY: install
+install: rebuild installdirs
+	stack install
+
+.PHONY: uninstall
+uninstall:
+	rm -f $${HOME}/.local/bin/macrm
+
+.PHONY: distclean
+distclean:
+	cd $(MAKEFILE_DIR) && rm -rf .stack-work macrm.cabal stack.yaml.lock
+
+.PHONY: check
+check:
+	stack test --coverage
+
+.PHONY: test
+test: check
+
+.PHONY: lint
+lint:
+	hlint src app test
+
+.PHONY: installdirs
+installdirs:
+	mkdir -p $(HOME)/.local/bin
+
+.PHONY: pre-dist
+pre-dist: /usr/local/Homebrew/Library/Taps/satosystems/homebrew-tap/macrm.rb
+	@if [ -f "$^" ]; then \
+		$(eval CURRENT_VERSION := $(shell (cat $^ | grep url | awk -F'/' '{print $$8}'))) \
+		$(eval NEW_VERSION := $(shell git tag | tail -n 1)) \
+		if [ "$(CURRENT_VERSION)" != "$(NEW_VERSION)" ]; then \
+			$(eval URL := $(shell echo "https://github.com/satosystems/macrm/releases/download/$(NEW_VERSION)/macrm")) \
+			if [ "`curl -I $(URL) | head -n 1 | awk -F' ' '{print $$2}'`" != "404" ]; then \
+				$(eval ESCAPED_URL := $(shell echo "$(URL)" | sed -E 's/\//\\\//g')) \
+				curl -L -o "$(TMPDIR)macrm" "$(URL)"; \
+				export __MACRM_HASH=`openssl sha256 "$(TMPDIR)macrm" | awk -F' ' '{print $$2}'`; \
+				sed -E "/^  url /s/\"[^\"]+\"/\"$(ESCAPED_URL)\"/; /^  sha256 /s/\"[^\"]+\"/\"$${__MACRM_HASH}\"/" "$^" > "$(TMPDIR)macrm.rb" && mv "$(TMPDIR)macrm.rb" "$^"; \
+				export -n __MACRM_HASH; \
+				rm $(TMPDIR)macrm; \
+			fi \
+		fi \
+	fi
+
+hie.yaml: $(SRCS)
+	cd $(MAKEFILE_DIR) && type gen-hie 2> /dev/null && gen-hie > $@ || stack install implicit-hie && make -f $(MAKEFILE_DIR)$(MAKEFILE) $@
+
+/usr/local/Homebrew/Library/Taps/satosystems/homebrew-tap/macrm.rb: /usr/local/Homebrew/Library/Taps/satosystems/homebrew-tap
+
+/usr/local/Homebrew/Library/Taps/satosystems/homebrew-tap:
+	brew tap satosystems/tap
+
+help:
+	@cat $(MAKEFILE) | grep -E '^[^ :]+ *:([^=]|$$)' | grep -v '^.PHONY *:' | awk -F':' '{print $$1}'
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+# macrm
+
+[![Build Status](https://travis-ci.org/satosystems/macrm.svg?branch=master)](https://travis-ci.org/satosystems/macrm)
+
+This program is a replacement of macOS's `rm` command.
+Unlike `rm` command, this `macrm` command moves deleted files to trash.
+That is not just a move, of cause it is possible to undo.
+
+## How to install
+
+### Install via self build
+
+```shell-session
+$ git clone https://github.com/satosystems/macrm.git
+...
+$ cd macrm
+...
+$ make install
+...
+$
+```
+
+### Install via Homebrew
+
+```shell-session
+$ brew install satosystems/tap/macrm
+...
+$
+```
+
+### Install via Hackage
+
+```shell-session
+$ stack install macrm
+...
+$
+```
+
+## How to use
+
+It is same as `rm` command.
+Please see `macrm --help` for details.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,4 @@
+import           Distribution.Simple            ( defaultMain )
+
+main :: IO ()
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,6 @@
+module Main where
+
+import           Macrm                          ( run )
+
+main :: IO ()
+main = run
diff --git a/hie.yaml b/hie.yaml
new file mode 100644
--- /dev/null
+++ b/hie.yaml
@@ -0,0 +1,13 @@
+cradle:
+  stack:
+    - path: "./src"
+      component: "macrm:lib"
+
+    - path: "./app/Main.hs"
+      component: "macrm:exe:macrm"
+
+    - path: "./app/Paths_macrm.hs"
+      component: "macrm:exe:macrm"
+
+    - path: "./test"
+      component: "macrm:test:test"
diff --git a/macrm.cabal b/macrm.cabal
new file mode 100644
--- /dev/null
+++ b/macrm.cabal
@@ -0,0 +1,88 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 8c4cb58ceda2b19569334cea7c787535aad04b26e6bc6c1da0585cea2a6beabd
+
+name:           macrm
+version:        1.0.0.5
+synopsis:       Alternative rm command for macOS that remove files/dirs to the system trash
+description:    Please see the README on GitHub at <https://github.com/satosystems/macrm#readme>
+category:       Program
+homepage:       https://github.com/satosystems/macrm#readme
+bug-reports:    https://github.com/satosystems/macrm/issues
+author:         Satoshi Ogata
+maintainer:     satosystems@gmail.com
+copyright:      (c) 2018-2020 Satoshi Ogata
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    .editorconfig
+    .gitignore
+    ChangeLog.md
+    Makefile
+    README.md
+    hie.yaml
+
+source-repository head
+  type: git
+  location: https://github.com/satosystems/macrm
+
+library
+  exposed-modules:
+      Macrm
+  other-modules:
+      Paths_macrm
+  hs-source-dirs:
+      src
+  build-depends:
+      MissingH >=1.4.3 && <1.5
+    , base >=4.7 && <5
+    , cmdargs >=0.10.20 && <0.11
+    , cond >=0.4.1 && <0.5
+    , directory >=1.3.6 && <1.4
+    , exceptions >=0.10.4 && <0.11
+    , githash >=0.1.4 && <0.2
+    , inline-c >=0.9.1 && <0.10
+    , process >=1.6.9 && <1.7
+    , text >=1.2.4 && <1.3
+    , time >=1.9.3 && <1.10
+    , unix >=2.7.2 && <2.8
+  default-language: Haskell2010
+
+executable macrm
+  main-is: Main.hs
+  other-modules:
+      Paths_macrm
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -optP-Wno-nonportable-include-path
+  build-depends:
+      base >=4.7 && <5
+    , macrm
+  default-language: Haskell2010
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_macrm
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
+  build-depends:
+      MissingH >=1.4.3 && <1.5
+    , base >=4.7 && <5
+    , bytestring >=0.10.10 && <0.11
+    , directory >=1.3.6 && <1.4
+    , filepath >=1.4.2 && <1.5
+    , hspec >=2.7.4 && <2.8
+    , macrm
+    , main-tester >=0.2.0 && <0.3
+    , process >=1.6.9 && <1.7
+    , unix >=2.7.2 && <2.8
+    , uuid >=1.3.13 && <1.4
+  default-language: Haskell2010
diff --git a/src/Macrm.hs b/src/Macrm.hs
new file mode 100644
--- /dev/null
+++ b/src/Macrm.hs
@@ -0,0 +1,498 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Macrm where
+
+import           Control.Conditional            ( ifM )
+import           Control.Monad                  ( foldM
+                                                , unless
+                                                , when
+                                                )
+import           Data.Char                      ( toUpper )
+import           Data.Fixed                     ( Fixed
+                                                , HasResolution
+                                                , Uni
+                                                , showFixed
+                                                )
+import           Data.Int                       ( Int32 )
+import           Data.Maybe                     ( fromJust
+                                                , isNothing
+                                                )
+import qualified Data.Text                     as T
+import           Data.Time.LocalTime            ( TimeOfDay(TimeOfDay)
+                                                , ZonedTime
+                                                  ( zonedTimeToLocalTime
+                                                  )
+                                                , getZonedTime
+                                                , localTimeOfDay
+                                                )
+import           Data.Tuple.Utils               ( fst3
+                                                , snd3
+                                                , thd3
+                                                )
+import           Data.Version                   ( showVersion )
+import           Foreign.C.String               ( withCString )
+import           GitHash                        ( GitInfo
+                                                , giDirty
+                                                , giHash
+                                                , tGitInfoCwd
+                                                )
+import qualified Language.C.Inline             as C
+import           Paths_macrm                    ( version )
+import           System.Console.CmdArgs         ( (&=)
+                                                , Data
+                                                , Typeable
+                                                , args
+                                                , cmdArgs
+                                                , explicit
+                                                , help
+                                                , name
+                                                , noAtExpand
+                                                , program
+                                                , summary
+                                                , typ
+                                                )
+import           System.Exit                    ( ExitCode
+                                                  ( ExitFailure
+                                                  , ExitSuccess
+                                                  )
+                                                , exitWith
+                                                )
+import           System.Directory               ( doesDirectoryExist
+                                                , getHomeDirectory
+                                                , listDirectory
+                                                , renameDirectory
+                                                , renameFile
+                                                )
+import           System.IO                      ( hClose
+                                                , hFlush
+                                                , hGetContents
+                                                , hPutStr
+                                                , hPutStrLn
+                                                , stderr
+                                                , stdout
+                                                )
+import           System.Path.NameManip          ( absolute_path
+                                                , guess_dotdot
+                                                )
+import           System.Posix.Files             ( FileStatus
+                                                , fileGroup
+                                                , fileMode
+                                                , fileOwner
+                                                , getSymbolicLinkStatus
+                                                , groupExecuteMode
+                                                , groupReadMode
+                                                , groupWriteMode
+                                                , intersectFileModes
+                                                , isBlockDevice
+                                                , isCharacterDevice
+                                                , isDirectory
+                                                , isNamedPipe
+                                                , isSocket
+                                                , isSymbolicLink
+                                                , ownerExecuteMode
+                                                , ownerReadMode
+                                                , ownerWriteMode
+                                                , otherExecuteMode
+                                                , otherReadMode
+                                                , otherWriteMode
+                                                , setGroupIDMode
+                                                , setUserIDMode
+                                                )
+import           System.Posix.Types             ( FileMode
+                                                , GroupID
+                                                , UserID
+                                                )
+import           System.Posix.User              ( getRealUserID )
+import           System.Process                 ( CreateProcess
+                                                  ( std_err
+                                                  , std_in
+                                                  , std_out
+                                                  )
+                                                , StdStream(CreatePipe)
+                                                , createProcess
+                                                , proc
+                                                , waitForProcess
+                                                )
+
+C.include "<fcntl.h>"
+C.include "<unistd.h>"
+C.include "<sys/stat.h>"
+
+data FileExists = NotExists | DeadLink | Exists deriving (Eq, Show)
+
+type FileInfo = (FilePath, FileExists, Maybe FileStatus)
+
+data Options = Options
+  { directory :: Bool
+  , force :: Bool
+  , interactive :: Bool
+  , plaster :: Bool
+  , recursive :: Bool
+  , recursive' :: Bool
+  , verbose :: Bool
+  , whiteouts :: Bool
+  , files :: [FilePath]
+  } deriving (Data, Show, Typeable)
+
+getOptions :: Options
+getOptions =
+  Options
+      { directory   = False
+        &= help "Attempt to remove directories as well as other types of files."
+      , force       = False &= help
+        (  "Attempt to remove the files without prompting for confirmation, "
+        ++ "regardless of the file's permissions. If the file does not exist, "
+        ++ "do not display a diagnostic message or modify the exit status to reflect an error. "
+        ++ "The -f option overrides any previous -i options."
+        )
+      , interactive = False &= help
+        (  "Request confirmation before attempting to remove each file, "
+        ++ "regardless of the file's permissions, "
+        ++ "or whether or not the standard input device is a terminal. "
+        ++ "The -i option overrides any previous -f options."
+        )
+      , plaster     = False &= name "P" &= help
+        (  "Overwrite regular files before deleting them. "
+        ++ "Files are overwritten three times, first with the byte pattern 0xff, "
+        ++ "then 0x00, and then 0xff again, before they are deleted. "
+        ++ "This flag is ignored under macrm."
+        )
+      , recursive   = False &= name "R" &= help
+        ("Attempt to remove the file hierarchy rooted in each file argument. "
+        ++ "The -R option implies the -d option. If the -i option is specified, "
+        ++ "the user is prompted for confirmation before each directory's "
+        ++ "contents are processed (as well as before the attempt is made to remove the directory). "
+        ++ "If the user does not respond affirmatively, "
+        ++ "the file hierarchy rooted in that directory is skipped."
+        )
+      , recursive'  = False &= name "r" &= explicit &= help "Equivalent to -R."
+      , verbose     = False &= help
+        "Be verbose when deleting files, showing them as they are removed."
+      , whiteouts   = False &= name "W" &= help
+        ("Attempt to undelete the named files. "
+        ++ "Currently, this option can only be used to recover files covered by whiteouts. "
+        ++ "This flag is ignored under macrm."
+        )
+      , files       = [] &= args &= typ "FILES/DIRS"
+      }
+    &= summary versionString
+    &= program "macrm"
+    &= noAtExpand
+
+absolutize :: FilePath -> IO FilePath
+absolutize path = fromJust . guess_dotdot <$> absolute_path path
+
+moveToTrash :: [FileInfo] -> IO ()
+moveToTrash []               = return ()
+moveToTrash (fileInfo : fis) = do
+  pair <- makePair fileInfo
+  move pair
+  moveToTrash fis
+ where
+  makePair :: FileInfo -> IO (FilePath, FilePath)
+  makePair (path, _, _) = do
+    let fileOrDirName = T.unpack . last . T.splitOn "/" . T.pack $ path
+    removedPath <- getRemovedPath fileOrDirName
+    return (path, removedPath)
+  move :: (FilePath, FilePath) -> IO ()
+  move (from, to) = ifM (doesDirectoryExist from)
+                        (renameDirectory from to)
+                        (renameFile from to)
+
+getRemovedPath :: FilePath -> IO FilePath
+getRemovedPath fileOrDirName = do
+  dayOfTime <- getCurrentDayOfTime
+  searchRemovedPath fileOrDirName $ ' ' : dayOfTime
+
+searchRemovedPath :: FilePath -> String -> IO FilePath
+searchRemovedPath fileOrDirName suffix = do
+  homePath <- getHomeDirectory
+  let trashPath = homePath ++ "/.Trash/"
+  removed <- listDirectory trashPath
+  if fileOrDirName `elem` removed
+    then searchRemovedPath (fileOrDirName ++ suffix) suffix
+    else return $ trashPath ++ fileOrDirName
+
+getCurrentDayOfTime :: IO String
+getCurrentDayOfTime = do
+  zonedTime <- getZonedTime
+  let (TimeOfDay hour minute second) =
+        (localTimeOfDay . zonedTimeToLocalTime) zonedTime
+      sHour    = if hour >= 10 then show hour else '0' : show hour
+      sMinute  = if minute >= 10 then show minute else '0' : show minute
+      uSecond  = changeResolution second :: Uni
+      sSecond' = showFixed True uSecond
+      sSecond  = if uSecond >= 10 then sSecond' else '0' : sSecond'
+      suffix   = sHour ++ "." ++ sMinute ++ "." ++ sSecond
+  return suffix
+
+changeResolution :: (HasResolution a, HasResolution b) => Fixed a -> Fixed b
+changeResolution = fromRational . toRational
+
+rm :: Options -> ExitCode -> UserID -> [FileInfo] -> [FilePath] -> IO ExitCode
+rm (Options False False False False False False False False []) ExitSuccess _ [] []
+  = do
+    hPutStrLn stderr
+              "usage: macrm [-f | -i] [-dPRrvW] file ...\n       unlink file"
+    return $ ExitFailure 1
+rm _ exitCode _ []         [] = return exitCode
+rm _ exitCode _ removables [] = do
+  ec <- remove removables
+  case ec of
+    ExitSuccess -> return exitCode
+    _           -> return $ ExitFailure 1
+rm options exitCode uid removables (path : paths) = do
+  fileInfo <- getFileInfo path
+  if snd3 fileInfo == NotExists
+    then if force options
+      then rm options exitCode uid removables paths
+      else do
+        hPutStrLn stderr $ "macrm: " ++ path ++ ": No such file or directory"
+        rm options (ExitFailure 1) uid removables paths
+    else do
+      let status        = fromJust . thd3 $ fileInfo
+      let isDir         = snd3 fileInfo == Exists && isDirectory status
+      let withRecursive = recursive options || recursive' options
+      let withDirectory = directory options
+      isNotEmpty <- if isDir
+        then not . null <$> listDirectory path
+        else return False
+      if isDir
+           && not withRecursive
+           && (not withDirectory || withDirectory && isNotEmpty)
+        then if withDirectory && isNotEmpty
+          then do
+            hPutStrLn stderr $ "macrm: " ++ path ++ ": Directory not empty"
+            rm options (ExitFailure 1) uid removables paths
+          else do
+            hPutStrLn stderr $ "macrm: " ++ path ++ ": is a directory"
+            rm options (ExitFailure 1) uid removables paths
+        else if interactive options
+          then do
+            let message = case (isDir, withRecursive) of
+                  (True, True) -> "examine files in directory "
+                  _            -> "remove "
+            agreement <- getAgreement message path
+            if agreement
+              then do
+                when (verbose options) $ putStrLn path
+                rm options exitCode uid (fileInfo : removables) paths
+              else rm options exitCode uid removables paths
+          else do
+            let fileUid = fileOwner status
+                fileGid = fileGroup status
+            mMessage <- if uid == fileUid
+              then return Nothing
+              else Just <$> makeMessage fileInfo fileUid fileGid
+            needRemove <- if isNothing mMessage
+              then return True
+              else getAgreement (fromJust mMessage) path
+            if needRemove
+              then do
+                when (verbose options) $ putStrLn path
+                rm options exitCode uid (fileInfo : removables) paths
+              else rm options exitCode uid removables paths
+
+remove :: [FileInfo] -> IO ExitCode
+remove fileInfos = do
+  let (paths, isDeadLinks, mStatuses) = foldl
+        (\(paths', isDeadLinks', mStatuses') (path, isDeadLink, mStatus) ->
+          (path : paths', isDeadLink : isDeadLinks', mStatus : mStatuses')
+        )
+        ([], [], [])
+        fileInfos
+  absolutePaths       <- mapM absolutize paths
+  (normals, specials) <- foldM filterSpecialFiles ([], [])
+    $ zip3 absolutePaths isDeadLinks mStatuses
+  unless (null specials) $ moveToTrash . reverse $ specials
+  if null normals
+    then return ExitSuccess
+    else executeScript . createScript . map fst3 . reverse $ normals
+
+executeScript :: String -> IO ExitCode
+executeScript script = do
+  (Just stdIn, _, _, ph) <- createProcess (proc "osascript" [])
+    { std_in  = CreatePipe
+    , std_out = CreatePipe
+    , std_err = CreatePipe
+    }
+  hPutStr stdIn script
+  hFlush stdIn
+  hClose stdIn
+  waitForProcess ph
+
+createScript :: [FilePath] -> String
+createScript paths = concat
+  [ "set l to {}\n"
+  , concatMap
+    (\path -> "set end of l to posix file \"" ++ path ++ "\" as alias\n")
+    paths
+  , "tell application \"Finder\"\n"
+  , "delete l\n"
+  , "end tell\n"
+  , "return"
+  ]
+
+getAgreement :: String -> FilePath -> IO Bool
+getAgreement message path = do
+  putStr $ message ++ path ++ "? "
+  hFlush stdout
+  input <- getLine
+  return $ not (null input) && toUpper (head input) == 'Y'
+
+filterSpecialFiles
+  :: ([FileInfo], [FileInfo]) -> FileInfo -> IO ([FileInfo], [FileInfo])
+filterSpecialFiles (normals, specials) fileInfo = if isSpecialFile fileInfo
+  then return (normals, fileInfo : specials)
+  else return (fileInfo : normals, specials)
+
+getFileFlags :: FilePath -> IO (Maybe String)
+getFileFlags path = do
+  (_, Just stdOut, _, ph) <- createProcess (proc "/bin/ls" ["-lO", path])
+    { std_in  = CreatePipe
+    , std_out = CreatePipe
+    , std_err = CreatePipe
+    }
+  _      <- waitForProcess ph
+  output <- hGetContents stdOut
+  if null output
+    then return Nothing
+    else
+      let flags = T.unpack $ (T.splitOn " " . T.pack $ output) !! 7
+      in  return $ Just flags
+
+makeMessage :: FileInfo -> UserID -> GroupID -> IO String
+makeMessage (path, _, Just status) uid gid = do
+  userAndGroup <- makeUserAndGroupString uid gid
+  mFlags       <- getFileFlags path
+  return
+    $  "override "
+    ++ makePermissionString status
+    ++ "  "
+    ++ userAndGroup
+    ++ maybe "" (" " ++) mFlags
+    ++ " for "
+makeMessage _ _ _ = undefined -- never happen
+
+makePermissionString :: FileStatus -> String
+makePermissionString status =
+  [ ifm permission ownerReadMode    'r'
+  , ifm permission ownerWriteMode   'w'
+  , ifm permission ownerExecuteMode 'x'
+  , ifm permission groupReadMode    'r'
+  , ifm permission groupWriteMode   'w'
+  , ifm permission groupExecuteMode 'x'
+  , ifm permission otherReadMode    'r'
+  , ifm permission otherWriteMode   'w'
+  , ifm permission otherExecuteMode 'x'
+  ]
+ where
+  permission :: FileMode
+  permission = fileMode status
+  ifm :: FileMode -> FileMode -> Char -> Char
+  ifm p m a =
+    let
+      stickyBit = 0o1000
+      isU =
+        m
+          == ownerExecuteMode
+          && intersectFileModes p setUserIDMode
+          == setUserIDMode
+      isG =
+        m
+          == groupExecuteMode
+          && intersectFileModes p setGroupIDMode
+          == setGroupIDMode
+      isO =
+        m == otherExecuteMode && intersectFileModes p stickyBit == stickyBit
+    in
+      if intersectFileModes p m == m
+        then if isU || isG then 's' else if isO then 't' else a
+        else if isU || isG then 'S' else if isO then 'T' else '-'
+
+makeUserAndGroupString :: UserID -> GroupID -> IO String
+makeUserAndGroupString uid gid = do
+  passwdContents <- readFile "/etc/passwd"
+  let user =
+        searchIdName (show (fromIntegral uid :: Int32)) $ lines passwdContents
+  groupContents <- readFile "/etc/group"
+  let group =
+        searchIdName (show (fromIntegral gid :: Int32)) $ lines groupContents
+  return $ user ++ "/" ++ group
+ where
+  searchIdName :: String -> [String] -> String
+  searchIdName uidOrGid []               = uidOrGid
+  searchIdName uidOrGid (('#' : _) : ss) = searchIdName uidOrGid ss
+  searchIdName uidOrGid (s         : ss) = if id' == uidOrGid
+    then name'
+    else searchIdName uidOrGid ss
+   where
+    splitted :: [T.Text]
+    splitted = T.splitOn ":" . T.pack $ s
+    id' :: String
+    id' = T.unpack $ splitted !! 2
+    name' :: String
+    name' = T.unpack . head $ splitted
+
+getFileInfo :: FilePath -> IO FileInfo
+getFileInfo path = do
+  fileExists <- isPathExists path
+  if fileExists == NotExists
+    then return (path, fileExists, Nothing)
+    else do
+      status <- getSymbolicLinkStatus path
+      return (path, fileExists, Just status)
+
+isSpecialFile :: FileInfo -> Bool
+isSpecialFile (_, NotExists, _      ) = False
+isSpecialFile (_, DeadLink , _      ) = True
+isSpecialFile (_, Exists   , Nothing) = undefined -- never happen
+isSpecialFile (_, Exists, Just status) =
+  isSymbolicLink status
+    || isNamedPipe status
+    || isSocket status
+    || isCharacterDevice status
+    || isBlockDevice status
+
+isPathExists :: FilePath -> IO FileExists
+isPathExists path = do
+  rc <- withCString path $ \cpath -> [C.block| int {
+      struct stat lstat_info;
+      int fd;
+      if (lstat($(char *cpath), &lstat_info) == -1) {
+          return 0;  // not exists
+      }
+      fd = open($(char *cpath), O_RDONLY);
+      if (fd == -1) {
+          return 1;  // dead link
+      }
+      close(fd);
+      return 2;  // exists
+    } |]
+  return $ case rc of
+    0 -> NotExists
+    1 -> DeadLink
+    2 -> Exists
+    _ -> undefined -- never happen
+
+gitInfo :: GitInfo
+gitInfo = $$(tGitInfoCwd)
+
+versionString :: String
+versionString = concat
+  [ "macrm ver "
+  , showVersion version
+  , " based on Git commit "
+  , giHash gitInfo
+  , if giDirty gitInfo then " Dirty" else " Clean"
+  ]
+
+run :: IO ()
+run = do
+  options <- cmdArgs getOptions
+  uid     <- getRealUserID
+  ec      <- rm options ExitSuccess uid [] . files $ options
+  exitWith ec
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,741 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import qualified Data.ByteString.Char8         as BS8
+import           Data.List.Utils                ( startswith )
+import           Data.Maybe                     ( isNothing )
+import           Data.UUID                      ( toString )
+import           Data.UUID.V4                   ( nextRandom )
+import           Data.Version                   ( showVersion )
+import           Test.Hspec                     ( hspec
+                                                , describe
+                                                , it
+                                                , shouldBe
+                                                , shouldContain
+                                                , shouldReturn
+                                                , shouldSatisfy
+                                                , Spec
+                                                )
+import           Test.Main                      ( withArgs
+                                                , ExitCode
+                                                  ( ExitSuccess
+                                                  , ExitFailure
+                                                  )
+                                                , captureProcessResult
+                                                , withStdin
+                                                , ProcessResult
+                                                  ( prStdout
+                                                  , prStderr
+                                                  , prExitCode
+                                                  , prException
+                                                  )
+                                                )
+import           System.Directory               ( createDirectory
+                                                , createDirectoryIfMissing
+                                                , doesPathExist
+                                                , listDirectory
+                                                , removeDirectoryRecursive
+                                                , removeFile
+                                                , renameFile
+                                                )
+import           System.Environment             ( getEnv )
+import           System.FilePath                ( addTrailingPathSeparator )
+import           System.IO                      ( hGetContents )
+import           System.Posix.Files             ( createSymbolicLink )
+import           System.Process                 ( createProcess
+                                                , proc
+                                                , CreateProcess(std_out)
+                                                , StdStream(CreatePipe)
+                                                )
+
+import           Macrm                          ( absolutize
+                                                , run
+                                                )
+import           Paths_macrm                    ( version )
+
+data Path = Path
+  { fileOrDirName :: String
+  , relativePath :: FilePath
+  , absolutePath :: FilePath
+  }
+
+data TestFiles = TestFiles
+  { parentDir :: Path
+  , normalFiles :: [Path]
+  , notExistPaths :: [Path]
+  , emptyDirs :: [Path]
+  , notEmptyDirs :: [Path]
+  , symbolicLinkFiles :: [Path]
+  , symbolicLinkDirs :: [Path]
+  , deadSymbolicLinks :: [Path]
+  }
+
+createTestFile :: String -> IO Path
+createTestFile baseDir = do
+  uuid <- nextRandom
+  let name = toString uuid
+  let path = addTrailingPathSeparator baseDir ++ name
+  writeFile path ""
+  Path name path <$> absolutize path
+
+createNotExistTestFile :: String -> IO Path
+createNotExistTestFile baseDir = do
+  uuid <- nextRandom
+  let name = toString uuid
+  let path = addTrailingPathSeparator baseDir ++ name
+  Path name path <$> absolutize path
+
+createTestDir :: String -> IO Path
+createTestDir baseDir = do
+  uuid <- nextRandom
+  let name = toString uuid
+  let path = addTrailingPathSeparator baseDir ++ name
+  createDirectoryIfMissing True path
+  Path name path <$> absolutize path
+
+createTestSymbolicLink :: FilePath -> String -> IO Path
+createTestSymbolicLink baseDir source = do
+  uuid <- nextRandom
+  let name = toString uuid
+  let path = addTrailingPathSeparator baseDir ++ name
+  createSymbolicLink source path
+  Path name path <$> absolutize path
+
+createTestFiles :: IO TestFiles
+createTestFiles = do
+  baseDir <- createTestDir ".test"
+  let baseDirPath = relativePath baseDir
+  let baseDirs    = replicate 3 baseDirPath
+  normalFiles'   <- mapM createTestFile baseDirs
+  notExistPaths' <- mapM createNotExistTestFile baseDirs
+  emptyDirs'     <- mapM createTestDir baseDirs
+  notEmptyDirs'  <- mapM createTestDir baseDirs
+  mapM_ (createTestFile . relativePath) notEmptyDirs'
+  let createTestSymbolicLink' =
+        mapM (\(Path fn _ _) -> createTestSymbolicLink baseDirPath fn)
+  symbolicLinkFiles' <- createTestSymbolicLink' normalFiles'
+  symbolicLinkDirs'  <- createTestSymbolicLink' emptyDirs'
+  normalFiles''      <- mapM createTestFile baseDirs
+  deadSymbolicLinks' <- createTestSymbolicLink' normalFiles''
+  mapM_ (\(Path _ _ ap) -> removeFile ap) normalFiles''
+  return $ TestFiles { parentDir         = baseDir
+                     , normalFiles       = normalFiles'
+                     , notExistPaths     = notExistPaths'
+                     , emptyDirs         = emptyDirs'
+                     , notEmptyDirs      = notEmptyDirs'
+                     , symbolicLinkFiles = symbolicLinkFiles'
+                     , symbolicLinkDirs  = symbolicLinkDirs'
+                     , deadSymbolicLinks = deadSymbolicLinks'
+                     }
+
+removeTestFiles :: TestFiles -> IO ()
+removeTestFiles = removeDirectoryRecursive . relativePath . parentDir
+
+spec_run :: FilePath -> Spec
+spec_run trashPath = describe "run" $ do
+  it "shows usage with no options" $ do
+    pr <- withArgs [] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` BS8.unlines
+      ["usage: macrm [-f | -i] [-dPRrvW] file ...", "       unlink file"]
+    prExitCode pr `shouldBe` ExitFailure 1
+    (isNothing . prException) pr `shouldBe` True
+  it "shows version with `--version' option" $ do
+    (_, Just hout, _, _) <- createProcess (proc "git" ["rev-parse", "HEAD"])
+      { std_out = CreatePipe
+      }
+    output <- hGetContents hout
+    pr     <- withArgs ["--version"] $ captureProcessResult run
+    prStdout pr `shouldBe` BS8.pack
+      (  "macrm ver "
+      ++ showVersion version
+      ++ " based on Git commit "
+      ++ (head . lines) output
+      ++ " Clean\n"
+      )
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    pr' <- withArgs ["-V"] $ captureProcessResult run
+    pr `shouldBe` pr'
+  it "shows numeric version with `--numeric-version' option" $ do
+    pr <- withArgs ["--numeric-version"] $ captureProcessResult run
+    prStdout pr `shouldBe` BS8.pack (showVersion version ++ "\n")
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+  it "shows help with `--help' option" $ do
+    let
+      help = BS8.unlines
+        [ "macrm [OPTIONS] [FILES/DIRS]"
+        , ""
+        , "Common flags:"
+        , "  -d --directory        Attempt to remove directories as well as other types"
+        , "                        of files."
+        , "  -f --force            Attempt to remove the files without prompting for"
+        , "                        confirmation, regardless of the file's permissions. If"
+        , "                        the file does not exist, do not display a diagnostic"
+        , "                        message or modify the exit status to reflect an error."
+        , "                        The -f option overrides any previous -i options."
+        , "  -i --interactive      Request confirmation before attempting to remove each"
+        , "                        file, regardless of the file's permissions, or whether"
+        , "                        or not the standard input device is a terminal. The -i"
+        , "                        option overrides any previous -f options."
+        , "  -P --plaster          Overwrite regular files before deleting them. Files"
+        , "                        are overwritten three times, first with the byte"
+        , "                        pattern 0xff, then 0x00, and then 0xff again, before"
+        , "                        they are deleted. This flag is ignored under macrm."
+        , "  -R --recursive        Attempt to remove the file hierarchy rooted in each"
+        , "                        file argument. The -R option implies the -d option. If"
+        , "                        the -i option is specified, the user is prompted for"
+        , "                        confirmation before each directory's contents are"
+        , "                        processed (as well as before the attempt is made to"
+        , "                        remove the directory). If the user does not respond"
+        , "                        affirmatively, the file hierarchy rooted in that"
+        , "                        directory is skipped."
+        , "  -r                    Equivalent to -R."
+        , "  -v --verbose          Be verbose when deleting files, showing them as they"
+        , "                        are removed."
+        , "  -W --whiteouts        Attempt to undelete the named files. Currently, this"
+        , "                        option can only be used to recover files covered by"
+        , "                        whiteouts. This flag is ignored under macrm."
+        , "  -? --help             Display help message"
+        , "  -V --version          Print version information"
+        , "     --numeric-version  Print just the version number"
+        ]
+    pr <- withArgs ["--help"] $ captureProcessResult run
+    prStdout pr `shouldSatisfy` (help `BS8.isSuffixOf`)
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    pr' <- withArgs ["-?"] $ captureProcessResult run
+    pr `shouldBe` pr'
+  it "will get an error if it tries to remove a file that does not exist" $ do
+    testFiles <- createTestFiles
+    let filePath = (relativePath . head . notExistPaths) testFiles
+    pr <- withArgs [filePath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` BS8.pack
+      ("macrm: " ++ filePath ++ ": No such file or directory\n")
+    prExitCode pr `shouldBe` ExitFailure 1
+    (isNothing . prException) pr `shouldBe` True
+    removeTestFiles testFiles
+  it "removes a normal file" $ do
+    testFiles <- createTestFiles
+    let path     = head . normalFiles $ testFiles
+    let fileName = fileOrDirName path
+    let filePath = relativePath path
+    pr <- withArgs [filePath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePath = addTrailingPathSeparator trashPath ++ fileName
+    doesPathExist removedFilePath `shouldReturn` True
+    doesPathExist filePath `shouldReturn` False
+    removeFile removedFilePath
+    removeTestFiles testFiles
+  it "removes normal files" $ do
+    testFiles <- createTestFiles
+    let paths     = normalFiles testFiles
+    let fileNames = map fileOrDirName paths
+    let filePaths = map relativePath paths
+    pr <- withArgs filePaths $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePaths =
+          map (addTrailingPathSeparator trashPath ++) fileNames
+    mapM_ (\path -> doesPathExist path `shouldReturn` True) removedFilePaths
+    mapM_ (\path -> doesPathExist path `shouldReturn` False) filePaths
+    mapM_ removeFile removedFilePaths
+    removeTestFiles testFiles
+  it "removes same name files" $ do
+    testFiles <- createTestFiles
+    let basePath     = head . normalFiles $ testFiles
+    let filePath0    = relativePath basePath
+    let baseDir      = relativePath . head . notEmptyDirs $ testFiles
+    let sameFileName = fileOrDirName basePath
+    let filePath1    = addTrailingPathSeparator baseDir ++ sameFileName
+    renameFile (relativePath . last . normalFiles $ testFiles) filePath1
+    pr <- withArgs [filePath0, filePath1] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePath0 = addTrailingPathSeparator trashPath ++ sameFileName
+    removed <- listDirectory trashPath
+    let filtered = filter (startswith $ sameFileName ++ " ") removed
+    length filtered `shouldBe` 1
+    doesPathExist removedFilePath0 `shouldReturn` True
+    doesPathExist filePath0 `shouldReturn` False
+    doesPathExist filePath1 `shouldReturn` False
+    removeFile removedFilePath0
+    removeFile . (addTrailingPathSeparator trashPath ++) . head $ filtered
+    removeTestFiles testFiles
+  it "removes same name special files" $ do
+    testFiles <- createTestFiles
+    let path      = head . symbolicLinkFiles $ testFiles
+    let fileName  = fileOrDirName path
+    let filePath0 = relativePath path
+    let baseDir   = relativePath . head . notEmptyDirs $ testFiles
+    let filePath1 = addTrailingPathSeparator baseDir ++ fileName
+    renameFile (relativePath . last . symbolicLinkFiles $ testFiles) filePath1
+    pr <- withArgs [filePath0, filePath1] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePath0 = addTrailingPathSeparator trashPath ++ fileName
+    removed <- listDirectory trashPath
+    let filtered = filter (startswith $ fileName ++ " ") removed
+    length filtered `shouldBe` 1
+    (fileName `elem` removed) `shouldBe` True
+    doesPathExist filePath0 `shouldReturn` False
+    doesPathExist filePath1 `shouldReturn` False
+    removeFile removedFilePath0
+    removeFile . (addTrailingPathSeparator trashPath ++) . head $ filtered
+    removeTestFiles testFiles
+  it "removes same name directries" $ do
+    testFiles <- createTestFiles
+    let path     = head . emptyDirs $ testFiles
+    let dirName  = fileOrDirName path
+    let dirPath0 = relativePath path
+    let baseDir  = relativePath . head . notEmptyDirs $ testFiles
+    let dirPath1 = addTrailingPathSeparator baseDir ++ dirName
+    createDirectory dirPath1
+    pr <- withArgs ["-d", dirPath0, dirPath1] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath0 = addTrailingPathSeparator trashPath ++ dirName
+    removed <- listDirectory trashPath
+    let filtered = filter (startswith $ dirName ++ " ") removed
+    length filtered `shouldBe` 1
+    doesPathExist removedDirPath0 `shouldReturn` True
+    doesPathExist dirPath0 `shouldReturn` False
+    doesPathExist dirPath1 `shouldReturn` False
+    removeDirectoryRecursive removedDirPath0
+    removeDirectoryRecursive
+      . (addTrailingPathSeparator trashPath ++)
+      . head
+      $ filtered
+    removeTestFiles testFiles
+  it "fails to remove directory" $ do
+    testFiles <- createTestFiles
+    let path    = head . emptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withArgs [dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr
+      `shouldBe` BS8.pack ("macrm: " ++ dirPath ++ ": is a directory\n")
+    prExitCode pr `shouldBe` ExitFailure 1
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` False
+    doesPathExist dirPath `shouldReturn` True
+    removeTestFiles testFiles
+  it "removes empty directory with `--directory' option" $ do
+    testFiles <- createTestFiles
+    let path    = head . emptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withArgs ["--directory", dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "removes empty directory with `-d' option" $ do
+    testFiles <- createTestFiles
+    let path    = head . emptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withArgs ["-d", dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "fails to remove non empty directory with `--directory' option" $ do
+    testFiles <- createTestFiles
+    let path    = head . notEmptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withArgs ["--directory", dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr
+      `shouldBe` BS8.pack ("macrm: " ++ dirPath ++ ": Directory not empty\n")
+    prExitCode pr `shouldBe` ExitFailure 1
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` False
+    doesPathExist dirPath `shouldReturn` True
+    removeTestFiles testFiles
+  it "fails to remove non empty directory with `-d' option" $ do
+    testFiles <- createTestFiles
+    let path    = head . notEmptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withArgs ["-d", dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr
+      `shouldBe` BS8.pack ("macrm: " ++ dirPath ++ ": Directory not empty\n")
+    prExitCode pr `shouldBe` ExitFailure 1
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` False
+    doesPathExist dirPath `shouldReturn` True
+    removeTestFiles testFiles
+  it "does nothing when specified non-existance file with `--force' option" $ do
+    testFiles <- createTestFiles
+    let path     = head . notExistPaths $ testFiles
+    let filePath = relativePath path
+    pr <- withArgs ["--force", filePath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    removeTestFiles testFiles
+  it "does nothing when specified non-existance file with `-f' option" $ do
+    testFiles <- createTestFiles
+    let path     = head . notExistPaths $ testFiles
+    let filePath = relativePath path
+    pr <- withArgs ["-f", filePath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    removeTestFiles testFiles
+  it "removes or not with `--interactive' option" $ do
+    testFiles <- createTestFiles
+    let paths     = normalFiles testFiles
+    let fileNames = map fileOrDirName paths
+    let filePaths = map relativePath paths
+    pr <-
+      withStdin "Y\nn\nyes\n"
+      . withArgs ("--interactive" : filePaths)
+      $ captureProcessResult run
+    let prompt =
+          foldl (\acc cur -> acc ++ "remove " ++ cur ++ "? ") "" filePaths
+    prStdout pr `shouldBe` BS8.pack prompt
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePaths =
+          map (addTrailingPathSeparator trashPath ++) fileNames
+    doesPathExist (head removedFilePaths) `shouldReturn` True
+    doesPathExist (removedFilePaths !! 1) `shouldReturn` False
+    doesPathExist (last removedFilePaths) `shouldReturn` True
+    doesPathExist (head filePaths) `shouldReturn` False
+    doesPathExist (filePaths !! 1) `shouldReturn` True
+    doesPathExist (last filePaths) `shouldReturn` False
+    removeFile $ head removedFilePaths
+    removeFile $ last removedFilePaths
+    removeTestFiles testFiles
+  it "removes or not with `-i' option" $ do
+    testFiles <- createTestFiles
+    let paths     = normalFiles testFiles
+    let fileNames = map fileOrDirName paths
+    let filePaths = map relativePath paths
+    pr <-
+      withStdin "Y\nn\nyes\n"
+      . withArgs ("-i" : filePaths)
+      $ captureProcessResult run
+    let prompt =
+          foldl (\acc cur -> acc ++ "remove " ++ cur ++ "? ") "" filePaths
+    prStdout pr `shouldBe` BS8.pack prompt
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePaths =
+          map (addTrailingPathSeparator trashPath ++) fileNames
+    doesPathExist (head removedFilePaths) `shouldReturn` True
+    doesPathExist (removedFilePaths !! 1) `shouldReturn` False
+    doesPathExist (last removedFilePaths) `shouldReturn` True
+    doesPathExist (head filePaths) `shouldReturn` False
+    doesPathExist (filePaths !! 1) `shouldReturn` True
+    doesPathExist (last filePaths) `shouldReturn` False
+    removeFile $ head removedFilePaths
+    removeFile $ last removedFilePaths
+    removeTestFiles testFiles
+  it "ignores `--plaster' option" $ do
+    testFiles <- createTestFiles
+    let path     = head . normalFiles $ testFiles
+    let fileName = fileOrDirName path
+    let filePath = relativePath path
+    pr <- withArgs ["--plaster", filePath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePath = addTrailingPathSeparator trashPath ++ fileName
+    doesPathExist removedFilePath `shouldReturn` True
+    doesPathExist filePath `shouldReturn` False
+    removeFile removedFilePath
+    removeTestFiles testFiles
+  it "ignores `-P' option" $ do
+    testFiles <- createTestFiles
+    let path     = head . normalFiles $ testFiles
+    let fileName = fileOrDirName path
+    let filePath = relativePath path
+    pr <- withArgs ["-P", filePath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePath = addTrailingPathSeparator trashPath ++ fileName
+    doesPathExist removedFilePath `shouldReturn` True
+    doesPathExist filePath `shouldReturn` False
+    removeFile removedFilePath
+    removeTestFiles testFiles
+  it "removes non empty directory with `--recursive' option" $ do
+    testFiles <- createTestFiles
+    let path    = head . notEmptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withArgs ["--recursive", dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "removes non empty directory with `-R' option" $ do
+    testFiles <- createTestFiles
+    let path    = head . notEmptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withArgs ["-R", dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "removes non empty directory with `-r' option" $ do
+    testFiles <- createTestFiles
+    let path    = head . notEmptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withArgs ["-r", dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "removes specified files and shows verbose with `--verbose' option" $ do
+    testFiles <- createTestFiles
+    let paths     = normalFiles testFiles
+    let fileNames = map fileOrDirName paths
+    let filePaths = map relativePath paths
+    pr <- withArgs ("--verbose" : filePaths) $ captureProcessResult run
+    prStdout pr `shouldBe` (BS8.pack . unlines) filePaths
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePaths =
+          map (addTrailingPathSeparator trashPath ++) fileNames
+    mapM_ (\path -> doesPathExist path `shouldReturn` True) removedFilePaths
+    mapM_ (\path -> doesPathExist path `shouldReturn` False) filePaths
+    mapM_ removeFile removedFilePaths
+    removeTestFiles testFiles
+  it "removes specified files and shows verbose with `-v' option" $ do
+    testFiles <- createTestFiles
+    let paths     = normalFiles testFiles
+    let fileNames = map fileOrDirName paths
+    let filePaths = map relativePath paths
+    pr <- withArgs ("-v" : filePaths) $ captureProcessResult run
+    prStdout pr `shouldBe` (BS8.pack . unlines) filePaths
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePaths =
+          map (addTrailingPathSeparator trashPath ++) fileNames
+    mapM_ (\path -> doesPathExist path `shouldReturn` True) removedFilePaths
+    mapM_ (\path -> doesPathExist path `shouldReturn` False) filePaths
+    mapM_ removeFile removedFilePaths
+    removeTestFiles testFiles
+  it "ignores `--whiteouts' option" $ do
+    testFiles <- createTestFiles
+    let path     = head . normalFiles $ testFiles
+    let fileName = fileOrDirName path
+    let filePath = relativePath path
+    pr <- withArgs ["--whiteouts", filePath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePath = addTrailingPathSeparator trashPath ++ fileName
+    doesPathExist removedFilePath `shouldReturn` True
+    doesPathExist filePath `shouldReturn` False
+    removeFile removedFilePath
+    removeTestFiles testFiles
+  it "ignores `-W' option" $ do
+    testFiles <- createTestFiles
+    let path     = head . normalFiles $ testFiles
+    let fileName = fileOrDirName path
+    let filePath = relativePath path
+    pr <- withArgs ["-W", filePath] $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedFilePath = addTrailingPathSeparator trashPath ++ fileName
+    doesPathExist removedFilePath `shouldReturn` True
+    doesPathExist filePath `shouldReturn` False
+    removeFile removedFilePath
+    removeTestFiles testFiles
+  it "removes symbolic files" $ do
+    testFiles <- createTestFiles
+    let paths     = symbolicLinkFiles testFiles
+    let fileNames = map fileOrDirName paths
+    let filePaths = map relativePath paths
+    pr <- withArgs filePaths $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    removed <- listDirectory trashPath
+    mapM_ (\fileName -> removed `shouldContain` [fileName])  fileNames -- cannot use doesPathExist for simbolic link
+    mapM_ (\path -> doesPathExist path `shouldReturn` False) filePaths
+    let removedFilePaths =
+          map (addTrailingPathSeparator trashPath ++) fileNames
+    mapM_ removeFile removedFilePaths
+    removeTestFiles testFiles
+  it "removes dead link files" $ do
+    testFiles <- createTestFiles
+    let paths     = deadSymbolicLinks testFiles
+    let fileNames = map fileOrDirName paths
+    let filePaths = map relativePath paths
+    pr <- withArgs filePaths $ captureProcessResult run
+    prStdout pr `shouldBe` ""
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    removed <- listDirectory trashPath
+    mapM_ (\fileName -> removed `shouldContain` [fileName])  fileNames -- cannot use doesPathExist for simbolic link
+    mapM_ (\path -> doesPathExist path `shouldReturn` False) filePaths
+    let removedFilePaths =
+          map (addTrailingPathSeparator trashPath ++) fileNames
+    mapM_ removeFile removedFilePaths
+    removeTestFiles testFiles
+  it "removes an empty directory with `--interactive --directory' options" $ do
+    testFiles <- createTestFiles
+    let path    = head . emptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <-
+      withStdin "y\n"
+      $ withArgs ["--interactive", "--directory", dirPath]
+      $ captureProcessResult run
+    prStdout pr `shouldBe` BS8.pack ("remove " ++ dirPath ++ "? ")
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "removes empty directory with `-id' options" $ do
+    testFiles <- createTestFiles
+    let path    = head . emptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withStdin "y\n" $ withArgs ["-id", dirPath] $ captureProcessResult run
+    prStdout pr `shouldBe` BS8.pack ("remove " ++ dirPath ++ "? ")
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "removes empty directory with `--interactive --recursive' options" $ do
+    testFiles <- createTestFiles
+    let path    = head . emptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <-
+      withStdin "y\n"
+      $ withArgs ["--interactive", "--recursive", dirPath]
+      $ captureProcessResult run
+    prStdout pr
+      `shouldBe` BS8.pack ("examine files in directory " ++ dirPath ++ "? ")
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "removes empty directory with `-iR' options" $ do
+    testFiles <- createTestFiles
+    let path    = head . emptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withStdin "y\n" $ withArgs ["-iR", dirPath] $ captureProcessResult run
+    prStdout pr
+      `shouldBe` BS8.pack ("examine files in directory " ++ dirPath ++ "? ")
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "removes empty directory with `-ir' options" $ do
+    testFiles <- createTestFiles
+    let path    = head . emptyDirs $ testFiles
+    let dirName = fileOrDirName path
+    let dirPath = relativePath path
+    pr <- withStdin "y\n" $ withArgs ["-ir", dirPath] $ captureProcessResult run
+    prStdout pr
+      `shouldBe` BS8.pack ("examine files in directory " ++ dirPath ++ "? ")
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+    let removedDirPath = addTrailingPathSeparator trashPath ++ dirName
+    doesPathExist removedDirPath `shouldReturn` True
+    doesPathExist dirPath `shouldReturn` False
+    removeDirectoryRecursive removedDirPath
+    removeTestFiles testFiles
+  it "tries to remove /bin/rm" $ do
+    pr <- withStdin "n\n" $ withArgs ["/bin/rm"] $ captureProcessResult run
+    prStdout pr `shouldBe` BS8.pack
+      "override rwxr-xr-x  root/wheel restricted,compressed for /bin/rm? "
+    prStderr pr `shouldBe` ""
+    prExitCode pr `shouldBe` ExitSuccess
+    (isNothing . prException) pr `shouldBe` True
+
+main :: IO ()
+main = do
+  homePath <- getEnv "HOME"
+  let trashPath = homePath ++ "/.Trash"
+  hspec $ spec_run trashPath
