handsy 0.0.13.1 → 0.0.14
raw patch · 9 files changed
+266/−305 lines, 9 filesdep −errorsdep −lifted-basedep ~base
Dependencies removed: errors, lifted-base
Dependency ranges changed: base
Files
- handsy.cabal +5/−8
- src/System/Handsy.hs +88/−39
- src/System/Handsy/Actions.hs +0/−147
- src/System/Handsy/Internal.hs +23/−23
- src/System/Handsy/Local.hs +0/−12
- src/System/Handsy/Remote.hs +40/−41
- src/System/Handsy/Tutorial.hs +2/−0
- src/System/Handsy/Util.hs +81/−0
- test/Test.hs +27/−35
handsy.cabal view
@@ -1,8 +1,8 @@ name: handsy-version: 0.0.13.1+version: 0.0.14 synopsis: A DSL to describe common shell operations and interpeters for running them locally and remotely. description:- DEPRECATED. @handsy@ is a small library mainly for applications which should make some+ @handsy@ is a small library mainly for applications which should make some operations on remote machines by SSH. It currently provides you: . * A DSL describing basic system operations('command', 'readFile', 'writeFile' etc.)@@ -29,11 +29,10 @@ library exposed-modules: System.Handsy- System.Handsy.Tutorial- other-modules: System.Handsy.Actions- System.Handsy.Local System.Handsy.Remote+ System.Handsy.Util System.Handsy.Internal+ System.Handsy.Tutorial build-depends: base >=4.6 && <4.9 , bytestring , transformers@@ -43,8 +42,6 @@ , retry , data-default-class , split- , errors- , lifted-base hs-source-dirs: src default-language: Haskell2010 @@ -55,7 +52,7 @@ main-is: Test.hs default-language: Haskell2010 hs-source-dirs: test- build-depends: base >=4.6 && <4.9+ build-depends: base >=4.6 && < 4.8 , handsy , bytestring , tasty
src/System/Handsy.hs view
@@ -3,59 +3,108 @@ module System.Handsy ( Handsy- , handsyIO- , handsyLeft-- -- * Interpreters- , Options (..)-- -- ** Local , run - -- ** Remote- , runRemote- , Host- , SSHOptions (..)-- -- * Actions- , CommandOptions (..)- , command+ -- * Commands , shell- , command_- , shell_+ , command , readFile , writeFile , appendFile- , sleep- , mkTemp- , mkTempDir- , isRunning- , os - -- ** For remote actions- , pushFile- , pullFile+ -- * Helpers+ , shell_+ , command_ - -- * Utils- , IsReturnValue- , stdout- , stderr- , exitCode- , isSuccessful- , isExitSuccess- , strLines+ -- * Options+ , CommandOptions (..)+ , Options (..) -- * Re-exports , ExitCode (..) , def ) where -import Prelude hiding (appendFile, readFile, writeFile)+import Prelude hiding (appendFile, readFile,+ writeFile) -import System.Handsy.Actions-import System.Handsy.Internal-import System.Handsy.Local-import System.Handsy.Remote+import Data.Bool+import qualified Data.ByteString.Char8 as C8+import qualified Data.ByteString.Lazy as B+import qualified Data.ByteString.Lazy.Char8 as C+import System.Exit import Data.Default.Class-import System.Exit+import System.Process.ByteString.Lazy++import Text.ShellEscape++import System.Handsy.Internal hiding (shell)+import qualified System.Handsy.Internal as I++-- * Commands++-- | Runs a command+command :: FilePath -- ^ Command to run+ -> [String] -- ^ Arguments+ -> CommandOptions+ -> Handsy (ExitCode, B.ByteString, B.ByteString) -- ^ (status, stdout, stderr)+command cmd args opts = let cmd' = C8.unpack . C8.intercalate " " . map (bytes . bash . C8.pack) $ (cmd:args)+ in shell cmd' opts++{-| Executes the given string in shell. Example:++ > shell "ls" $~ def{cwd="/var/www"}+-}+shell :: String -- ^ String to execute+ -> CommandOptions+ -> Handsy (ExitCode, B.ByteString, B.ByteString) -- ^ (ExitCode, Stdout, Stderr)+shell cmd opts = let esc = C8.unpack . bytes . bash . C8.pack+ CommandOptions stdin' cwd' = opts+ in I.shell (bool ("cd " ++ esc cwd' ++ "; ") "" (null cwd') ++ cmd) stdin'++data CommandOptions =+ CommandOptions { stdin :: B.ByteString+ , cwd :: String+ }+ deriving Show++instance Default CommandOptions where+ def = CommandOptions "" ""++-- | Reads a file and returns the contents of the file.+readFile :: FilePath -> Handsy B.ByteString+readFile fp = command "cat" [fp] def >>= \case+ (ExitSuccess, stdout, _) -> return stdout+ (_, _, stderr) -> error $ "Error reading " ++ fp ++ "\nStderr was: " ++ C.unpack stderr++-- | @writeFile file str@ function writes the bytestring @str@, to the file @file@.+writeFile :: FilePath -> B.ByteString -> Handsy ()+writeFile fp s = command "dd" ["of=" ++ fp] def{stdin=s} >>= \case+ (ExitSuccess, _, _) -> return ()+ (_, _, stderr) -> error $ "Error writing to " ++ fp ++ "\nStderr was: " ++ C.unpack stderr++-- | @appendFile file str@ function appends the bytestring @str@, to the file @file@.+appendFile :: FilePath -> B.ByteString -> Handsy ()+appendFile fp s = command "dd" ["of=" ++ fp, "conv=notrunc", "oflag=append"] def{stdin=s} >>= \case+ (ExitSuccess, _, _) -> return ()+ (_, _, stderr) -> error $ "Error appending to " ++ fp ++ "\nStderr was: " ++ C.unpack stderr++-- | Same as 'command', but ExitFailure is a runtime error.+command_ :: FilePath -> [String] -> CommandOptions -> Handsy (B.ByteString, B.ByteString)+command_ path args opts = command path args opts >>= \case+ (ExitFailure code, _, stderr) -> error ('`':path ++ ' ' : show args ++ "` returned " ++ show code+ ++ "\nStderr was: " ++ C.unpack stderr)+ (ExitSuccess, stdout, stderr) -> return (stdout, stderr)++-- | Same as 'shell', but ExitFailure is a runtime error.+shell_ :: String -> CommandOptions -> Handsy (B.ByteString, B.ByteString)+shell_ cmd opts = shell cmd opts >>= \case+ (ExitFailure code, _, stderr) -> error ('`':cmd ++ "` returned " ++ show code+ ++ "\nStderr was: " ++ C.unpack stderr)+ (ExitSuccess, stdout, stderr) -> return (stdout, stderr)++-- | Executes the actions locally+run :: Options -> Handsy a -> IO a+run = interpretSimple (\cmdline -> readProcessWithExitCode "bash" ["-c", cmdline])+
− src/System/Handsy/Actions.hs
@@ -1,147 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}--module System.Handsy.Actions where--import Control.Applicative-import Control.Concurrent-import Control.Error-import Control.Monad.IO.Class-import Control.Monad.Trans.Class-import qualified Data.ByteString.Char8 as B8-import qualified Data.ByteString.Lazy as BL-import qualified Data.ByteString.Lazy.Char8 as BL8-import Data.Default.Class-import Data.List.Split-import Prelude hiding (appendFile, readFile,- writeFile)-import System.Exit-import System.Handsy.Internal-import Text.ShellEscape---- | Runs a command-command :: FilePath -- ^ Command to run- -> [String] -- ^ Arguments- -> CommandOptions- -> Handsy (ExitCode, BL.ByteString, BL.ByteString) -- ^ (status, stdout, stderr)-command cmd args opts = let cmd' = B8.unpack . B8.intercalate " " . map (bytes . bash . B8.pack) $ (cmd:args)- in shell cmd' opts--{-| Executes the given string in shell. Example:-- > shell "ls" $~ def{cwd="/var/www"}--}-shell :: String -- ^ String to execute- -> CommandOptions- -> Handsy (ExitCode, BL.ByteString, BL.ByteString) -- ^ (ExitCode, Stdout, Stderr)-shell cmd opts = let esc = B8.unpack . bytes . bash . B8.pack- CommandOptions stdin' cwd' = opts- in shellF ((if null cwd' then "" else ("cd " ++ esc cwd' ++ "; ")) ++ cmd) stdin'--data CommandOptions =- CommandOptions { stdin :: BL.ByteString- , cwd :: String- }--instance Default CommandOptions where- def = CommandOptions "" ""---- | Reads a file and returns the contents of the file.-readFile :: FilePath -> Handsy BL.ByteString-readFile fp = command "cat" [fp] def >>= \case- (ExitSuccess, sout, _) -> return sout- (_, _, serr) -> lift . left $ "Serror reading " ++ fp ++ "\nSerr was: " ++ BL8.unpack serr---- | @writeFile file str@ function writes the bytestring @str@, to the file @file@.-writeFile :: FilePath -> BL.ByteString -> Handsy ()-writeFile fp s = command "dd" ["of=" ++ fp] def{stdin=s} >>= \case- (ExitSuccess, _, _) -> return ()- (_, _, serr) -> lift . left $ "Serror writing to " ++ fp ++ "\nSerr was: " ++ BL8.unpack serr---- | @appendFile file str@ function appends the bytestring @str@, to the file @file@.-appendFile :: FilePath -> BL.ByteString -> Handsy ()-appendFile fp s = command "dd" ["of=" ++ fp, "conv=notrunc", "oflag=append"] def{stdin=s} >>= \case- (ExitSuccess, _, _) -> return ()- (_, _, serr) -> lift . left $ "Serror appending to " ++ fp ++ "\nSerr was: " ++ BL8.unpack serr---- | Same as 'command', but ExitFailure ends the computation.-command_ :: FilePath -> [String] -> CommandOptions -> Handsy (BL.ByteString, BL.ByteString)-command_ path args opts = command path args opts >>= \case- (ExitFailure code, _, serr) -> lift $ left ('`':path ++ ' ' : show args ++ "` returned " ++ show code- ++ "\nSerr was: " ++ BL8.unpack serr)- (ExitSuccess, sout, serr) -> return (sout, serr)---- | Same as 'shell', but ExitFailure ends the computation.-shell_ :: String -> CommandOptions -> Handsy (BL.ByteString, BL.ByteString)-shell_ cmd opts = shell cmd opts >>= \case- (ExitFailure code, _, serr) -> lift $ left ('`':cmd ++ "` returned " ++ show code- ++ "\nSerr was: " ++ BL8.unpack serr)- (ExitSuccess, sout, serr) -> return (sout, serr)---- * Helpers for parsing return values of 'command' and 'shell'--class IsReturnValue a where- stdout :: a -> BL.ByteString- stderr :: a -> BL.ByteString- exitCode :: a -> ExitCode--instance IsReturnValue (BL.ByteString, BL.ByteString) where- stdout = fst- stderr = snd- exitCode = const ExitSuccess--instance IsReturnValue (ExitCode, BL.ByteString, BL.ByteString) where- stdout = \case (_, a, _) -> a- stderr = \case (_, _, a) -> a- exitCode = \case (a, _, _) -> a--isSuccessful :: IsReturnValue a => a -> Bool-isSuccessful = isExitSuccess . exitCode--isExitSuccess :: ExitCode -> Bool-isExitSuccess = \case- ExitSuccess -> True- ExitFailure _ -> False---- | Extract lines from a ByteString. Useful for parsing unix commands.-strLines :: BL.ByteString -> [String]-strLines = lines . BL8.unpack---- | Waits specified number of seconds-sleep :: Int -> Handsy ()-sleep = liftIO . threadDelay . (* 1000000)---- | Creates a temporary file-mkTemp :: String -> Handsy String-mkTemp suffix = do- out <- strLines .stdout <$> command_ "mktemp" (bool ["--suffix=" ++ suffix] [] (null suffix)) def- lift $ tryHead ("weird output from mktemp: " ++ show out) out---- | Creates a temporary directory-mkTempDir :: String -> Handsy String-mkTempDir suffix- = command_ "mktemp" ("-d" : bool ["--suffix" ++ suffix] [] (null suffix)) def- >>= lift . tryHead "weird output from mktemp" . strLines . stdout---- | Returns if the specified process is running. Uses `pidof`-isRunning :: String -> Handsy Bool-isRunning p = isSuccessful <$> command "pidof" ["-s", "-x", p] def--data OS = NixOS | Debian | Ubuntu | RHEL | CentOS | Fedora | ArchLinux- deriving (Show, Eq)--{-| Guesses the os using `/etc/os-release`. This currently only supports Linux distributions- abiding systemd standards. -}-os :: Handsy (Maybe OS)-os = parseOsRelease <$> readFile "/etc/os-release" >>= return . \case- Just "ubuntu" -> Just Ubuntu- Just "debian" -> Just Debian- Just "nixos" -> Just NixOS- Just "rhel" -> Just RHEL- Just "centos" -> Just CentOS- Just "fedora" -> Just Fedora- Just "arch" -> Just ArchLinux- _ -> Nothing- where parseOsRelease = fmap (filter $ not . flip elem ['\'', '"']) -- Hack to unquote- <$> lookup "ID" . map ((\(x:xs) -> (x, concat xs)) . splitOn "=") . strLines
src/System/Handsy/Internal.hs view
@@ -2,14 +2,18 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} -module System.Handsy.Internal where+module System.Handsy.Internal+ ( Handsy+ , interpret+ , interpretSimple+ , shell+ , Options (..)+ )+ where -import Control.Error-import Control.Exception.Lifted (bracket)+import Control.Exception (bracket) import Control.Monad-import Control.Monad.IO.Class import Control.Monad.Operational-import Control.Monad.Trans.Class import qualified Data.ByteString.Lazy as B import Data.Default.Class import System.Exit (ExitCode)@@ -21,10 +25,10 @@ data HandsyInstruction a where Shell :: String -> B.ByteString -> HandsyInstruction (ExitCode, StdOut, StdErr) -type Handsy a = ProgramT HandsyInstruction Script a+type Handsy a = ProgramT HandsyInstruction IO a -shellF :: FilePath -> B.ByteString -> Handsy (ExitCode, B.ByteString, B.ByteString)-shellF cmd stdin = singleton $ Shell cmd stdin+shell :: FilePath -> B.ByteString -> Handsy (ExitCode, B.ByteString, B.ByteString)+shell cmd stdin = singleton $ Shell cmd stdin data Options = Options { debug :: Bool -- ^ Log commands to stderr before running@@ -34,27 +38,23 @@ def = Options False interpret :: forall r . forall a- . Script r -- ^ Acquire resource- -> (r -> Script ()) -- ^ Release resource- -> (r -> String -> B.ByteString -> Script (ExitCode, B.ByteString, B.ByteString))+ . IO r -- ^ Acquire resource+ -> (r -> IO ()) -- ^ Release resource+ -> (r -> String -> B.ByteString+ -> IO (ExitCode, B.ByteString, B.ByteString)) -> Options -> Handsy a- -> EitherT String IO a+ -> IO a interpret acquire destroy f opts handsy = bracket acquire destroy (`go` handsy)- where go :: r -> Handsy a -> Script a+ where go :: r -> Handsy a -> IO a go res h = viewT h >>= \case- Return x -> right x- Shell cmdline stdin :>>= k -> when (debug opts) (liftIO $ hPutStrLn stderr cmdline)+ Return x -> return x+ Shell cmdline stdin :>>= k -> when (debug opts) (hPutStrLn stderr cmdline) >> f res cmdline stdin >>= go res . k -interpretSimple :: (FilePath -> B.ByteString -> Script (ExitCode, B.ByteString, B.ByteString)) -- ^ 'readProcessWithExitCode'+interpretSimple :: (FilePath -> B.ByteString+ -> IO (ExitCode, B.ByteString, B.ByteString)) -- ^ 'readProcessWithExitCode' -> Options -> Handsy a- -> Script a+ -> IO a interpretSimple f = interpret (return ()) (const (return ())) (const f)--handsyIO :: IO a -> Handsy a-handsyIO = lift . scriptIO--handsyLeft :: String -> Handsy a-handsyLeft = lift . left
− src/System/Handsy/Local.hs
@@ -1,12 +0,0 @@-module System.Handsy.Local where--import Control.Error-import System.Handsy.Internal-import System.Process.ByteString.Lazy--runS :: Options -> Handsy a -> Script a-runS opts = interpretSimple (\cmdline -> scriptIO . readProcessWithExitCode "bash" ["-c", cmdline]) opts---- | Executes the actions locally-run :: Options -> Handsy a -> IO (Either String a)-run opts = runEitherT . runS opts
src/System/Handsy/Remote.hs view
@@ -1,62 +1,67 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -module System.Handsy.Remote where+module System.Handsy.Remote+ ( runRemote+ , Host + -- * Options+ , SSHOptions (..)++ -- * Helpers+ , pushFile+ , pullFile++ -- * Re-exports+ , def+ ) where+ import Prelude hiding (appendFile, readFile, writeFile) +import System.Handsy+import System.Handsy.Internal (interpret, interpretSimple)+import System.Handsy.Util+ import Control.Applicative import Control.Concurrent-import Control.Error import Control.Monad import Control.Retry+import Data.Bool import qualified Data.ByteString.Lazy as B++import Control.Monad.IO.Class import Data.Default.Class-import Data.Monoid-import System.Exit-import System.Handsy.Actions-import System.Handsy.Internal-import System.Handsy.Local type Host = String data SSHOptions = SSHOptions { -- | Path of `ssh` command- sshPath :: FilePath,-- -- | User- sshUser :: Maybe String,+ sshPath :: FilePath, -- | Port to connect- sshPort :: Int,-- -- | Identity file to use- identityFile :: Maybe FilePath,-- -- | Connect timeout- connectTimeout :: Int,+ sshPort :: Int, {-| Whether to use control master for SSH connections. This significantly reduces execution time. -}- controlMaster :: Bool+ controlMaster :: Bool } instance Default SSHOptions where- def = SSHOptions "ssh" Nothing 22 Nothing 30 True+ def = SSHOptions "ssh" 22 True -acquireCM :: Host -> SSHOptions -> Script FilePath+acquireCM :: Host -> SSHOptions -> IO FilePath acquireCM host opts = do- cm <- scriptIO (run def $ head . strLines . fst <$> command_ "mktemp" ["-u", "--suffix=.handsy"] def) >>= hoistEither+ cm <- run def $ head . strLines . fst <$> command_ "mktemp" ["-u", "--suffix=.handsy"] def let (ssh, params) = genSsh opts (Just cm)- _ <- scriptIO . forkIO . void . run def . void $ command_ ssh (params ++ ["-M", "-N", host]) def- scriptIO (waitForCM cm) >>= bool (left "Error establishing ControlMaster connection") (right ())+ _ <- forkIO . run def . void $ command_ ssh (params ++ ["-M", "-N", host]) def+ bool (error "Error establishing ControlMaster connection") () <$> waitForCM cm return cm where checkCM :: FilePath -> IO Bool- checkCM p = fmap (either (const False) id) . run def $ do+ checkCM p = run def $ do let args = snd (genSsh opts Nothing) ++ ["-o", "ControlPath=" ++ p, "-O", "check"] command (sshPath opts) args def >>= return . \case (ExitSuccess, _, _) -> True@@ -64,30 +69,24 @@ waitForCM p = retrying (limitRetries 30) (\_ n -> return (not n)) (checkCM p) -releaseCM :: FilePath -> Script ()-releaseCM p = (scriptIO . run def{debug=False} $ void $ command_ "rm" ["-f", p] def) >>= hoistEither+releaseCM :: FilePath -> IO ()+releaseCM p = run def{debug=False} $ void $ command_ "rm" ["-f", p] def genSsh :: SSHOptions -> Maybe FilePath -> (FilePath, [String])-genSsh opts cm = (sshPath opts,- [ "-p", show $ sshPort opts- , "-o", "ConnectTimeout=" ++ show (connectTimeout opts)]- <> maybe mempty (("-l":) . pure) (sshUser opts)- <> maybe mempty (("-i":) . pure) (identityFile opts)- <> maybe mempty (("-S":) . pure) cm- )+genSsh opts cm = (sshPath opts, ["-p", show $ sshPort opts] ++ maybe [] (\i->["-S", i]) cm) runSsh :: Host -> SSHOptions -> Maybe FilePath -> String -> B.ByteString- -> Script (ExitCode, B.ByteString, B.ByteString)+ -> IO (ExitCode, B.ByteString, B.ByteString) runSsh host opts cm cmdline stdin' = let (ssh, params) = genSsh opts cm- in runS def{debug=False} $ command ssh (params ++ [host] ++ [cmdline]) def{stdin=stdin'}+ in run def{debug=False} $ command ssh (params ++ [host] ++ [cmdline]) def{stdin=stdin'} -- | Executes the actions at a remote host-runRemote :: Options -> Host -> SSHOptions -> Handsy a -> IO (Either String a)-runRemote opts host sshOpts = runEitherT .+runRemote :: Options -> Host -> SSHOptions -> Handsy a -> IO a+runRemote opts host sshOpts = case controlMaster sshOpts of False -> interpretSimple (runSsh host sshOpts Nothing) opts- True -> interpret (acquireCM host sshOpts)+ True -> interpret (acquireCM host sshOpts) releaseCM (runSsh host sshOpts . Just) opts@@ -96,10 +95,10 @@ pushFile :: FilePath -- ^ Local path of source -> FilePath -- ^ Remote path of destination -> Handsy ()-pushFile local remote = handsyIO (B.readFile local) >>= writeFile remote+pushFile local remote = liftIO (B.readFile local) >>= writeFile remote -- | Fetches a file from remote host pullFile :: FilePath -- ^ Remote path of source -> FilePath -- ^ Local path of destination -> Handsy ()-pullFile remote local = readFile remote >>= handsyIO . B.writeFile local+pullFile remote local = readFile remote >>= liftIO . B.writeFile local
src/System/Handsy/Tutorial.hs view
@@ -9,6 +9,8 @@ ) where import System.Handsy+import System.Handsy.Remote+import System.Handsy.Util {- $introduction @handsy@ is a small library mainly for applications which should make some
+ src/System/Handsy/Util.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}++module System.Handsy.Util where++import Control.Applicative+import Control.Concurrent+import Control.Monad.IO.Class+import Data.Bool+import qualified Data.ByteString.Lazy.Char8 as C+import Data.List.Split+import Prelude hiding (appendFile, readFile,+ writeFile)+import System.Handsy++-- * Helpers for parsing return values of 'command' and 'shell'++class IsReturnValue a where+ stdout :: a -> C.ByteString+ stderr :: a -> C.ByteString+ exitCode :: a -> ExitCode++instance IsReturnValue (C.ByteString, C.ByteString) where+ stdout = fst+ stderr = snd+ exitCode = const ExitSuccess++instance IsReturnValue (ExitCode, C.ByteString, C.ByteString) where+ stdout = \case (_, a, _) -> a+ stderr = \case (_, _, a) -> a+ exitCode = \case (a, _, _) -> a++isSuccessful :: IsReturnValue a => a -> Bool+isSuccessful = isExitSuccess . exitCode++isExitSuccess :: ExitCode -> Bool+isExitSuccess = \case+ ExitSuccess -> True+ ExitFailure _ -> False++-- | Extract lines from a ByteString. Useful for parsing unix commands.+strLines :: C.ByteString -> [String]+strLines = lines . C.unpack++-- * Frequently used functionality++-- | Waits specified number of seconds+sleep :: Int -> Handsy ()+sleep = liftIO . threadDelay . (* 1000000)++-- | Creates a temporary file+mkTemp :: String -> Handsy String+mkTemp suffix = head . strLines . fst+ <$> command_ "mktemp" (bool ["--suffix=" ++ suffix] [] (null suffix)) def++-- | Creates a temporary directory+mkTempDir :: String -> Handsy String+mkTempDir suffix = head . strLines . fst+ <$> command_ "mktemp" ("-d" : bool ["--suffix" ++ suffix] [] (null suffix)) def++-- | Returns if the specified process is running. Uses `pidof`+isRunning :: String -> Handsy Bool+isRunning p = isSuccessful <$> command "pidof" ["-s", "-x", p] def++data OS = NixOS | Debian | Ubuntu | RHEL | CentOS | Fedora | ArchLinux+ deriving (Show, Eq)++{-| Guesses the os using `/etc/os-release`. This currently only supports Linux distributions+ abiding systemd standards. -}+os :: Handsy (Maybe OS)+os = parseOsRelease <$> readFile "/etc/os-release" >>= return . \case+ Just "ubuntu" -> Just Ubuntu+ Just "debian" -> Just Debian+ Just "nixos" -> Just NixOS+ Just "rhel" -> Just RHEL+ Just "centos" -> Just CentOS+ Just "fedora" -> Just Fedora+ Just "arch" -> Just ArchLinux+ _ -> Nothing+ where parseOsRelease = fmap (filter $ not . flip elem "'\"") -- Hack to unquote+ <$> lookup "ID" . map ((\(x:xs) -> (x, concat xs)) . splitOn "=") . strLines
test/Test.hs view
@@ -1,14 +1,15 @@-{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-missing-signatures #-} module Main where -import Prelude hiding (appendFile, readFile,- writeFile)+import System.Handsy as H -import System.Handsy+{- These aren't currently used in tests, but I+ import them for coverage reports. -}+import System.Handsy.Remote as H+import System.Handsy.Util as H import Control.Applicative import qualified Data.ByteString.Lazy as B@@ -22,67 +23,58 @@ arbitraryBinary = B.pack [1..255] case_writeFile = do- f <- run def $ do+ f <- H.run def $ do tmp <- mkTemp ""- writeFile tmp arbitraryBinary+ H.writeFile tmp arbitraryBinary return tmp- case f of- Right fname -> B.readFile fname >>= assertEqual "" arbitraryBinary- Left err -> assertFailure err+ ret <- B.readFile f+ assertEqual "" arbitraryBinary ret case_readFile = do- run def (mkTemp "") >>= \case- Left err -> assertFailure err- Right tmp -> do- B.writeFile tmp arbitraryBinary- run def (readFile tmp) >>= \case- Left err -> assertFailure err- Right ret -> assertEqual "" arbitraryBinary ret+ tmp <- H.run def $ mkTemp ""+ B.writeFile tmp arbitraryBinary+ ret <- H.run def $ H.readFile tmp+ assertEqual "" arbitraryBinary ret case_appendFile = do- ret <- run def $ do+ ret <- H.run def $ do tmp <- mkTemp "" - writeFile tmp "ut"- appendFile tmp "demir"+ H.writeFile tmp "ut"+ H.appendFile tmp "demir" - readFile tmp+ H.readFile tmp - either assertFailure (assertEqual "" "utdemir") ret+ assertEqual "" "utdemir" ret case_shell = do- ret <- run def $ do+ (h1, h2) <- H.run def $ do tmp <- mkTemp "" - writeFile tmp (B.pack [1..255])+ H.writeFile tmp (B.pack [1..255]) h1 <- takeWhile isHexDigit . C.unpack . fst <$> command_ "md5sum" [tmp] def h2 <- takeWhile isHexDigit . C.unpack . fst <$> shell_ ("cat " ++ tmp ++ " | md5sum -") def return (h1, h2) - case ret of- Left err -> assertFailure err- Right (h1, h2) -> assertEqual "" h1 h2+ assertEqual "" h1 h2 case_exit = do- ret <- run def $ do+ (e1, e2) <- H.run def $ do (e1, _, _) <- command "grep" [] def (e2, _, _) <- command "id" [] def return (e1, e2) - case ret of- Right (ExitFailure _, ExitSuccess) -> return ()- other -> assertFailure $ "Invalid return values: " ++ show other+ case (e1, e2) of+ (ExitFailure _, ExitSuccess) -> return ()+ _ -> assertFailure $ "Invalid return values: " ++ show (e1, e2) case_cwd = do- ret <- run def $ do+ (temp, pwd) <- H.run def $ do temp <- mkTempDir "" pwd:[] <- strLines . stdout <$> command_ "pwd" [] def{cwd=temp} return (temp, pwd)-- case ret of- Right (temp, pwd) -> assertEqual "" temp pwd- other -> assertFailure $ "Invalid paths: " ++ show other+ assertEqual "" temp pwd main :: IO () main = $(defaultMainGenerator)