{-# language LambdaCase #-} -- | -- Module : System.RawFilePath -- Copyright : (c) XT 2016 -- License : Apache 2.0 -- -- Maintainer : e@xtendo.org -- Stability : experimental -- Portability : POSIX -- -- Higher-level API for the 'RawFilePath'-variants of functions in the 'unix' -- module. module System.RawFilePath ( RawFilePath -- * Process , callProcess , callProcessSilent , readProcess , readProcessEither -- * Directory , listDirectory , getDirectoryFiles , getDirectoryFilesRecursive , copyFile , getHomeDirectory , doesFileExist , doesDirectoryExist , setCurrentDirectory , tryRemoveFile ) where import Data.Monoid import Control.Monad import Control.Exception import Data.ByteString (ByteString) import qualified Data.ByteString as B import System.IO import System.IO.Error import System.Exit (ExitCode(..)) import Foreign.Marshal.Alloc (allocaBytes) import System.Posix.ByteString processError :: RawFilePath -> IOError processError cmd = mkIOError userErrorType ("calling process " <> show cmd) Nothing (Just $ show cmd) -- | Creates a new process to run the specified command with the given -- arguments, and waits for it to finish. Throws an exception if the process -- returns a nonzero exit code. -- -- > *System.RawFilePath> callProcess "ls" ["-a", "src"] -- > . .. System callProcess :: RawFilePath -- ^ Command to run -> [ByteString] -- ^ Command arguments -> IO () callProcess cmd args = do pid <- forkProcess $ executeFile cmd True args Nothing getProcessStatus True False pid >>= \case Just status -> case status of Exited exitCode -> case exitCode of ExitSuccess -> return () ExitFailure _ -> failure _ -> failure Nothing -> failure where failure = ioError (processError cmd) -- | Same as 'callProcess' except the child process will share the parent\'s -- stdout and stderr, meaning it won\'t print anything. callProcessSilent :: RawFilePath -- ^ Command to run -> [ByteString] -- ^ Command arguments -> IO ExitCode callProcessSilent cmd args = do pid <- forkProcess $ do closeFd stdOutput closeFd stdError executeFile cmd True args Nothing getProcessStatus True False pid >>= \case Just status -> case status of Exited exitCode -> return exitCode _ -> failure Nothing -> failure where failure = ioError (processError cmd) getContentsAndClose :: Handle -> IO ByteString getContentsAndClose h = B.hGetContents h <* hClose h -- | Runs a command, reads its standard output strictly, blocking until the process terminates, and returns the output as 'ByteString'. -- -- > *System.RawFilePath> readProcess "date" ["+%s"] -- > "1469999314\n" readProcess :: RawFilePath -- ^ Command to run -> [ByteString] -- ^ Command arguments -> IO ByteString -- ^ The output from the command readProcess cmd args = do (fd0, fd1) <- createPipe pid <- forkProcess $ do closeFd fd0 closeFd stdOutput closeFd stdError void $ dupTo fd1 stdOutput executeFile cmd True args Nothing closeFd fd1 (fdToHandle fd0 >>= getContentsAndClose) <* getProcessStatus True False pid -- | A \'safer\' approach to 'readProcess'. Depending on the exit status of -- the process, this function will return output either from stderr or stdout. -- -- > *System.RawFilePath> readProcessEither "date" ["%s"] -- > Left "date: invalid date \226\128\152%s\226\128\153\n" -- > *System.RawFilePath> readProcessEither "date" ["+%s"] -- > Right "1469999817\n" readProcessEither :: RawFilePath -- ^ Command to run -> [ByteString] -- ^ Command arguments -> IO (Either ByteString ByteString) -- ^ Either the stedrr output from -- the command if it finished with a nonzero exit code, or the stdout data -- if it finished normally. readProcessEither cmd args = do (fd0, fd1) <- createPipe (efd0, efd1) <- createPipe pid <- forkProcess $ do closeFd fd0 closeFd stdOutput void $ dupTo fd1 stdOutput closeFd efd0 closeFd stdError void $ dupTo efd1 stdError executeFile cmd True args Nothing closeFd fd1 closeFd efd1 content <- fdToHandle fd0 >>= getContentsAndClose err <- fdToHandle efd0 >>= getContentsAndClose getProcessStatus True False pid >>= \case Just status -> case status of Exited exitCode -> case exitCode of ExitSuccess -> return $ Right content ExitFailure _ -> return $ Left err _ -> return $ Left err Nothing -> return $ Left err -- | Get a list of files in the specified directory, excluding "." and ".." -- -- > *System.RawFilePath> listDirectory "src" -- > ["..","System","."] listDirectory :: RawFilePath -- ^ The path of directory to inspect -> IO [RawFilePath] -- ^ A list of files in the directory listDirectory dirPath = filter f <$> getDirectoryFiles dirPath where f p = p /= "." && p /= ".." -- | Get a list of files in the specified directory, including "." and ".." -- -- > *System.RawFilePath> getDirectoryFiles "src" -- > ["..","System","."] getDirectoryFiles :: RawFilePath -- ^ The path of directory to inspect -> IO [RawFilePath] -- ^ A list of files in the directory getDirectoryFiles dirPath = bracket open close repeatRead where open = openDirStream dirPath close = closeDirStream repeatRead stream = do d <- readDirStream stream if B.length d == 0 then return [] else do rest <- repeatRead stream return $ d : rest -- | Recursively get all files in all subdirectories of the specified -- directory. -- -- > *System.RawFilePath> getDirectoryFilesRecursive "src" -- > ["src/System/RawFilePath.hs"] getDirectoryFilesRecursive :: RawFilePath -- ^ The path of directory to inspect -> IO [RawFilePath] -- ^ A list of relative paths getDirectoryFilesRecursive path = do names <- map (path ) . filter (\x -> x /= ".." && x /= ".") <$> getDirectoryFiles path inspectedNames <- mapM inspect names return $ concat inspectedNames where inspect :: RawFilePath -> IO [RawFilePath] inspect p = fmap isDirectory (getFileStatus p) >>= \i -> if i then getDirectoryFilesRecursive p else return [p] defaultFlags :: OpenFileFlags defaultFlags = OpenFileFlags { append = False , exclusive = False , noctty = True , nonBlock = False , trunc = False } -- Buffer size for file copy bufferSize :: Int bufferSize = 4096 -- | Copy a file from the source path to the destination path. copyFile :: RawFilePath -- ^ The source path -> RawFilePath -- ^ The destination path -> IO () copyFile srcPath tgtPath = do bracket ropen hClose $ \hi -> bracket topen hClose $ \ho -> allocaBytes bufferSize $ copyContents hi ho rename tmpPath tgtPath where ropen = openFd srcPath ReadOnly Nothing defaultFlags >>= fdToHandle topen = createFile tmpPath stdFileMode >>= fdToHandle tmpPath = tgtPath <> ".copyFile.tmp" copyContents hi ho buffer = do count <- hGetBuf hi buffer bufferSize when (count > 0) $ do hPutBuf ho buffer count copyContents hi ho buffer -- | A function that "tries" to remove a file. If the file does not exist, -- nothing happens. tryRemoveFile :: RawFilePath -> IO () tryRemoveFile path = catchIOError (removeLink path) $ \e -> unless (isDoesNotExistError e) $ ioError e -- | Returns the current user\'s home directory. getHomeDirectory :: IO RawFilePath getHomeDirectory = getEnv "HOME" >>= maybe err return where err = ioError $ mkIOError doesNotExistErrorType errMsg Nothing Nothing errMsg = "Environment variable $HOME" -- | Returns 'True' if the argument file exists and is not a directory. doesFileExist :: RawFilePath -> IO Bool doesFileExist path = fileExist path >>= \i -> if i then not . isDirectory <$> getFileStatus path else return False -- | Returns 'True' if the argument file exists and is a directory. doesDirectoryExist :: RawFilePath -> IO Bool doesDirectoryExist path = fileExist path >>= \i -> if i then isDirectory <$> getFileStatus path else return False -- | Change the working directory to the given path. setCurrentDirectory :: RawFilePath -> IO () setCurrentDirectory = changeWorkingDirectory -- An extremely simplistic approach for path concatenation. infixr 5 () :: RawFilePath -> RawFilePath -> RawFilePath a b = mconcat [a, "/", b]