simple-cmd 0.2.4 → 0.2.5
raw patch · 9 files changed
+610/−583 lines, 9 filesdep +time
Dependencies added: time
Files
- ChangeLog.md +4/−0
- SimpleCmd.hs +0/−482
- SimpleCmd/Git.hs +0/−74
- SimpleCmd/Rpm.hs +0/−17
- simple-cmd.cabal +12/−10
- src/SimpleCmd.hs +496/−0
- src/SimpleCmd/Git.hs +74/−0
- src/SimpleCmd/Rpm.hs +17/−0
- test/Spec.hs +7/−0
ChangeLog.md view
@@ -1,5 +1,9 @@ # Version history for simple-cmd +## 0.2.5 (2022-04-24)+- cmdN: quote arguments with show+- add timeIO: runs action and prints the time it took+ ## 0.2.4 (2022-03-27) - error' and warning are now strict - pipeBool: return False if either end fails
− SimpleCmd.hs
@@ -1,482 +0,0 @@-{-# LANGUAGE CPP #-}--{-|-Some simple String wrappers of `readProcess`, `readProcessWithExitCode`,-`rawSystem` from the Haskell <https://hackage.haskell.org/package/process process> library.--Simplest is--@cmd_ :: String -> [String] -> IO ()@--which outputs to stdout. For example:--@cmd_ "git" ["clone", url]@--Then--@cmd :: String -> [String] -> IO String@--returns stdout as a @String@.--There are also @cmdBool@, @cmdMaybe@, @cmdLines@, @shell@, and others.--Other examples:--@grep_ pat file :: IO Bool@--@sudo c args :: IO ()@---}--module SimpleCmd (- cmd, cmd_,- cmdBool,- cmdIgnoreErr, {- badly named -}- cmdLines,- cmdMaybe,- cmdFull,- cmdLog, cmdlog {-TODO: remove for 0.3 -},- cmdN,- cmdQuiet,- cmdSilent,- cmdStdIn,- cmdStdErr,- cmdTry_,- cmdStderrToStdout,- cmdStderrToStdoutIn,- needProgram,- error',- warning,- logMsg,- (+-+),- removePrefix, removeStrictPrefix, removeSuffix,- egrep_, grep, grep_,- shell, shell_,- shellBool,- sudo, sudo_,- PipeCommand,- pipe, pipe_, pipeBool,- pipe3, pipe3_, pipeFile_,- ifM,- whenM,- filesWithExtension,- fileWithExtension- ) where--#if !MIN_VERSION_base(4,8,0)-import Control.Applicative ((<$>))-#endif-import Control.Monad.Extra--import Data.List (-#if !MIN_VERSION_filepath(1,4,2)- isSuffixOf,-#endif- stripPrefix)-import Data.Maybe (isJust, isNothing, fromMaybe)--import System.Directory (findExecutable, listDirectory)-import System.Exit (ExitCode (..))-import System.FilePath-import System.IO (hGetContents, hPutStr, hPutStrLn, IOMode(ReadMode),- stderr, stdout, withFile, Handle)-import System.Posix.User (getEffectiveUserID)-import System.Process (createProcess, CreateProcess (cmdspec), proc,- ProcessHandle,- rawSystem, readProcess,- readProcessWithExitCode, runProcess, showCommandForUser,- std_err, std_in, std_out,- StdStream(CreatePipe, UseHandle),- waitForProcess, withCreateProcess)--removeTrailingNewline :: String -> String-removeTrailingNewline "" = ""-removeTrailingNewline str =- if last str == '\n'- then init str- else str--quoteCmd :: String -> [String] -> String-quoteCmd = showCommandForUser---- | Alias for errorWithoutStackTrace (for base >= 4.9)------ @since 0.1.4-error' :: String -> a-#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,9,0))-error' s = errorWithoutStackTrace $! s-#else-error' s = error $! s-#endif---- | @cmd c args@ runs a command in a process and returns stdout-cmd :: String -- ^ command to run- -> [String] -- ^ list of arguments- -> IO String -- ^ stdout-cmd c args = cmdStdIn c args ""---- | @cmd_ c args@ runs command in a process, output goes to stdout and stderr-cmd_ :: String -> [String] -> IO ()-cmd_ c args = do- ret <- rawSystem c args- case ret of- ExitSuccess -> return ()- ExitFailure n -> error' $ quoteCmd c args +-+ "failed with exit code" +-+ show n--boolWrapper :: IO ExitCode -> IO Bool-boolWrapper pr = do- ret <- pr- case ret of- ExitSuccess -> return True- ExitFailure _ -> return False---- | @cmdBool c args@ runs a command, and return Boolean status-cmdBool :: String -> [String] -> IO Bool-cmdBool c args =- boolWrapper (rawSystem c args)---- | @cmdMaybe c args@ runs a command, maybe returning output if it succeeds-cmdMaybe :: String -> [String] -> IO (Maybe String)-cmdMaybe c args = do- (ok, out, _err) <- cmdFull c args ""- return $ if ok then Just out else Nothing---- | @cmdLines c args@ runs a command, and returns list of stdout lines------ @since 0.1.1-cmdLines :: String -> [String] -> IO [String]-cmdLines c args = lines <$> cmd c args---- | @cmdStdIn c args inp@ runs a command, passing input string as stdin, and returns stdout-cmdStdIn :: String -> [String] -> String -> IO String-cmdStdIn c args inp = removeTrailingNewline <$> readProcess c args inp---- | @shell cs@ runs a command string in a shell, and returns stdout-shell :: String -> IO String-shell cs = cmd "sh" ["-c", cs]---- | @shell_ cs@ runs a command string in a shell, output goes to stdout-shell_ :: String -> IO ()-shell_ cs = cmd_ "sh" ["-c", cs]---- | @shellBool cs@ runs a command string in a shell, output goes to stdout------ @since 0.2.0-shellBool :: String -> IO Bool-shellBool cs =- boolWrapper (rawSystem "sh" ["-c", cs])---- FIXME cmdLog_--- | @cmdLog c args@ logs a command with a datestamp------ @since 0.1.4-cmdLog :: String -> [String] -> IO ()-cmdLog c args = do- logMsg $ unwords $ c:args- cmd_ c args---- | @cmdlog@ deprecated alias for 'cmdLog' (will be removed in 0.3)-cmdlog :: String -> [String] -> IO ()-cmdlog = cmdLog---- | @logMsg msg@ outputs message with a timestamp-logMsg :: String -> IO ()-logMsg msg = do- date <- cmd "date" ["+%T"]- putStrLn $ date +-+ msg---- | @cmdN c args@ dry-runs a command: prints command to stdout - more used for debugging-cmdN :: String -> [String] -> IO ()-cmdN c args = putStrLn $ unwords $ c:args---- | @cmdStdErr c args@ runs command in a process, returning stdout and stderr-cmdStdErr :: String -> [String] -> IO (String, String)-cmdStdErr c args = do- (_ok, out, err) <- cmdFull c args ""- return (out, err)---- -- | @cmdAssert msg c args@ runs command, if it fails output msg as error'.--- cmdAssert :: String -> String -> [String] -> IO ()--- cmdAssert msg c args = do--- ret <- rawSystem c args--- case ret of--- ExitSuccess -> return ()--- ExitFailure _ -> error' msg---- | @cmdQuiet c args@ runs a command hiding stderr, if it succeeds returns stdout-cmdQuiet :: String -> [String] -> IO String-cmdQuiet c args = do- (ok, out, err) <- cmdFull c args ""- return $ if ok- then out- else error' $ quoteCmd c args +-+ "failed with\n" ++ err---- | @cmdSilent c args@ runs a command hiding stdout: stderr is only output if it fails.-cmdSilent :: String -> [String] -> IO ()-cmdSilent c args = do- (ret, _, err) <- cmdFull c args ""- unless ret $- error' $ quoteCmd c args +-+ "failed with\n" ++ err---- -- | @cmdSilentIn c args inp@ is like @cmdSilent@ but additionally takes some stdin--- cmdSilentIn :: String -> [String] -> String -> IO ()--- cmdSilentIn c args inp = do--- (ret, _, err) <- cmdFull c args inp--- unless ret $--- error' $ quoteCmd c args +-+ "failed with:\n" ++ err---- | @cmdIgnoreErr c args inp@ runs a command with input, drops stderr, and return stdout-cmdIgnoreErr :: String -> [String] -> String -> IO String-cmdIgnoreErr c args input = do- (_ret, out, _err) <- cmdFull c args input- return out---- | @cmdFull c args inp@ runs readProcessWithExitCode and converts the ExitCode to Bool--- Removes the last newline from stdout and stderr (like the other functions)-cmdFull :: String -> [String] -> String -> IO (Bool, String, String)-cmdFull c args input = do- (ret, out, err) <- readProcessWithExitCode c args input- return (ret == ExitSuccess, removeTrailingNewline out, removeTrailingNewline err)---- | @cmdTry_ c args@ runs the command if available------ @since 0.2.1-cmdTry_ :: String -> [String] -> IO ()-cmdTry_ c args = do- have <- findExecutable c- when (isJust have) $- cmd_ c args---- | Redirect stderr to stdout, ie with interleaved output------ @since 0.2.2-cmdStderrToStdout :: String -> [String] -> IO (ExitCode, String)-cmdStderrToStdout c args = do- (_ , Just hout, _, p) <- createProcess ((proc c args)- {std_out = CreatePipe,- std_err = UseHandle stdout})- ret <- waitForProcess p- out <- hGetContents hout- return (ret, removeTrailingNewline out)---- | Redirect stderr to stdout, ie with interleaved output------ @since 0.2.3-cmdStderrToStdoutIn :: String -> [String] -> String -> IO (Bool, String)-cmdStderrToStdoutIn c args inp = do- (Just hin, Just hout, _, p) <- createProcess ((proc c args)- {std_in = CreatePipe,- std_out = CreatePipe,- std_err = UseHandle stdout})- hPutStr hin inp- ret <- waitForProcess p- out <- hGetContents hout- return (ret == ExitSuccess, removeTrailingNewline out)---- | @grep pat file@ greps pattern in file, and returns list of matches------ @since 0.1.2 (fixed not to error in 0.2.2)-grep :: String -> FilePath -> IO [String]-grep pat file = do- mres <- cmdMaybe "grep" [pat, file]- return $ maybe [] lines mres---- | @grep_ pat file@ greps pattern in file and returns Boolean status-grep_ :: String -- ^ pattern- -> FilePath -- ^ file- -> IO Bool -- ^ result-grep_ pat file =- cmdBool "grep" ["-q", pat, file]---- | @egrep_ pat file@ greps extended regexp in file, and returns Boolean status-egrep_ :: String -> FilePath -> IO Bool-egrep_ pat file =- cmdBool "grep" ["-q", "-e", pat, file]---- | @sudo c args@ runs a command as sudo returning stdout------ Result type changed from IO () to IO String in 0.2.0-sudo :: String -- ^ command- -> [String] -- ^ arguments- -> IO String-sudo = sudoInternal cmd---- | @sudo_ c args@ runs a command as sudo------ @since 0.2.0-sudo_ :: String -- ^ command- -> [String] -- ^ arguments- -> IO ()-sudo_ = sudoInternal cmdLog--sudoInternal :: (String -> [String] -> IO a) -> String -> [String] -> IO a-sudoInternal exc c args = do- uid <- getEffectiveUserID- sd <- if uid == 0- then return Nothing- else findExecutable "sudo"- let noSudo = isNothing sd- when (uid /= 0 && noSudo) $- warning "'sudo' not found"- exc (fromMaybe c sd) (if noSudo then args else c:args)---- | Combine two strings with a single space-infixr 4 +-+-(+-+) :: String -> String -> String-"" +-+ s = s-s +-+ "" = s-s +-+ t | last s == ' ' = s ++ t- | head t == ' ' = s ++ t-s +-+ t = s ++ " " ++ t---- singleLine :: String -> String--- singleLine "" = ""--- singleLine s = (head . lines) s---- | @removePrefix prefix original@ removes prefix from string if present-removePrefix :: String -> String-> String-removePrefix prefix orig =- fromMaybe orig $ stripPrefix prefix orig---- | @removeStrictPrefix prefix original@ removes prefix, or fails with error'-removeStrictPrefix :: String -> String -> String-removeStrictPrefix prefix orig =- fromMaybe (error' prefix +-+ "is not prefix of" +-+ orig) $ stripPrefix prefix orig---- | @removeSuffix suffix original@ removes suffix from string if present-removeSuffix :: String -> String -> String-removeSuffix suffix orig =- fromMaybe orig $ stripSuffix suffix orig- where- stripSuffix sf str = reverse <$> stripPrefix (reverse sf) (reverse str)---- | @warning@ outputs to stderr------ @since 0.2.0-warning :: String -> IO ()-warning s = hPutStrLn stderr $! s----- | Type alias for a command in a pipe------ @since 0.2.0-type PipeCommand = (String,[String])--withCreateProcessOutput :: CreateProcess -> (Handle -> ProcessHandle -> IO a) -> IO a-withCreateProcessOutput p act =- withCreateProcess p $- \ _si mso _se p' ->- case mso of- Nothing -> error $ "no stdout handle for: " ++ show (cmdspec p)- Just so -> act so p'---- | Return stdout from piping the output of one process to another------ @since 0.2.0-pipe :: PipeCommand -> PipeCommand -> IO String-pipe (c1,args1) (c2,args2) =- withCreateProcessOutput ((proc c1 args1) { std_out = CreatePipe }) $- \ ho1 p1 -> do- (_, mho2, _, p2) <- createProcess ((proc c2 args2) {std_in = UseHandle ho1, std_out = CreatePipe})- case mho2 of- Nothing -> error $ "no stdout handle for: " ++ c2- Just ho2 -> do- out <- hGetContents ho2- void $ waitForProcess p1- void $ waitForProcess p2- return $ removeTrailingNewline out---- | Pipe two commands without returning anything------ @since 0.2.0-pipe_ :: PipeCommand -> PipeCommand -> IO ()-pipe_ (c1,args1) (c2,args2) =- void $ pipeBool (c1,args1) (c2,args2)---- | Bool result of piping of commands--- @since 0.2.0--- Returns False if either command fails (since 0.2.4).-pipeBool :: PipeCommand -> PipeCommand -> IO Bool-pipeBool (c1,args1) (c2,args2) =- -- nicer with process-typed:- -- withProcess_ (setStdout createPipe proc1) $ \ p -> runProcess (setStdin (useHandleClose (getStdout p)) proc2)- withCreateProcess ((proc c1 args1) { std_out = CreatePipe }) $- \ _si so _se p1 -> do- p2 <- runProcess c2 args2 Nothing Nothing so Nothing Nothing- ok1 <- boolWrapper $ waitForProcess p1- ok2 <- boolWrapper $ waitForProcess p2- return $ ok1 && ok2---- | Pipe 3 commands, returning stdout------ @since 0.2.3-pipe3 :: PipeCommand -> PipeCommand -> PipeCommand -> IO String-pipe3 (c1,a1) (c2,a2) (c3,a3) =- withCreateProcessOutput ((proc c1 a1) { std_out = CreatePipe }) $- \ ho1 p1 ->- withCreateProcessOutput ((proc c2 a2) {std_in = UseHandle ho1, std_out = CreatePipe}) $- \ ho2 p2 -> do- (_, Just ho3, _, p3) <- createProcess ((proc c3 a3) {std_in = UseHandle ho2, std_out = CreatePipe})- out <- hGetContents ho3- forM_ [p1,p2,p3] waitForProcess- return $ removeTrailingNewline out---- | Pipe 3 commands, not returning anything------ @since 0.2.0-pipe3_ :: PipeCommand -> PipeCommand -> PipeCommand -> IO ()-pipe3_ (c1,a1) (c2,a2) (c3,a3) =- withCreateProcessOutput ((proc c1 a1) { std_out = CreatePipe }) $- \ ho1 p1 ->- withCreateProcess ((proc c2 a2) {std_in = UseHandle ho1, std_out = CreatePipe}) $- \ _hi2 mho2 _he2 p2 -> do- p3 <- runProcess c3 a3 Nothing Nothing mho2 Nothing Nothing- forM_ [p1,p2,p3] waitForProcess---- | Pipe a file to the first of a pipe of commands------ @since 0.2.0-pipeFile_ :: FilePath -> PipeCommand -> PipeCommand -> IO ()-pipeFile_ infile (c1,a1) (c2,a2) =- withFile infile ReadMode $- \ hin ->- withCreateProcess ((proc c1 a1) { std_in = UseHandle hin, std_out = CreatePipe }) $- \ _si so _se p1 -> do- p2 <- runProcess c2 a2 Nothing Nothing so Nothing Nothing- void $ waitForProcess p1- void $ waitForProcess p2---- | Assert program in PATH------ @needProgram progname@------ @since 0.2.1-needProgram :: String -> IO ()-needProgram prog = do- mx <- findExecutable prog- unless (isJust mx) $ error' $ "missing program: " ++ prog---- FIXME handle empty extension?--- | returns the files with the give extension-filesWithExtension :: FilePath -- directory- -> String -- file extension- -> IO [FilePath]-filesWithExtension dir ext =- filter (ext `isExtensionOf`) <$> listDirectory dir---- looks in dir for a unique file with given extension-fileWithExtension :: FilePath -- directory- -> String -- file extension- -> IO (Maybe FilePath)-fileWithExtension dir ext = do- files <- filesWithExtension dir ext- case files of- [file] -> return $ Just $ dir </> file- [] -> return Nothing- _ -> putStrLn ("More than one " ++ ext ++ " file found!") >> return Nothing--#if !MIN_VERSION_filepath(1,4,2)-isExtensionOf :: String -> FilePath -> Bool-isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions-isExtensionOf ext = isSuffixOf ('.':ext) . takeExtensions-#endif
− SimpleCmd/Git.hs
@@ -1,74 +0,0 @@-{-|-Some wrappers for git commands.--}--module SimpleCmd.Git (- git,- git_,- gitBool,- gitBranch,- gitDiffQuiet,- grepGitConfig,- isGitDir,- rwGitDir) where--import System.Directory (doesDirectoryExist)-import System.FilePath ((</>))--import SimpleCmd (cmd, cmd_, cmdBool, egrep_)--#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,0))-#else-import Control.Applicative ((<$>))-#endif---- | @git c args@ runs git command and return output-git :: String -- ^ git command- -> [String] -- ^ arguments- -> IO String -- ^ output-git c args =- cmd "git" (c:args)---- | @git_ c args@ run git command with output to stdout and stderr-git_ :: String -> [String] -> IO ()-git_ c args =- cmd_ "git" (c:args)---- | @gitBool c args@ runs git command and return result------ @since 0.2.2-gitBool :: String -- ^ git command- -> [String] -- ^ arguments- -> IO Bool -- ^ result-gitBool c args =- cmdBool "git" (c:args)---- | @isGitDir dir@ checks if directory has a .git/ subdir-isGitDir :: FilePath -> IO Bool-isGitDir dir = doesDirectoryExist (dir </> ".git")---- | @gitBranch@ returns the git branch of the current directory-gitBranch :: IO String-gitBranch =- git "rev-parse" ["--abbrev-ref", "HEAD"]---- | @rwGitDir@ checks if a git repo is under ssh-rwGitDir :: IO Bool-rwGitDir =- grepGitConfig "url = \\(ssh://\\|git@\\)"---- | @grepGitConfig pat@ greps ".git/config" for extended regexp------ @since 0.1.1-grepGitConfig :: String -> IO Bool-grepGitConfig key = do- gitdir <- isGitDir "."- if gitdir- then egrep_ key ".git/config"- else return False---- | @gitDiffQuiet@ checks if unchanged------ @since 0.1.3-gitDiffQuiet :: [String] -> IO Bool-gitDiffQuiet args = cmdBool "git" $ ["diff", "--quiet"] ++ args
− SimpleCmd/Rpm.hs
@@ -1,17 +0,0 @@-{-|-This Rpm module currently only provides @rpmspec@.--}--module SimpleCmd.Rpm (- rpmspec- ) where--import SimpleCmd (cmdLines)---- | @rpmspec args mqueryformat specfile@ runs rpmspec query on file with optional args (a newline is appended to any queryformat)------ @since 0.1.1-rpmspec :: [String] -> Maybe String -> FilePath -> IO [String]-rpmspec args mqf spec = do- let qf = maybe [] (\ q -> ["--queryformat", q ++ "\n"]) mqf- cmdLines "rpmspec" (["-q"] ++ args ++ qf ++ [spec])
simple-cmd.cabal view
@@ -1,5 +1,5 @@ name: simple-cmd-version: 0.2.4+version: 0.2.5 synopsis: Simple String-based process commands description: Simple wrappers over System.Process@@ -30,11 +30,13 @@ exposed-modules: SimpleCmd, SimpleCmd.Git, SimpleCmd.Rpm+ hs-source-dirs: src build-depends: base < 5,- directory,+ directory >= 1.2.5.0, extra, filepath, process >= 1.4.3.0,+ time, unix default-language: Haskell2010 default-extensions: CPP@@ -54,16 +56,16 @@ ghc-options: -Wunused-packages test-suite tests- type: exitcode-stdio-1.0- hs-source-dirs: test- main-is: Spec.hs- build-depends: hspec,- base < 5.0,- simple-cmd+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: hspec,+ base < 5.0,+ simple-cmd - default-language: Haskell2010+ default-language: Haskell2010 - ghc-options: -Wall+ ghc-options: -Wall if impl(ghc >= 8.0) ghc-options: -Wcompat -Widentities
+ src/SimpleCmd.hs view
@@ -0,0 +1,496 @@+{-# LANGUAGE CPP #-}++{-|+Some simple String wrappers of `readProcess`, `readProcessWithExitCode`,+`rawSystem` from the Haskell <https://hackage.haskell.org/package/process process> library.++Simplest is++@cmd_ :: String -> [String] -> IO ()@++which outputs to stdout. For example:++@cmd_ "git" ["clone", url]@++Then++@cmd :: String -> [String] -> IO String@++returns stdout as a @String@.++There are also @cmdBool@, @cmdMaybe@, @cmdLines@, @shell@, and others.++Other examples:++@grep_ pat file :: IO Bool@++@sudo c args :: IO ()@++-}++module SimpleCmd (+ cmd, cmd_,+ cmdBool,+ cmdIgnoreErr, {- badly named -}+ cmdLines,+ cmdMaybe,+ cmdFull,+ cmdLog, cmdlog {-TODO: remove for 0.3 -},+ cmdN,+ cmdQuiet,+ cmdSilent,+ cmdStdIn,+ cmdStdErr,+ cmdTry_,+ cmdStderrToStdout,+ cmdStderrToStdoutIn,+ needProgram,+ error',+ warning,+ logMsg,+ (+-+),+ removePrefix, removeStrictPrefix, removeSuffix,+ egrep_, grep, grep_,+ shell, shell_,+ shellBool,+ sudo, sudo_,+ PipeCommand,+ pipe, pipe_, pipeBool,+ pipe3, pipe3_, pipeFile_,+ ifM,+ whenM,+ filesWithExtension,+ fileWithExtension,+ timeIO+ ) where++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((<$>))+#endif+import Control.Exception+import Control.Monad.Extra++import Data.List (+#if !MIN_VERSION_filepath(1,4,2)+ isSuffixOf,+#endif+ stripPrefix)+import Data.Maybe (isJust, isNothing, fromMaybe)+import Data.Time.Clock++import System.Directory (findExecutable, listDirectory)+import System.Exit (ExitCode (..))+import System.FilePath+import System.IO (hGetContents, hPutStr, hPutStrLn, IOMode(ReadMode),+ stderr, stdout, withFile, Handle)+import System.Posix.User (getEffectiveUserID)+import System.Process (createProcess, CreateProcess (cmdspec), proc,+ ProcessHandle,+ rawSystem, readProcess,+ readProcessWithExitCode, runProcess, showCommandForUser,+ std_err, std_in, std_out,+ StdStream(CreatePipe, UseHandle),+ waitForProcess, withCreateProcess)++removeTrailingNewline :: String -> String+removeTrailingNewline "" = ""+removeTrailingNewline str =+ if last str == '\n'+ then init str+ else str++quoteCmd :: String -> [String] -> String+quoteCmd = showCommandForUser++-- | Alias for errorWithoutStackTrace (for base >= 4.9)+--+-- @since 0.1.4+error' :: String -> a+#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,9,0))+error' s = errorWithoutStackTrace $! s+#else+error' s = error $! s+#endif++-- | @cmd c args@ runs a command in a process and returns stdout+cmd :: String -- ^ command to run+ -> [String] -- ^ list of arguments+ -> IO String -- ^ stdout+cmd c args = cmdStdIn c args ""++-- | @cmd_ c args@ runs command in a process, output goes to stdout and stderr+cmd_ :: String -> [String] -> IO ()+cmd_ c args = do+ ret <- rawSystem c args+ case ret of+ ExitSuccess -> return ()+ ExitFailure n -> error' $ quoteCmd c args +-+ "failed with exit code" +-+ show n++boolWrapper :: IO ExitCode -> IO Bool+boolWrapper pr = do+ ret <- pr+ case ret of+ ExitSuccess -> return True+ ExitFailure _ -> return False++-- | @cmdBool c args@ runs a command, and return Boolean status+cmdBool :: String -> [String] -> IO Bool+cmdBool c args =+ boolWrapper (rawSystem c args)++-- | @cmdMaybe c args@ runs a command, maybe returning output if it succeeds+cmdMaybe :: String -> [String] -> IO (Maybe String)+cmdMaybe c args = do+ (ok, out, _err) <- cmdFull c args ""+ return $ if ok then Just out else Nothing++-- | @cmdLines c args@ runs a command, and returns list of stdout lines+--+-- @since 0.1.1+cmdLines :: String -> [String] -> IO [String]+cmdLines c args = lines <$> cmd c args++-- | @cmdStdIn c args inp@ runs a command, passing input string as stdin, and returns stdout+cmdStdIn :: String -> [String] -> String -> IO String+cmdStdIn c args inp = removeTrailingNewline <$> readProcess c args inp++-- | @shell cs@ runs a command string in a shell, and returns stdout+shell :: String -> IO String+shell cs = cmd "sh" ["-c", cs]++-- | @shell_ cs@ runs a command string in a shell, output goes to stdout+shell_ :: String -> IO ()+shell_ cs = cmd_ "sh" ["-c", cs]++-- | @shellBool cs@ runs a command string in a shell, output goes to stdout+--+-- @since 0.2.0+shellBool :: String -> IO Bool+shellBool cs =+ boolWrapper (rawSystem "sh" ["-c", cs])++-- FIXME cmdLog_+-- | @cmdLog c args@ logs a command with a datestamp+--+-- @since 0.1.4+cmdLog :: String -> [String] -> IO ()+cmdLog c args = do+ logMsg $ unwords $ c:args+ cmd_ c args++-- | @cmdlog@ deprecated alias for 'cmdLog' (will be removed in 0.3)+cmdlog :: String -> [String] -> IO ()+cmdlog = cmdLog++-- | @logMsg msg@ outputs message with a timestamp+logMsg :: String -> IO ()+logMsg msg = do+ date <- cmd "date" ["+%T"]+ putStrLn $ date +-+ msg++-- | @cmdN c args@ dry-runs a command: prints command to stdout - more used for debugging+cmdN :: String -> [String] -> IO ()+cmdN c args = putStrLn $ unwords $ c : map show args++-- | @cmdStdErr c args@ runs command in a process, returning stdout and stderr+cmdStdErr :: String -> [String] -> IO (String, String)+cmdStdErr c args = do+ (_ok, out, err) <- cmdFull c args ""+ return (out, err)++-- -- | @cmdAssert msg c args@ runs command, if it fails output msg as error'.+-- cmdAssert :: String -> String -> [String] -> IO ()+-- cmdAssert msg c args = do+-- ret <- rawSystem c args+-- case ret of+-- ExitSuccess -> return ()+-- ExitFailure _ -> error' msg++-- | @cmdQuiet c args@ runs a command hiding stderr, if it succeeds returns stdout+cmdQuiet :: String -> [String] -> IO String+cmdQuiet c args = do+ (ok, out, err) <- cmdFull c args ""+ return $ if ok+ then out+ else error' $ quoteCmd c args +-+ "failed with\n" ++ err++-- | @cmdSilent c args@ runs a command hiding stdout: stderr is only output if it fails.+cmdSilent :: String -> [String] -> IO ()+cmdSilent c args = do+ (ret, _, err) <- cmdFull c args ""+ unless ret $+ error' $ quoteCmd c args +-+ "failed with\n" ++ err++-- -- | @cmdSilentIn c args inp@ is like @cmdSilent@ but additionally takes some stdin+-- cmdSilentIn :: String -> [String] -> String -> IO ()+-- cmdSilentIn c args inp = do+-- (ret, _, err) <- cmdFull c args inp+-- unless ret $+-- error' $ quoteCmd c args +-+ "failed with:\n" ++ err++-- | @cmdIgnoreErr c args inp@ runs a command with input, drops stderr, and return stdout+cmdIgnoreErr :: String -> [String] -> String -> IO String+cmdIgnoreErr c args input = do+ (_ret, out, _err) <- cmdFull c args input+ return out++-- | @cmdFull c args inp@ runs readProcessWithExitCode and converts the ExitCode to Bool+-- Removes the last newline from stdout and stderr (like the other functions)+cmdFull :: String -> [String] -> String -> IO (Bool, String, String)+cmdFull c args input = do+ (ret, out, err) <- readProcessWithExitCode c args input+ return (ret == ExitSuccess, removeTrailingNewline out, removeTrailingNewline err)++-- | @cmdTry_ c args@ runs the command if available+--+-- @since 0.2.1+cmdTry_ :: String -> [String] -> IO ()+cmdTry_ c args = do+ have <- findExecutable c+ when (isJust have) $+ cmd_ c args++-- | Redirect stderr to stdout, ie with interleaved output+--+-- @since 0.2.2+cmdStderrToStdout :: String -> [String] -> IO (ExitCode, String)+cmdStderrToStdout c args = do+ (_ , Just hout, _, p) <- createProcess ((proc c args)+ {std_out = CreatePipe,+ std_err = UseHandle stdout})+ ret <- waitForProcess p+ out <- hGetContents hout+ return (ret, removeTrailingNewline out)++-- | Redirect stderr to stdout, ie with interleaved output+--+-- @since 0.2.3+cmdStderrToStdoutIn :: String -> [String] -> String -> IO (Bool, String)+cmdStderrToStdoutIn c args inp = do+ (Just hin, Just hout, _, p) <- createProcess ((proc c args)+ {std_in = CreatePipe,+ std_out = CreatePipe,+ std_err = UseHandle stdout})+ hPutStr hin inp+ ret <- waitForProcess p+ out <- hGetContents hout+ return (ret == ExitSuccess, removeTrailingNewline out)++-- | @grep pat file@ greps pattern in file, and returns list of matches+--+-- @since 0.1.2 (fixed not to error in 0.2.2)+grep :: String -> FilePath -> IO [String]+grep pat file = do+ mres <- cmdMaybe "grep" [pat, file]+ return $ maybe [] lines mres++-- | @grep_ pat file@ greps pattern in file and returns Boolean status+grep_ :: String -- ^ pattern+ -> FilePath -- ^ file+ -> IO Bool -- ^ result+grep_ pat file =+ cmdBool "grep" ["-q", pat, file]++-- | @egrep_ pat file@ greps extended regexp in file, and returns Boolean status+egrep_ :: String -> FilePath -> IO Bool+egrep_ pat file =+ cmdBool "grep" ["-q", "-e", pat, file]++-- | @sudo c args@ runs a command as sudo returning stdout+--+-- Result type changed from IO () to IO String in 0.2.0+sudo :: String -- ^ command+ -> [String] -- ^ arguments+ -> IO String+sudo = sudoInternal cmd++-- | @sudo_ c args@ runs a command as sudo+--+-- @since 0.2.0+sudo_ :: String -- ^ command+ -> [String] -- ^ arguments+ -> IO ()+sudo_ = sudoInternal cmdLog++sudoInternal :: (String -> [String] -> IO a) -> String -> [String] -> IO a+sudoInternal exc c args = do+ uid <- getEffectiveUserID+ sd <- if uid == 0+ then return Nothing+ else findExecutable "sudo"+ let noSudo = isNothing sd+ when (uid /= 0 && noSudo) $+ warning "'sudo' not found"+ exc (fromMaybe c sd) (if noSudo then args else c:args)++-- | Combine two strings with a single space+infixr 4 +-++(+-+) :: String -> String -> String+"" +-+ s = s+s +-+ "" = s+s +-+ t | last s == ' ' = s ++ t+ | head t == ' ' = s ++ t+s +-+ t = s ++ " " ++ t++-- singleLine :: String -> String+-- singleLine "" = ""+-- singleLine s = (head . lines) s++-- | @removePrefix prefix original@ removes prefix from string if present+removePrefix :: String -> String-> String+removePrefix prefix orig =+ fromMaybe orig $ stripPrefix prefix orig++-- | @removeStrictPrefix prefix original@ removes prefix, or fails with error'+removeStrictPrefix :: String -> String -> String+removeStrictPrefix prefix orig =+ fromMaybe (error' prefix +-+ "is not prefix of" +-+ orig) $ stripPrefix prefix orig++-- | @removeSuffix suffix original@ removes suffix from string if present+removeSuffix :: String -> String -> String+removeSuffix suffix orig =+ fromMaybe orig $ stripSuffix suffix orig+ where+ stripSuffix sf str = reverse <$> stripPrefix (reverse sf) (reverse str)++-- | @warning@ outputs to stderr+--+-- @since 0.2.0+warning :: String -> IO ()+warning s = hPutStrLn stderr $! s+++-- | Type alias for a command in a pipe+--+-- @since 0.2.0+type PipeCommand = (String,[String])++withCreateProcessOutput :: CreateProcess -> (Handle -> ProcessHandle -> IO a) -> IO a+withCreateProcessOutput p act =+ withCreateProcess p $+ \ _si mso _se p' ->+ case mso of+ Nothing -> error $ "no stdout handle for: " ++ show (cmdspec p)+ Just so -> act so p'++-- | Return stdout from piping the output of one process to another+--+-- @since 0.2.0+pipe :: PipeCommand -> PipeCommand -> IO String+pipe (c1,args1) (c2,args2) =+ withCreateProcessOutput ((proc c1 args1) { std_out = CreatePipe }) $+ \ ho1 p1 -> do+ (_, mho2, _, p2) <- createProcess ((proc c2 args2) {std_in = UseHandle ho1, std_out = CreatePipe})+ case mho2 of+ Nothing -> error $ "no stdout handle for: " ++ c2+ Just ho2 -> do+ out <- hGetContents ho2+ void $ waitForProcess p1+ void $ waitForProcess p2+ return $ removeTrailingNewline out++-- | Pipe two commands without returning anything+--+-- @since 0.2.0+pipe_ :: PipeCommand -> PipeCommand -> IO ()+pipe_ (c1,args1) (c2,args2) =+ void $ pipeBool (c1,args1) (c2,args2)++-- | Bool result of piping of commands+-- @since 0.2.0+-- Returns False if either command fails (since 0.2.4).+pipeBool :: PipeCommand -> PipeCommand -> IO Bool+pipeBool (c1,args1) (c2,args2) =+ -- nicer with process-typed:+ -- withProcess_ (setStdout createPipe proc1) $ \ p -> runProcess (setStdin (useHandleClose (getStdout p)) proc2)+ withCreateProcess ((proc c1 args1) { std_out = CreatePipe }) $+ \ _si so _se p1 -> do+ p2 <- runProcess c2 args2 Nothing Nothing so Nothing Nothing+ ok1 <- boolWrapper $ waitForProcess p1+ ok2 <- boolWrapper $ waitForProcess p2+ return $ ok1 && ok2++-- | Pipe 3 commands, returning stdout+--+-- @since 0.2.3+pipe3 :: PipeCommand -> PipeCommand -> PipeCommand -> IO String+pipe3 (c1,a1) (c2,a2) (c3,a3) =+ withCreateProcessOutput ((proc c1 a1) { std_out = CreatePipe }) $+ \ ho1 p1 ->+ withCreateProcessOutput ((proc c2 a2) {std_in = UseHandle ho1, std_out = CreatePipe}) $+ \ ho2 p2 -> do+ (_, Just ho3, _, p3) <- createProcess ((proc c3 a3) {std_in = UseHandle ho2, std_out = CreatePipe})+ out <- hGetContents ho3+ forM_ [p1,p2,p3] waitForProcess+ return $ removeTrailingNewline out++-- | Pipe 3 commands, not returning anything+--+-- @since 0.2.0+pipe3_ :: PipeCommand -> PipeCommand -> PipeCommand -> IO ()+pipe3_ (c1,a1) (c2,a2) (c3,a3) =+ withCreateProcessOutput ((proc c1 a1) { std_out = CreatePipe }) $+ \ ho1 p1 ->+ withCreateProcess ((proc c2 a2) {std_in = UseHandle ho1, std_out = CreatePipe}) $+ \ _hi2 mho2 _he2 p2 -> do+ p3 <- runProcess c3 a3 Nothing Nothing mho2 Nothing Nothing+ forM_ [p1,p2,p3] waitForProcess++-- | Pipe a file to the first of a pipe of commands+--+-- @since 0.2.0+pipeFile_ :: FilePath -> PipeCommand -> PipeCommand -> IO ()+pipeFile_ infile (c1,a1) (c2,a2) =+ withFile infile ReadMode $+ \ hin ->+ withCreateProcess ((proc c1 a1) { std_in = UseHandle hin, std_out = CreatePipe }) $+ \ _si so _se p1 -> do+ p2 <- runProcess c2 a2 Nothing Nothing so Nothing Nothing+ void $ waitForProcess p1+ void $ waitForProcess p2++-- | Assert program in PATH+--+-- @needProgram progname@+--+-- @since 0.2.1+needProgram :: String -> IO ()+needProgram prog = do+ mx <- findExecutable prog+ unless (isJust mx) $ error' $ "missing program: " ++ prog++-- FIXME handle empty extension?+-- | returns the files with the give extension+filesWithExtension :: FilePath -- directory+ -> String -- file extension+ -> IO [FilePath]+filesWithExtension dir ext =+ filter (ext `isExtensionOf`) <$> listDirectory dir++-- looks in dir for a unique file with given extension+fileWithExtension :: FilePath -- directory+ -> String -- file extension+ -> IO (Maybe FilePath)+fileWithExtension dir ext = do+ files <- filesWithExtension dir ext+ case files of+ [file] -> return $ Just $ dir </> file+ [] -> return Nothing+ _ -> putStrLn ("More than one " ++ ext ++ " file found!") >> return Nothing++#if !MIN_VERSION_filepath(1,4,2)+isExtensionOf :: String -> FilePath -> Bool+isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions+isExtensionOf ext = isSuffixOf ('.':ext) . takeExtensions+#endif++-- | Run an IO action and then print how long it took+timeIO :: IO a -> IO a+timeIO action = do+ bracket+ getCurrentTime+ (\start -> do+ end <- getCurrentTime+ let duration = diffUTCTime end start+ putStrLn $ "took " ++ show duration)+ (const action)
+ src/SimpleCmd/Git.hs view
@@ -0,0 +1,74 @@+{-|+Some wrappers for git commands.+-}++module SimpleCmd.Git (+ git,+ git_,+ gitBool,+ gitBranch,+ gitDiffQuiet,+ grepGitConfig,+ isGitDir,+ rwGitDir) where++import System.Directory (doesDirectoryExist)+import System.FilePath ((</>))++import SimpleCmd (cmd, cmd_, cmdBool, egrep_)++#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,0))+#else+import Control.Applicative ((<$>))+#endif++-- | @git c args@ runs git command and return output+git :: String -- ^ git command+ -> [String] -- ^ arguments+ -> IO String -- ^ output+git c args =+ cmd "git" (c:args)++-- | @git_ c args@ run git command with output to stdout and stderr+git_ :: String -> [String] -> IO ()+git_ c args =+ cmd_ "git" (c:args)++-- | @gitBool c args@ runs git command and return result+--+-- @since 0.2.2+gitBool :: String -- ^ git command+ -> [String] -- ^ arguments+ -> IO Bool -- ^ result+gitBool c args =+ cmdBool "git" (c:args)++-- | @isGitDir dir@ checks if directory has a .git/ subdir+isGitDir :: FilePath -> IO Bool+isGitDir dir = doesDirectoryExist (dir </> ".git")++-- | @gitBranch@ returns the git branch of the current directory+gitBranch :: IO String+gitBranch =+ git "rev-parse" ["--abbrev-ref", "HEAD"]++-- | @rwGitDir@ checks if a git repo is under ssh+rwGitDir :: IO Bool+rwGitDir =+ grepGitConfig "url = \\(ssh://\\|git@\\)"++-- | @grepGitConfig pat@ greps ".git/config" for extended regexp+--+-- @since 0.1.1+grepGitConfig :: String -> IO Bool+grepGitConfig key = do+ gitdir <- isGitDir "."+ if gitdir+ then egrep_ key ".git/config"+ else return False++-- | @gitDiffQuiet@ checks if unchanged+--+-- @since 0.1.3+gitDiffQuiet :: [String] -> IO Bool+gitDiffQuiet args = cmdBool "git" $ ["diff", "--quiet"] ++ args
+ src/SimpleCmd/Rpm.hs view
@@ -0,0 +1,17 @@+{-|+This Rpm module currently only provides @rpmspec@.+-}++module SimpleCmd.Rpm (+ rpmspec+ ) where++import SimpleCmd (cmdLines)++-- | @rpmspec args mqueryformat specfile@ runs rpmspec query on file with optional args (a newline is appended to any queryformat)+--+-- @since 0.1.1+rpmspec :: [String] -> Maybe String -> FilePath -> IO [String]+rpmspec args mqf spec = do+ let qf = maybe [] (\ q -> ["--queryformat", q ++ "\n"]) mqf+ cmdLines "rpmspec" (["-q"] ++ args ++ qf ++ [spec])
test/Spec.hs view
@@ -23,7 +23,14 @@ out <- cmdLines "echo" ["1\n2"] out `shouldBe` ["1","2"] + it "cmdN" $+ cmdN "ls" ["*"]+ describe "pipes" $ do it "pipe" $ do out <- pipe ("echo", ["hello"]) ("grep",["hello"]) out `shouldBe` "hello"++ describe "timeIO" $ do+ it "sleep" $+ timeIO $ cmd_ "sleep" ["1"]